diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 792f727..9b26d02 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -7,7 +7,12 @@ import { fetchFromApi, requestHeaderFrom } from "../../api/request"; -import { SearchResponse, SearchResponseWithCount } from "../../models/models"; +import { + SearchResponse, + SearchResponseWithCount, + SearchSuccessResult +} from "../../models/models"; +import { ProblemDetails, isProblemDetails } from "../../models/problemDetails"; import { requestContextFrom } from "../../utils/requestContext"; import { buildQueryParams, buildUrl, sluggize } from "../../utils/url"; import { Renderer } from "./renderer"; @@ -18,65 +23,68 @@ const emptyResult = { } as SearchResponseWithCount; export default async function Page(req: any) { - const { props } = await handler(req); - return ; + if (req.searchParams["q"] === undefined) { + const err = { + title: "Unknown Error", + status: 422, + detail: "Unknown Error", + instance: "Unknown Error", + errors: [] as Array + } as ProblemDetails; + return ; + } + + const qs = + req.searchParams["q"] instanceof Array + ? req.searchParams["q"] + : [req.searchParams["q"]]; + + const result: SearchSuccessResult | ProblemDetails = await handler(req, qs); + return ; } -async function handler(req: any) { +async function handler( + req: any, + qs: Array +): Promise { // TODO: refactor // TODO: assert query params before POST to server try { - if (req.searchParams["q"] === undefined) { - return { - props: { - statusCode: req.res.statusCode, - hits: 0, - count: 0, - contents: [], - queryStrings: [] - } - }; - } - const qs = - req.searchParams["q"] instanceof Array - ? req.searchParams["q"] - : [req.searchParams["q"]]; - if (qs.length > 0) { - const result = await execute(req, qs); - return { - props: { - statusCode: 422, // TODO - hits: result.count, - count: result.contents.length, - contents: result.contents, - queryStrings: qs - } - }; - } else { + const result: SearchResponseWithCount | ProblemDetails = await execute( + req, + qs + ); + if (isProblemDetails(result)) { return { - props: { - statusCode: 422, // TODO - hits: 0, - count: 0, - contents: [], - queryStrings: qs - } + title: result.title, + status: result.status, + detail: result.detail, + instance: result.instance, + errors: result.errors }; } - } catch { return { - props: { - statusCode: 422, // TODO - hits: 0, - count: 0, - contents: [], - queryStrings: [] - } - }; + statusCode: 200, + hits: result.count, + count: result.contents.length, + contents: result.contents, + queryStrings: qs + } as SearchSuccessResult; + } catch (e) { + return { + title: "Unknown Error", + status: 500, + detail: "Unknown Error", + instance: "Unknown Error", + errors: [] as Array + } as ProblemDetails; } } -async function execute(req, words: Array) { +async function execute( + req, + words: Array +): Promise { // TODO: devide into another `function` and move `api` dir. const url = buildUrl(api.url, sluggize(["v1", "search"]), false); const ctx = requestContextFrom(headers()); @@ -86,28 +94,32 @@ async function execute(req, words: Array) { params: { key: "q", values: words } }) }; + const response = await fetchFromApi(url, options); + const responseBody = await response.json(); if (response.status !== 200) { - return emptyResult; + return responseBody as ProblemDetails; } - const sr = (await response.json()) as SearchResponseWithCount; - if (sr.count === 0) { + + const searchResponseWithCount = responseBody as SearchResponseWithCount; + if (searchResponseWithCount.count === 0) { return emptyResult; } let contents = []; - contents = sr.contents.map((content) => { - return { - path: content.path, - title: content.title, - content: content.content, - publishedAt: content.publishedAt - } as SearchResponse; - }); + contents = (searchResponseWithCount as SearchResponseWithCount).contents.map( + (content) => { + return { + path: content.path, + title: content.title, + content: content.content, + publishedAt: content.publishedAt + } as SearchResponse; + } + ); return { - count: sr.count, + count: searchResponseWithCount.count, contents: contents } as SearchResponseWithCount; - // TODO: Error handling } diff --git a/src/app/search/renderer.tsx b/src/app/search/renderer.tsx index cb93c17..b1d39c3 100644 --- a/src/app/search/renderer.tsx +++ b/src/app/search/renderer.tsx @@ -6,35 +6,46 @@ import { CoverComponent, SearchResultComponent } from "../../components/components"; -import { SearchResponse, SearchResponseWithCount } from "../../models/models"; +import { SearchSuccessResult } from "../../models/models"; +import { ProblemDetails, isProblemDetails } from "../../models/problemDetails"; import containerStyles from "../../styles/components/container.module.scss"; export const Renderer: React.FunctionComponent<{ - hits: number; - count: number; - contents: Array; - queryStrings: Array; -}> = ({ hits, count, contents, queryStrings }) => { - let contentsWithCount: SearchResponseWithCount = { - count: 0, - contents: [] + props: SearchSuccessResult | ProblemDetails; + qs: Array; +}> = ({ props, qs }) => { + let result: SearchSuccessResult = { + statusCode: 200, + queryStrings: [], + contents: [], + hits: 0, + count: 0 }; + + if (!isProblemDetails(props)) { + const s = props as SearchSuccessResult; + result.queryStrings = qs; + result.contents = s.contents; + result.hits = s.hits; + result.count = s.count; + } + const router = useRouter(); - const [searchWord, setSearchWord] = useState(queryStrings.join(" ")); - const [searchResults, setSearchResults] = useState(contents); + const [searchWord, setSearchWord] = useState(result.queryStrings.join(" ")); + const [searchResults, setSearchResults] = useState(result.contents); useEffect(() => { if (searchWord === "") { - setSearchResults(contentsWithCount.contents); + setSearchResults(result.contents); return; } const searchKeywords = searchWord.trim().toLowerCase(); if (searchKeywords.length == 0) { - setSearchResults(contentsWithCount.contents); + setSearchResults(result.contents); return; } - setSearchResults(contents); + setSearchResults(result.contents); }, [searchWord]); return ( @@ -48,25 +59,24 @@ export const Renderer: React.FunctionComponent<{ />
-
-

- NOTE: - The search feature is WIP and it has some - undocumented limitations. For more details please see belows. -

- -
+ {(() => { + if (isProblemDetails(props)) { + const messages = []; + props.errors.forEach((e, idx) => { + // @ts-ignore + messages.push(
  • {e.message}
  • ); + }); + return ( +
    +

    + ERROR +

    +
      {messages}
    +
    + ); + } + })()} +
    { e.preventDefault(); @@ -86,9 +96,9 @@ export const Renderer: React.FunctionComponent<{ />
    diff --git a/src/models/problemDetails.ts b/src/models/problemDetails.ts new file mode 100644 index 0000000..ebe0122 --- /dev/null +++ b/src/models/problemDetails.ts @@ -0,0 +1,26 @@ +// https://datatracker.ietf.org/doc/html/rfc7807 +export interface ProblemDetails { + title: string; + status: number; + detail: string; + instance: string; + errors: [code: string, message: string]; +} + +// TODO: Write test code +export function isProblemDetails(value: unknown): value is ProblemDetails { + if (typeof value !== "object" || value === null) { + return false; + } + + const { status, errors } = value as Record; + if (typeof status !== "number") { + return false; + } + + if (Array.isArray(errors)) { + return true; + } + + return false; +} diff --git a/src/models/search.ts b/src/models/search.ts index 035191d..14826c2 100644 --- a/src/models/search.ts +++ b/src/models/search.ts @@ -10,3 +10,11 @@ export interface SearchResponseWithCount { count: number; contents: Array; } + +export interface SearchSuccessResult { + statusCode: number; + hits: number; + count: number; + contents: Array; + queryStrings: Array; +}