Breadcrumbs (#296)

* add breadcrumbs

* rm space

* move breadcrumbs to ui
This commit is contained in:
tmshv authored and GitHub committed 2026-04-25 03:15:25 +03:00
1 parent ad9695127a
commit 98dcaaa237
11 files changed
+92 -12

No files matched your search

+1 -2
View File
@@ -5,12 +5,11 @@ import s from "./app.module.css"
import { Wrapper } from "../Wrapper" import { Wrapper } from "../Wrapper"
import { Footer } from "../Footer" import { Footer } from "../Footer"
import { Content } from "../Content" import { Content } from "../Content"
import { Breadcrumbs } from "../Breadcrumbs"
import type { BreadcrumbPart } from "@/types" import type { BreadcrumbPart } from "@/types"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { useMobile } from "@hudozka/hooks" import { useMobile } from "@hudozka/hooks"
import { Navigation } from "@/components/Navigation" 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 { AccessibilityButton } from "../AccessibilityButton"
import { Copyright } from "@/components/Copyright" import { Copyright } from "@/components/Copyright"
import { yearRange } from "@/const" import { yearRange } from "@/const"
+42 -4
View File
@@ -2,7 +2,43 @@ import { pb } from "./pb"
import { createPage, createHomeCards, createMenu, createFeedPages } from "./factory" import { createPage, createHomeCards, createMenu, createFeedPages } from "./factory"
import type { PbPage, PbImage, PbFile, PbTag, PbHomeData, PbMenuData } from "./types" import type { PbPage, PbImage, PbFile, PbTag, PbHomeData, PbMenuData } from "./types"
import type { DocV1Block } from "./doc" 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<PbPage[]> {
if (!startId) return []
const chain: PbPage[] = []
const visited = new Set<string>()
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<PbPage>(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 { function buildIdFilter(ids: string[]): string {
return ids.map(id => `id="${id}"`).join(" || ") return ids.map(id => `id="${id}"`).join(" || ")
@@ -90,12 +126,13 @@ export async function getPageBySlug(slug: string): Promise<Page | null> {
refs.imageIds.add(record.cover) refs.imageIds.add(record.cover)
} }
// Round 2: fetch images, files, tags, card-grid pages in parallel // Round 2: fetch images, files, tags, card-grid pages, parent chain in parallel
const [images, files, tags, cardGridPages] = await Promise.all([ const [images, files, tags, cardGridPages, parentChain] = await Promise.all([
fetchImagesByIds([...refs.imageIds]), fetchImagesByIds([...refs.imageIds]),
fetchFilesByIds([...refs.fileIds]), fetchFilesByIds([...refs.fileIds]),
fetchTagsByIds(record.tags), fetchTagsByIds(record.tags),
fetchPagesByIds([...refs.pageIds]), fetchPagesByIds([...refs.pageIds]),
fetchParentChain(getParentId(record.parent)),
]) ])
// Round 3: fetch cover images for card-grid pages // Round 3: fetch cover images for card-grid pages
@@ -112,7 +149,8 @@ export async function getPageBySlug(slug: string): Promise<Page | null> {
// Merge all images for card-grid covers // Merge all images for card-grid covers
const allCardGridImages = new Map([...images, ...cardGridImages]) 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) { } catch (error) {
console.error(`Failed to fetch page: ${error}`) console.error(`Failed to fetch page: ${error}`)
return null return null
+3 -1
View File
@@ -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 { PbPage, PbImage, PbFile, PbTag, PbHomeData, PbMenuData } from "./types"
import type { DocV1Block } from "./doc" import type { DocV1Block } from "./doc"
import { typograf, markdownToHtml } from "@hudozka/text" import { typograf, markdownToHtml } from "@hudozka/text"
@@ -159,6 +159,7 @@ export function createPage(
tags: PbTag[], tags: PbTag[],
cardGridPages: Map<string, PbPage>, cardGridPages: Map<string, PbPage>,
cardGridImages: Map<string, PbImage>, cardGridImages: Map<string, PbImage>,
breadcrumb: BreadcrumbPart[] = [],
): Page { ): Page {
const cover = getCoverPic(record.cover, images) const cover = getCoverPic(record.cover, images)
@@ -190,6 +191,7 @@ export function createPage(
...tokens, ...tokens,
], ],
tags: pageTags, tags: pageTags,
breadcrumb,
featured: false, featured: false,
} }
} }
+1
View File
@@ -36,6 +36,7 @@ export type PbPage = PbRecord & {
doc: DocV1 doc: DocV1
tags: string[] tags: string[]
draft: boolean draft: boolean
parent?: string | string[]
} }
export type PbTag = PbRecord & { export type PbTag = PbRecord & {
+1
View File
@@ -9633,6 +9633,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@hudozka/hooks": "*", "@hudozka/hooks": "*",
"@hudozka/utils": "*",
"classnames": "^2.5.0" "classnames": "^2.5.0"
}, },
"peerDependencies": { "peerDependencies": {
+2 -1
View File
@@ -11,7 +11,8 @@
}, },
"dependencies": { "dependencies": {
"classnames": "^2.5.0", "classnames": "^2.5.0",
"@hudozka/hooks": "*" "@hudozka/hooks": "*",
"@hudozka/utils": "*"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=19", "react": ">=19",
@@ -1,12 +1,16 @@
import s from "./styles.module.css" import s from "./styles.module.css"
import type { BreadcrumbPart } from "@/types" import { Button } from "../Button"
import { Button } from "@hudozka/ui"
import { insertBetween } from "@hudozka/utils" import { insertBetween } from "@hudozka/utils"
export type Crumb = {
name: string
href: string
}
export type BreadcrumbsProps = { export type BreadcrumbsProps = {
style?: React.CSSProperties style?: React.CSSProperties
items: BreadcrumbPart[] items: Crumb[]
path: string path: string
} }
@@ -37,6 +41,6 @@ export const Breadcrumbs: React.FC<BreadcrumbsProps> = props => {
</li> </li>
) )
})} })}
</nav > </nav>
) )
} }
@@ -11,3 +11,7 @@
.breadcrumbs li { .breadcrumbs li {
margin-bottom: var(--size-xs); margin-bottom: var(--size-xs);
} }
.breadcrumbs a {
text-decoration: none;
}
+1
View File
@@ -1,4 +1,5 @@
export * from "./Box" export * from "./Box"
export * from "./Breadcrumbs"
export * from "./Button" export * from "./Button"
export * from "./Panel" export * from "./Panel"
export * from "./Title" export * from "./Title"
@@ -0,0 +1,29 @@
/// <reference path="../pb_data/types.d.ts" />
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)
})