diff --git a/messages/ja.json b/messages/ja.json index 922608a..34af6fb 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -58,30 +58,6 @@ "/components/DialectSelector/DialectSelector": { "legend": "方言" }, - "/components/DialectSelector/useDialects": { - "karahuto": "樺太", - "east_coast": "西海岸", - "otasu": "小田洲", - "ushiro": "鵜城", - "raichishi": "来知志", - "hokkaido": "北海道", - "southwest": "南西", - "northeast": "北東", - "saru": "沙流", - "chitose": "千歳", - "mukawa": "鵡川", - "horobetsu": "幌別", - "abuta": "虻田", - "shiraoi": "白老", - "shizunai": "静内", - "ishikari": "石狩", - "shiranuka": "白糠", - "tokachi": "十勝", - "kushiro": "釧路", - "urakawa": "浦河", - "bihoro": "美幌", - "samani": "様似" - }, "/components/DialectSelector/DialectSelectorCheckbox": { "count": "{count}件)" }, @@ -137,5 +113,29 @@ }, "/components/Search/Search": { "label": "キーワード" + }, + "/hooks/useDialectFormatter": { + "karahuto": "樺太", + "east_coast": "西海岸", + "otasu": "小田洲", + "ushiro": "鵜城", + "raichishi": "来知志", + "hokkaido": "北海道", + "southwest": "南西", + "northeast": "北東", + "saru": "沙流", + "chitose": "千歳", + "mukawa": "鵡川", + "horobetsu": "幌別", + "abuta": "虻田", + "shiraoi": "白老", + "shizunai": "静内", + "ishikari": "石狩", + "shiranuka": "白糠", + "tokachi": "十勝", + "kushiro": "釧路", + "urakawa": "浦河", + "bihoro": "美幌", + "samani": "様似" } } diff --git a/src/app/[locale]/search/Result.tsx b/src/app/[locale]/search/Result.tsx index e16f3e4..9f9002b 100644 --- a/src/app/[locale]/search/Result.tsx +++ b/src/app/[locale]/search/Result.tsx @@ -50,7 +50,6 @@ const ResultRoot: FC = (props) => { document={hit.document} uri={hit.uri} author={hit.author} - dialect={hit.dialect} dialectLv1={hit.dialect_lv1} dialectLv2={hit.dialect_lv2} dialectLv3={hit.dialect_lv3} diff --git a/src/components/Hierarchy/Hierarchy.tsx b/src/components/Breadcrumb/Breadcrumb.tsx similarity index 67% rename from src/components/Hierarchy/Hierarchy.tsx rename to src/components/Breadcrumb/Breadcrumb.tsx index 572f3ef..8a5536d 100644 --- a/src/components/Hierarchy/Hierarchy.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -2,21 +2,21 @@ import { FC, ReactNode, useMemo } from "react"; import { ChevronRightIcon } from "@radix-ui/react-icons"; import { Flex } from "@radix-ui/themes"; -export type HierarchyProps = { - children: string | null; +export type BreadcrumbProps = { + values: ReactNode[]; }; -export const Hierarchy: FC = (props) => { - const { children } = props; +export const Breadcrumb: FC = (props) => { + const { values } = props; const nodes = useMemo(() => { const nodes: ReactNode[] = []; - if (!children) { + if (!values) { return null; } - for (const [key, fragment] of Object.entries(children.split("/"))) { + for (const [key, fragment] of Object.entries(values)) { if (nodes.length !== 0) { nodes.push(); } @@ -25,7 +25,7 @@ export const Hierarchy: FC = (props) => { } return nodes; - }, [children]); + }, [values]); if (!nodes) { return null; diff --git a/src/components/Breadcrumb/index.ts b/src/components/Breadcrumb/index.ts new file mode 100644 index 0000000..3bc34d1 --- /dev/null +++ b/src/components/Breadcrumb/index.ts @@ -0,0 +1 @@ +export * from "./Breadcrumb"; diff --git a/src/components/DialectSelector/useDialects.ts b/src/components/DialectSelector/useDialects.ts index 0e1dff7..e0a4e87 100644 --- a/src/components/DialectSelector/useDialects.ts +++ b/src/components/DialectSelector/useDialects.ts @@ -1,4 +1,4 @@ -import { useTranslations } from "next-intl"; +import { useDialectFormatter } from "@/hooks/useDialectFormatter"; export type Dialect = { label: string; @@ -7,52 +7,61 @@ export type Dialect = { }; export const useDialects = (): Dialect[] => { - const t = useTranslations("/components/DialectSelector/useDialects"); + const format = useDialectFormatter(); const dialects: Dialect[] = [ { - label: t("karahuto"), + label: format("樺太"), value: "樺太", children: [ { - label: t("east_coast"), + label: format("樺太/西海岸"), value: "樺太/西海岸", children: [ - { label: t("otasu"), value: "樺太/西海岸/小田洲" }, - { label: t("ushiro"), value: "樺太/西海岸/鵜城" }, - { label: t("raichishi"), value: "樺太/西海岸/来知志" }, + { + label: format("樺太/西海岸/小田洲"), + value: "樺太/西海岸/小田洲", + }, + { + label: format("樺太/西海岸/鵜城"), + value: "樺太/西海岸/鵜城", + }, + { + label: format("樺太/西海岸/来知志"), + value: "樺太/西海岸/来知志", + }, ], }, ], }, { - label: t("hokkaido"), + label: format("北海道"), value: "北海道", children: [ { - label: t("southwest"), + label: format("北海道/南西"), value: "北海道/南西", children: [ - { label: t("saru"), value: "北海道/南西/沙流" }, - { label: t("chitose"), value: "北海道/南西/千歳" }, - { label: t("mukawa"), value: "北海道/南西/鵡川" }, - { label: t("horobetsu"), value: "北海道/南西/幌別" }, - { label: t("abuta"), value: "北海道/南西/虻田" }, - { label: t("shiraoi"), value: "北海道/南西/白老" }, + { label: format("北海道/南西/沙流"), value: "北海道/南西/沙流" }, + { label: format("北海道/南西/千歳"), value: "北海道/南西/千歳" }, + { label: format("北海道/南西/鵡川"), value: "北海道/南西/鵡川" }, + { label: format("北海道/南西/幌別"), value: "北海道/南西/幌別" }, + { label: format("北海道/南西/虻田"), value: "北海道/南西/虻田" }, + { label: format("北海道/南西/白老"), value: "北海道/南西/白老" }, ], }, { - label: t("northeast"), + label: format("北海道/北東"), value: "北海道/北東", children: [ - { label: t("shizunai"), value: "北海道/北東/静内" }, - { label: t("ishikari"), value: "北海道/北東/石狩" }, - { label: t("shiranuka"), value: "北海道/北東/白糠" }, - { label: t("tokachi"), value: "北海道/北東/十勝" }, - { label: t("kushiro"), value: "北海道/北東/釧路" }, - { label: t("urakawa"), value: "北海道/北東/浦河" }, - { label: t("bihoro"), value: "北海道/北東/美幌" }, - { label: t("samani"), value: "北海道/北東/様似" }, + { label: format("北海道/北東/静内"), value: "北海道/北東/静内" }, + { label: format("北海道/北東/石狩"), value: "北海道/北東/石狩" }, + { label: format("北海道/北東/白糠"), value: "北海道/北東/白糠" }, + { label: format("北海道/北東/十勝"), value: "北海道/北東/十勝" }, + { label: format("北海道/北東/釧路"), value: "北海道/北東/釧路" }, + { label: format("北海道/北東/浦河"), value: "北海道/北東/浦河" }, + { label: format("北海道/北東/美幌"), value: "北海道/北東/美幌" }, + { label: format("北海道/北東/様似"), value: "北海道/北東/様似" }, ], }, ], diff --git a/src/components/Entry/Entry.tsx b/src/components/Entry/Entry.tsx index fdb1aa7..07e363f 100644 --- a/src/components/Entry/Entry.tsx +++ b/src/components/Entry/Entry.tsx @@ -30,7 +30,6 @@ export type EntryRootProps = { collectionLv3: string | null; uri: string | null; author: string | null; - dialect: string | null; dialectLv1: string[] | null; dialectLv2: string[] | null; dialectLv3: string[] | null; @@ -49,7 +48,6 @@ const EntryRoot: React.FC = (props) => { document, uri, author, - dialect, dialectLv1, dialectLv2, dialectLv3, @@ -119,9 +117,14 @@ const EntryRoot: React.FC = (props) => { )} - {(author || dialect) && ( + {(author || dialectLv1 || dialectLv2 || dialectLv3) && ( - + )} @@ -132,7 +135,6 @@ const EntryRoot: React.FC = (props) => { collectionLv3={collectionLv3} document={document} author={author} - dialect={dialect} dialectLv1={dialectLv1} dialectLv2={dialectLv2} dialectLv3={dialectLv3} diff --git a/src/components/Entry/EntryAuthor.tsx b/src/components/Entry/EntryAuthor.tsx index 306654c..5c53704 100644 --- a/src/components/Entry/EntryAuthor.tsx +++ b/src/components/Entry/EntryAuthor.tsx @@ -1,23 +1,38 @@ +import { useDialectFormatter } from "@/hooks/useDialectFormatter"; +import { getMostDetailedDialects } from "@/utils/getMostDetailedDialects"; import { Text, VisuallyHidden } from "@radix-ui/themes"; import { useTranslations } from "next-intl"; import { FC } from "react"; type EntryAuthorProps = { author: string | null; - dialect: string | null; + dialectLv1: string[] | null; + dialectLv2: string[] | null; + dialectLv3: string[] | null; }; export const EntryAuthor: FC = (props) => { - const { author, dialect } = props; + const { author, dialectLv1, dialectLv2, dialectLv3 } = props; const t = useTranslations("/components/Entry/EntryAuthor"); + const formatDialect = useDialectFormatter(); - if (author && dialect) { + const dialects = getMostDetailedDialects( + dialectLv1 ?? [], + dialectLv2 ?? [], + dialectLv3 ?? [], + ); + const formattedDialects = + dialects.length > 0 + ? dialects.map((dialect) => formatDialect(dialect)).join(", ") + : null; + + if (author && formattedDialects) { return ( {t.rich("author_with_dialect", { author, - dialect, + dialect: formattedDialects, vh: (chunks) => {chunks}, })} @@ -35,11 +50,11 @@ export const EntryAuthor: FC = (props) => { ); } - if (dialect) { + if (formattedDialects) { return ( {t.rich("dialect", { - dialect, + dialect: formattedDialects, vh: (chunks) => {chunks}, })} diff --git a/src/components/Entry/EntryDetailsDialog.tsx b/src/components/Entry/EntryDetailsDialog.tsx index 7f9a247..8b87432 100644 --- a/src/components/Entry/EntryDetailsDialog.tsx +++ b/src/components/Entry/EntryDetailsDialog.tsx @@ -2,7 +2,6 @@ import { CopyIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"; import { - Badge, Button, Code, DataList, @@ -12,16 +11,16 @@ import { Link, Text, Tooltip, - VisuallyHidden, } from "@radix-ui/themes"; import { FC, useCallback } from "react"; import dayjs from "dayjs"; +import { useLocale, useTranslations } from "next-intl"; import { formatDateOrRange } from "@/utils/timestamp"; import { toHref } from "@/utils/uri"; -import { useLocale, useTranslations } from "next-intl"; +import { getMostDetailedDialects } from "@/utils/getMostDetailedDialects"; -import { Hierarchy } from "../Hierarchy/Hierarchy"; +import { Breadcrumb } from "../Breadcrumb"; const NoData = () => { const t = useTranslations("/components/Entry/EntryDetailsDialog"); @@ -35,7 +34,6 @@ export type EntryDetailsDialogProps = { collectionLv2: string | null; collectionLv3: string | null; author: string | null; - dialect: string | null; dialectLv1: string[] | null; dialectLv2: string[] | null; dialectLv3: string[] | null; @@ -52,8 +50,10 @@ export const EntryDetailsDialog: FC = (props) => { collectionLv3, document, author, - dialect, uri, + dialectLv1, + dialectLv2, + dialectLv3, recordedAt, publishedAt, } = props; @@ -62,6 +62,16 @@ export const EntryDetailsDialog: FC = (props) => { const t = useTranslations("/components/Entry/EntryDetailsDialog"); const href = uri ? toHref(uri) : null; + const hasDialect = !!(dialectLv1 || dialectLv2 || dialectLv3); + + const mostDetailedDialects = hasDialect + ? getMostDetailedDialects( + dialectLv1 ?? [], + dialectLv2 ?? [], + dialectLv3 ?? [], + ) + : null; + dayjs.locale(locale); const handleCopy = useCallback(() => { @@ -107,9 +117,14 @@ export const EntryDetailsDialog: FC = (props) => { {t("collection")} {collectionLv3 || collectionLv2 || collectionLv1 ? ( - - {collectionLv3 ?? collectionLv2 ?? collectionLv1} - + ) : ( )} @@ -129,7 +144,15 @@ export const EntryDetailsDialog: FC = (props) => { {t("dialect")} - {dialect ? {dialect} : } + + {mostDetailedDialects ? ( + mostDetailedDialects.map((dialect) => ( + + )) + ) : ( + + )} + diff --git a/src/components/Hierarchy/index.ts b/src/components/Hierarchy/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/hooks/useDialectFormatter.ts b/src/hooks/useDialectFormatter.ts new file mode 100644 index 0000000..640e619 --- /dev/null +++ b/src/hooks/useDialectFormatter.ts @@ -0,0 +1,67 @@ +import { useTranslations } from "next-intl"; +import { useCallback } from "react"; + +type UseDialectFormatterOutput = (dialect: string) => string; + +export const useDialectFormatter = (): UseDialectFormatterOutput => { + const t = useTranslations("/hooks/useDialectFormatter"); + + const formatter = useCallback( + (dialect: string) => { + switch (dialect) { + case "樺太": + return t("karahuto"); + case "樺太/西海岸": + return t("east_coast"); + case "樺太/西海岸/小田洲": + return t("otasu"); + case "樺太/西海岸/鵜城": + return t("ushiro"); + case "樺太/西海岸/来知志": + return t("raichishi"); + + case "北海道": + return t("hokkaido"); + case "北海道/南西": + return t("southwest"); + case "北海道/南西/沙流": + return t("saru"); + case "北海道/南西/千歳": + return t("chitose"); + case "北海道/南西/鵡川": + return t("mukawa"); + case "北海道/南西/幌別": + return t("horobetsu"); + case "北海道/南西/虻田": + return t("abuta"); + case "北海道/南西/白老": + return t("shiraoi"); + + case "北海道/北東/静内": + return t("shizunai"); + case "北海道/北東/石狩": + return t("ishikari"); + case "北海道/北東/白糠": + return t("shiranuka"); + case "北海道/北東/十勝": + return t("tokachi"); + case "北海道/北東/釧路": + return t("kushiro"); + case "北海道/北東/浦河": + return t("urakawa"); + case "北海道/北東/美幌": + return t("bihoro"); + case "北海道/北東/様似": + return t("samani"); + + case "北海道/北東": + return t("northeast"); + default: + return dialect; + } + }, + [t], + ); + + return formatter; +}; diff --git a/src/utils/getMostDetailedDialects.spec.ts b/src/utils/getMostDetailedDialects.spec.ts new file mode 100644 index 0000000..7d9d1db --- /dev/null +++ b/src/utils/getMostDetailedDialects.spec.ts @@ -0,0 +1,12 @@ +import { expect, test } from "vitest"; +import { getMostDetailedDialects } from "./getMostDetailedDialects"; + +test("getMostDetailedDialects", () => { + const dialects = getMostDetailedDialects( + ["北海道", "樺太"], + ["北海道/南西"], + ["北海道/南西/千歳"], + ); + + expect(dialects).toEqual(["北海道/南西/千歳", "樺太"]); +}); diff --git a/src/utils/getMostDetailedDialects.ts b/src/utils/getMostDetailedDialects.ts new file mode 100644 index 0000000..5db390a --- /dev/null +++ b/src/utils/getMostDetailedDialects.ts @@ -0,0 +1,29 @@ +export const getMostDetailedDialects = ( + lv1: string[], + lv2: string[], + lv3: string[], +): string[] => { + const dialects: string[] = []; + + for (const d1 of lv1) { + if (lv2.some((d2) => d2.startsWith(d1))) { + continue; + } + + dialects.push(d1); + } + + for (const d2 of lv2) { + if (lv3.some((d3) => d3.startsWith(d3))) { + continue; + } + + dialects.push(d2); + } + + dialects.push(...lv3); + + dialects.sort(); + + return dialects; +};