diff --git a/apps/hudozka/src/components/App/index.tsx b/apps/hudozka/src/components/App/index.tsx index 2038ce8a..3fb0f04a 100644 --- a/apps/hudozka/src/components/App/index.tsx +++ b/apps/hudozka/src/components/App/index.tsx @@ -5,12 +5,11 @@ import s from "./app.module.css" import { Wrapper } from "../Wrapper" import { Footer } from "../Footer" import { Content } from "../Content" -import { Breadcrumbs } from "../Breadcrumbs" import type { BreadcrumbPart } from "@/types" import { usePathname } from "next/navigation" import { useMobile } from "@hudozka/hooks" import { Navigation } from "@/components/Navigation" -import { Box, Panel, Title } from "@hudozka/ui" +import { Box, Breadcrumbs, Panel, Title } from "@hudozka/ui" import { AccessibilityButton } from "../AccessibilityButton" import { Copyright } from "@/components/Copyright" import { yearRange } from "@/const" diff --git a/apps/hudozka/src/remote/api.ts b/apps/hudozka/src/remote/api.ts index efad4fac..316c75b9 100644 --- a/apps/hudozka/src/remote/api.ts +++ b/apps/hudozka/src/remote/api.ts @@ -2,7 +2,43 @@ import { pb } from "./pb" import { createPage, createHomeCards, createMenu, createFeedPages } from "./factory" import type { PbPage, PbImage, PbFile, PbTag, PbHomeData, PbMenuData } from "./types" import type { DocV1Block } from "./doc" -import type { MenuItem, Page, PageCardDto, FeedPage } from "@/types" +import type { BreadcrumbPart, MenuItem, Page, PageCardDto, FeedPage } from "@/types" +import { menu } from "@/const" + +const MAX_PARENT_DEPTH = 10 + +function getParentId(parent: string | string[] | undefined): string | undefined { + if (!parent) return undefined + if (Array.isArray(parent)) return parent[0] + return parent +} + +async function fetchParentChain(startId: string | undefined): Promise { + if (!startId) return [] + const chain: PbPage[] = [] + const visited = new Set() + let currentId: string | undefined = startId + while (currentId && chain.length < MAX_PARENT_DEPTH) { + if (visited.has(currentId)) break + visited.add(currentId) + try { + const parent = await pb.collection("pages").getOne(currentId) + chain.unshift(parent) + currentId = getParentId(parent.parent) + } catch { + break + } + } + return chain +} + +function buildBreadcrumb(record: PbPage, parents: PbPage[]): BreadcrumbPart[] { + if (parents.length === 0) return [] + const home: BreadcrumbPart = { name: menu[0].name, href: "/" } + const ancestors: BreadcrumbPart[] = parents.map(p => ({ name: p.title, href: p.slug })) + const current: BreadcrumbPart = { name: record.title, href: record.slug } + return [home, ...ancestors, current] +} function buildIdFilter(ids: string[]): string { return ids.map(id => `id="${id}"`).join(" || ") @@ -90,12 +126,13 @@ export async function getPageBySlug(slug: string): Promise { refs.imageIds.add(record.cover) } - // Round 2: fetch images, files, tags, card-grid pages in parallel - const [images, files, tags, cardGridPages] = await Promise.all([ + // Round 2: fetch images, files, tags, card-grid pages, parent chain in parallel + const [images, files, tags, cardGridPages, parentChain] = await Promise.all([ fetchImagesByIds([...refs.imageIds]), fetchFilesByIds([...refs.fileIds]), fetchTagsByIds(record.tags), fetchPagesByIds([...refs.pageIds]), + fetchParentChain(getParentId(record.parent)), ]) // Round 3: fetch cover images for card-grid pages @@ -112,7 +149,8 @@ export async function getPageBySlug(slug: string): Promise { // Merge all images for card-grid covers const allCardGridImages = new Map([...images, ...cardGridImages]) - return createPage(record, images, files, tags, cardGridPages, allCardGridImages) + const breadcrumb = buildBreadcrumb(record, parentChain) + return createPage(record, images, files, tags, cardGridPages, allCardGridImages, breadcrumb) } catch (error) { console.error(`Failed to fetch page: ${error}`) return null diff --git a/apps/hudozka/src/remote/factory.ts b/apps/hudozka/src/remote/factory.ts index 601ff6c5..92f09514 100644 --- a/apps/hudozka/src/remote/factory.ts +++ b/apps/hudozka/src/remote/factory.ts @@ -1,4 +1,4 @@ -import type { MenuItem, Page, Tag, PageCardDto, Pic, Token, FeedPage } from "@/types" +import type { MenuItem, Page, Tag, PageCardDto, Pic, Token, FeedPage, BreadcrumbPart } from "@/types" import type { PbPage, PbImage, PbFile, PbTag, PbHomeData, PbMenuData } from "./types" import type { DocV1Block } from "./doc" import { typograf, markdownToHtml } from "@hudozka/text" @@ -159,6 +159,7 @@ export function createPage( tags: PbTag[], cardGridPages: Map, cardGridImages: Map, + breadcrumb: BreadcrumbPart[] = [], ): Page { const cover = getCoverPic(record.cover, images) @@ -190,6 +191,7 @@ export function createPage( ...tokens, ], tags: pageTags, + breadcrumb, featured: false, } } diff --git a/apps/hudozka/src/remote/types.ts b/apps/hudozka/src/remote/types.ts index 9f904af8..d229d52e 100644 --- a/apps/hudozka/src/remote/types.ts +++ b/apps/hudozka/src/remote/types.ts @@ -36,6 +36,7 @@ export type PbPage = PbRecord & { doc: DocV1 tags: string[] draft: boolean + parent?: string | string[] } export type PbTag = PbRecord & { diff --git a/package-lock.json b/package-lock.json index d9bf3023..62af32ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9633,6 +9633,7 @@ "version": "0.0.0", "dependencies": { "@hudozka/hooks": "*", + "@hudozka/utils": "*", "classnames": "^2.5.0" }, "peerDependencies": { diff --git a/packages/ui/package.json b/packages/ui/package.json index b8845aeb..48106d08 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "classnames": "^2.5.0", - "@hudozka/hooks": "*" + "@hudozka/hooks": "*", + "@hudozka/utils": "*" }, "peerDependencies": { "react": ">=19", diff --git a/apps/hudozka/src/components/Breadcrumbs/Breadcrumbs.test.tsx b/packages/ui/src/Breadcrumbs/Breadcrumbs.test.tsx similarity index 100% rename from apps/hudozka/src/components/Breadcrumbs/Breadcrumbs.test.tsx rename to packages/ui/src/Breadcrumbs/Breadcrumbs.test.tsx diff --git a/apps/hudozka/src/components/Breadcrumbs/index.tsx b/packages/ui/src/Breadcrumbs/index.tsx similarity index 89% rename from apps/hudozka/src/components/Breadcrumbs/index.tsx rename to packages/ui/src/Breadcrumbs/index.tsx index 2f759e65..4d345d99 100644 --- a/apps/hudozka/src/components/Breadcrumbs/index.tsx +++ b/packages/ui/src/Breadcrumbs/index.tsx @@ -1,12 +1,16 @@ import s from "./styles.module.css" -import type { BreadcrumbPart } from "@/types" -import { Button } from "@hudozka/ui" +import { Button } from "../Button" import { insertBetween } from "@hudozka/utils" +export type Crumb = { + name: string + href: string +} + export type BreadcrumbsProps = { style?: React.CSSProperties - items: BreadcrumbPart[] + items: Crumb[] path: string } @@ -37,6 +41,6 @@ export const Breadcrumbs: React.FC = props => { ) })} - + ) } diff --git a/apps/hudozka/src/components/Breadcrumbs/styles.module.css b/packages/ui/src/Breadcrumbs/styles.module.css similarity index 81% rename from apps/hudozka/src/components/Breadcrumbs/styles.module.css rename to packages/ui/src/Breadcrumbs/styles.module.css index daf83a8b..f8cbdc7f 100644 --- a/apps/hudozka/src/components/Breadcrumbs/styles.module.css +++ b/packages/ui/src/Breadcrumbs/styles.module.css @@ -11,3 +11,7 @@ .breadcrumbs li { margin-bottom: var(--size-xs); } + +.breadcrumbs a { + text-decoration: none; +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index aec2c3c7..690f967b 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,4 +1,5 @@ export * from "./Box" +export * from "./Breadcrumbs" export * from "./Button" export * from "./Panel" export * from "./Title" diff --git a/pb/pb_migrations/1777072193_updated_pages.js b/pb/pb_migrations/1777072193_updated_pages.js new file mode 100644 index 00000000..ceff0fa7 --- /dev/null +++ b/pb/pb_migrations/1777072193_updated_pages.js @@ -0,0 +1,29 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_1125843985") + + // add field + collection.fields.addAt(5, new Field({ + "cascadeDelete": false, + "collectionId": "pbc_1125843985", + "help": "", + "hidden": false, + "id": "relation1032740943", + "maxSelect": 0, + "minSelect": 0, + "name": "parent", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_1125843985") + + // remove field + collection.fields.removeById("relation1032740943") + + return app.save(collection) +})