@terrafirmagreg-team/guide 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +57 -3
  2. package/src/components/book-switcher.tsx +52 -0
  3. package/src/components/mdx/guide-entry.tsx +48 -0
  4. package/src/components/mdx/multiblock.tsx +86 -0
  5. package/src/components/tree/children.tsx +75 -0
  6. package/src/components/tree/index.tsx +127 -0
  7. package/src/components/views/browser.tsx +133 -0
  8. package/src/components/views/entry/pages.tsx +188 -0
  9. package/src/components/views/entry/view.tsx +62 -0
  10. package/src/components/views/kind/links.tsx +112 -0
  11. package/src/components/views/kind/media.tsx +168 -0
  12. package/src/components/views/kind/recipe.tsx +89 -0
  13. package/src/components/views/kind/table.tsx +140 -0
  14. package/src/components/views/kind/unknown.tsx +24 -0
  15. package/src/components/views/page/category-body.tsx +58 -0
  16. package/src/components/views/page/entry-body.tsx +81 -0
  17. package/src/components/views/page/overview-body.tsx +99 -0
  18. package/src/components/views/page/text.tsx +58 -0
  19. package/src/components/views/page/view.tsx +279 -0
  20. package/src/components/views/text.tsx +132 -0
  21. package/src/hooks/use-left-sidebar-tree.ts +28 -0
  22. package/src/hooks/use-tree.ts +35 -0
  23. package/src/lib/book/convert.ts +75 -0
  24. package/src/lib/book/index-rows.ts +136 -0
  25. package/src/lib/book/index.ts +3 -0
  26. package/src/lib/book/localize.ts +77 -0
  27. package/src/lib/book/raw.ts +81 -0
  28. package/src/lib/books.ts +31 -0
  29. package/src/lib/breadcrumb.ts +56 -0
  30. package/src/lib/client/guides.ts +262 -0
  31. package/src/lib/client-payload/extras.ts +103 -0
  32. package/src/lib/client-payload/index.ts +113 -0
  33. package/src/lib/client-payload/links.ts +103 -0
  34. package/src/lib/config.ts +47 -0
  35. package/src/lib/data/dto.ts +78 -0
  36. package/src/lib/data/parse.ts +94 -0
  37. package/src/lib/dump.ts +6 -0
  38. package/src/lib/embeds.ts +12 -0
  39. package/src/lib/format.ts +232 -0
  40. package/src/lib/links.ts +2 -0
  41. package/src/lib/markdown/book.ts +74 -0
  42. package/src/lib/markdown/ctx.ts +28 -0
  43. package/src/lib/markdown/index.ts +9 -0
  44. package/src/lib/markdown/page.ts +153 -0
  45. package/src/lib/markdown/table.ts +36 -0
  46. package/src/lib/markdown/text.ts +40 -0
  47. package/src/lib/markdown-page.ts +74 -0
  48. package/src/lib/messages.ts +14 -0
  49. package/src/lib/meta.ts +10 -0
  50. package/src/lib/multiblock/index.ts +9 -0
  51. package/src/lib/multiblock/url.ts +75 -0
  52. package/src/lib/names.ts +37 -0
  53. package/src/lib/page-kind.ts +118 -0
  54. package/src/lib/paths.ts +54 -0
  55. package/src/lib/preview.ts +37 -0
  56. package/src/lib/query.ts +29 -0
  57. package/src/lib/recipe.ts +31 -0
  58. package/src/lib/reference/index.ts +41 -0
  59. package/src/lib/reference/resolver.ts +88 -0
  60. package/src/lib/search/filter.ts +18 -0
  61. package/src/lib/search/index.ts +183 -0
  62. package/src/lib/sprites.ts +18 -0
  63. package/src/lib/table.ts +11 -0
  64. package/src/lib/toc.ts +138 -0
  65. package/src/locales/en_us.json +14 -0
  66. package/src/locales/ru_ru.json +14 -0
  67. package/src/locales.ts +6 -0
  68. package/src/types/index.ts +217 -0
  69. package/README.md +0 -3
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/guide",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.2.0",
4
+ "type": "module",
5
+ "description": "Guide/books section",
6
+ "license": "LGPL-3.0-only",
7
+ "exports": {
8
+ "./components/*": [
9
+ "./src/components/*.tsx",
10
+ "./src/components/*/index.tsx",
11
+ "./src/components/*/index.ts"
12
+ ],
13
+ "./hooks/*": [
14
+ "./src/hooks/*.ts",
15
+ "./src/hooks/*/index.ts"
16
+ ],
17
+ "./lib/*": [
18
+ "./src/lib/*.ts",
19
+ "./src/lib/*/index.ts"
20
+ ],
21
+ "./types/*": "./src/types/*.ts",
22
+ "./types": "./src/types/index.ts",
23
+ "./locales": "./src/locales.ts"
24
+ },
25
+ "dependencies": {
26
+ "@terrafirmagreg-team/data": "1.2.0",
27
+ "@terrafirmagreg-team/discussions": "1.2.0",
28
+ "@terrafirmagreg-team/dump": "1.2.0",
29
+ "@terrafirmagreg-team/game": "1.2.0",
30
+ "@terrafirmagreg-team/kit": "1.2.0",
31
+ "@terrafirmagreg-team/mdx": "1.2.0",
32
+ "@terrafirmagreg-team/preview": "1.2.0",
33
+ "@terrafirmagreg-team/search": "1.2.0",
34
+ "@terrafirmagreg-team/sites": "1.2.0",
35
+ "@terrafirmagreg-team/translate": "1.2.0",
36
+ "@terrafirmagreg-team/ui": "1.2.0",
37
+ "fumadocs-core": "16.17.8",
38
+ "lucide-react": "^0.577.0"
39
+ },
40
+ "peerDependencies": {
41
+ "next": ">=15",
42
+ "react": ">=19"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^26.6.5",
46
+ "@types/react": "^19.3.0",
47
+ "@types/react-dom": "^19.3.0"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/TerraFirmaGreg-Team/Web-Foundation.git",
52
+ "directory": "packages/guide"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "files": [
58
+ "src"
59
+ ]
6
60
  }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+ import { useMemo } from 'react';
5
+ import { BookOpen } from 'lucide-react';
6
+ import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
7
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
8
+ import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
9
+ import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
10
+ import { guideQueryFromParams } from '@terrafirmagreg-team/guide/lib/query';
11
+ import { listGuideBooks } from '@terrafirmagreg-team/guide/lib/books';
12
+ import { guideHref } from '@terrafirmagreg-team/guide/lib/links';
13
+ import { parseSectionLocation } from '@terrafirmagreg-team/sites/location';
14
+ import { Switcher } from '@terrafirmagreg-team/ui/components/blocks/nav/switcher';
15
+
16
+ function oneLineLanding(text: string | undefined) {
17
+ if (!text) return undefined;
18
+ const stripped = text.replace(/\$\([^)]*\)/g, ' ').replace(/\s+/g, ' ').trim();
19
+ return stripped || undefined;
20
+ }
21
+
22
+ export function GuideBookSwitcher() {
23
+ const t = useTranslations();
24
+ const { dataset: datasetId } = parseSectionLocation(usePathname());
25
+ const lang = useResolvedLang();
26
+ const params = useLocationSearchParams();
27
+ const { bookId } = guideQueryFromParams(params);
28
+ const version = params.get('version') ?? undefined;
29
+ const dataset = useMemo(
30
+ () => (datasetId ? { id: datasetId, version } : undefined),
31
+ [datasetId, version],
32
+ );
33
+ const books = useAsyncValue(
34
+ dataset ? () => listGuideBooks(dataset, lang) : undefined,
35
+ [dataset, lang],
36
+ );
37
+ if (!books || books.length < 2 || !dataset) return null;
38
+ return (
39
+ <Switcher
40
+ options={books.map((book) => ({
41
+ value: book.bookId,
42
+ title: book.name,
43
+ description: oneLineLanding(book.landingText),
44
+ icon: <BookOpen className="size-4" />,
45
+ url: guideHref(dataset, lang, book.bookId),
46
+ }))}
47
+ selected={bookId}
48
+ placeholder={t('Select book', { note: 'ui.select_book' })}
49
+ />
50
+ );
51
+ }
52
+
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+ import { useContext } from 'react';
5
+ import { parseSectionLocation } from '@terrafirmagreg-team/sites/location';
6
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
7
+ import { useEntryContext } from '@terrafirmagreg-team/game/hooks/use-entry-context';
8
+ import { ResolvedLangContext } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
9
+ import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
10
+ import { findClientGuideEntry } from '@terrafirmagreg-team/guide/lib/client/guides';
11
+ import type { GuideDirectiveProps } from '@terrafirmagreg-team/game/types/entry';
12
+
13
+ export function GuideEntry({
14
+ id,
15
+ dataset: datasetProp,
16
+ name,
17
+ modId,
18
+ bookPath,
19
+ entry: entryOpt,
20
+ ...rest
21
+ }: GuideDirectiveProps) {
22
+ const ctx = useEntryContext();
23
+ const pathname = usePathname();
24
+ const locId = parseSectionLocation(pathname).dataset;
25
+ const dataset =
26
+ datasetProp ?? ctx?.dataset ?? (locId ? { id: locId } : undefined);
27
+ const lang = useContext(ResolvedLangContext)?.lang;
28
+ const hit = useAsyncValue(
29
+ dataset && lang && id
30
+ ? () => findClientGuideEntry(dataset, lang, id)
31
+ : false,
32
+ [dataset, lang, id],
33
+ );
34
+ return (
35
+ <Entry
36
+ kind="guide"
37
+ id={hit?.entry.id ?? id}
38
+ dataset={dataset}
39
+ name={name ?? hit?.entry.name}
40
+ icon={hit?.entry.icon}
41
+ modId={modId ?? hit?.modId}
42
+ bookPath={bookPath ?? hit?.bookPath}
43
+ entry={entryOpt ?? hit?.entry.id}
44
+ {...rest}
45
+ />
46
+ );
47
+ }
48
+
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import { useContext, useMemo } from 'react';
4
+ import { usePathname } from 'next/navigation';
5
+ import {
6
+ type DatasetRef,
7
+ parseSectionLocation,
8
+ } from '@terrafirmagreg-team/sites/location';
9
+ import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
10
+ import { useDumpLocation } from '@terrafirmagreg-team/game/hooks/use-dump-location';
11
+ import { ResolvedLangContext } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
12
+ import { createDumpAssetResolver } from '@terrafirmagreg-team/dump/lib/dump/assets/urls';
13
+ import { asMultiblock } from '@terrafirmagreg-team/guide/lib/meta';
14
+ import { glbFor } from '@terrafirmagreg-team/guide/lib/multiblock/url';
15
+ import { loadMultiblockNameIndex } from '@terrafirmagreg-team/guide/lib/client/guides';
16
+ import { multiblockKey } from '@terrafirmagreg-team/guide/lib/multiblock';
17
+ import { MultiblockVisual } from '@terrafirmagreg-team/guide/components/views/kind/media';
18
+ import type { GuideMultiblock } from '@terrafirmagreg-team/guide/types';
19
+
20
+ export type MdxMultiblockProps = {
21
+ id?: string;
22
+ name?: string;
23
+ spec?: string;
24
+ label?: string;
25
+ dataset?: DatasetRef;
26
+ title?: string;
27
+ };
28
+
29
+ export function MdxMultiblock({
30
+ id,
31
+ name,
32
+ spec,
33
+ label,
34
+ dataset: datasetProp,
35
+ title,
36
+ }: MdxMultiblockProps) {
37
+ const pathname = usePathname();
38
+ const locId = parseSectionLocation(pathname).dataset;
39
+ const dataset = useMemo<DatasetRef>(
40
+ () => datasetProp ?? { id: locId ?? '' },
41
+ [datasetProp, locId],
42
+ );
43
+ const lang = useContext(ResolvedLangContext)?.lang;
44
+ const dumpLoc = useDumpLocation(dataset);
45
+ const resolveResource = useMemo(
46
+ () => createDumpAssetResolver(dataset),
47
+ [dataset],
48
+ );
49
+ const namedSpec = useAsyncValue(
50
+ dataset && lang && name
51
+ ? async () =>
52
+ (await loadMultiblockNameIndex(dataset, lang))[
53
+ name.trim().toLowerCase()
54
+ ]
55
+ : false,
56
+ [dataset, lang, name],
57
+ );
58
+ const value = useMemo<string | GuideMultiblock | undefined>(() => {
59
+ if (spec) {
60
+ try {
61
+ return JSON.parse(spec) as GuideMultiblock;
62
+ } catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ if (name) return namedSpec;
67
+ return id;
68
+ }, [id, name, spec, namedSpec]);
69
+ const key = multiblockKey(asMultiblock(value));
70
+ const src = useAsyncValue(
71
+ dataset && key
72
+ ? () => glbFor(dataset, '', asMultiblock(value), lang)
73
+ : false,
74
+ [dataset, key, lang, dumpLoc],
75
+ );
76
+ const glbs = src && key ? { [key]: src } : undefined;
77
+ return (
78
+ <MultiblockVisual
79
+ value={value}
80
+ title={title ?? label ?? id ?? name}
81
+ glbs={glbs}
82
+ resolveResource={resolveResource}
83
+ />
84
+ );
85
+ }
86
+
@@ -0,0 +1,75 @@
1
+ import { createElement } from 'react';
2
+ import type * as PageTree from 'fumadocs-core/page-tree';
3
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
4
+ import { encodeQueryValue } from '@terrafirmagreg-team/translate/lib/url';
5
+ import {
6
+ type DatasetRef,
7
+ sectionUrl,
8
+ versionSuffix,
9
+ } from '@terrafirmagreg-team/sites/location';
10
+ import { bookOptionId, guideCategoryHref } from '@terrafirmagreg-team/guide/lib/paths';
11
+ import type { GuideIndex } from '@terrafirmagreg-team/guide/types';
12
+ import type { TaggedReference } from '@terrafirmagreg-team/dump/lib/dump/contract/ref';
13
+
14
+ function nodeIcon(
15
+ dataset: DatasetRef,
16
+ icon: TaggedReference | undefined,
17
+ name: string,
18
+ ): React.ReactNode {
19
+ if (!icon?.id) return undefined;
20
+ return createElement(
21
+ 'span',
22
+ {
23
+ 'data-icon': true,
24
+ className:
25
+ 'inline-flex size-3.5 shrink-0 items-center justify-center overflow-hidden',
26
+ },
27
+ createElement(Entry, {
28
+ dataset,
29
+ id: icon.id,
30
+ kind: icon.kind,
31
+ name,
32
+ size: 14,
33
+ href: false,
34
+ tooltip: false,
35
+ missing: 'hide',
36
+ }),
37
+ );
38
+ }
39
+
40
+ export function guideTreeChildren(
41
+ dataset: DatasetRef,
42
+ index: GuideIndex,
43
+ ): PageTree.Node[] {
44
+ const bookId =
45
+ index.bookId ??
46
+ (index.modId && index.bookPath
47
+ ? bookOptionId(index.modId, index.bookPath)
48
+ : undefined);
49
+ return index.categories.map((category): PageTree.Folder => ({
50
+ type: 'folder',
51
+ name: category.name,
52
+ icon: nodeIcon(dataset, category.icon, category.name),
53
+ index: {
54
+ type: 'page',
55
+ name: category.name,
56
+ url: guideCategoryHref(
57
+ dataset,
58
+ undefined,
59
+ index.modId,
60
+ index.bookPath ?? '',
61
+ category,
62
+ ),
63
+ $id: `${dataset.id}/${dataset.version ?? ''}/${index.modId}/${index.bookPath}/${category.id}#index`,
64
+ },
65
+ $id: `${dataset.id}/${dataset.version ?? ''}/${index.modId}/${index.bookPath}/${category.id}`,
66
+ children: category.entries.map((entry): PageTree.Item => ({
67
+ type: 'page',
68
+ name: entry.name,
69
+ url: `${sectionUrl('guide', dataset.id)}?book=${encodeQueryValue(bookId ?? '')}&entry=${encodeQueryValue(entry.id)}${versionSuffix(dataset)}`,
70
+ icon: nodeIcon(dataset, entry.icon, entry.name),
71
+ $id: `${dataset.id}/${dataset.version ?? ''}/${index.modId}/${index.bookPath}/${category.id}/${entry.id}`,
72
+ })),
73
+ }));
74
+ }
75
+
@@ -0,0 +1,127 @@
1
+ import 'server-only';
2
+ import { createElement } from 'react';
3
+ import type * as PageTree from 'fumadocs-core/page-tree';
4
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
5
+ import { loadMergedLangMap } from '@terrafirmagreg-team/dump/lib/dump/assets/lang/load';
6
+ import { asRecord, asString } from '@terrafirmagreg-team/dump/lib/dump/contract/read';
7
+ import { loadDatasetDump } from '@terrafirmagreg-team/dump/lib/dump/mods/load';
8
+ import { DUMP_POINTER_TTL } from '@terrafirmagreg-team/dump/lib/dump/json-reader';
9
+ import { createAsyncCache } from '@terrafirmagreg-team/data/lib/async-cache';
10
+ import { clientDumpJson } from '@terrafirmagreg-team/dump/lib/dump/client/dump';
11
+ import { keyTreeNode } from '@terrafirmagreg-team/translate/server';
12
+ import { loadLocaleRegistry } from '@terrafirmagreg-team/translate/server';
13
+ import { encodeQueryValue } from '@terrafirmagreg-team/translate/lib/url';
14
+ import {
15
+ type DatasetRef,
16
+ sectionUrl,
17
+ versionSuffix,
18
+ } from '@terrafirmagreg-team/sites/location';
19
+ import { bookOptionId } from '@terrafirmagreg-team/guide/lib/paths';
20
+ import { bookSlug, toBook } from '@terrafirmagreg-team/guide/lib/book';
21
+ import { sectionDatasets } from '@terrafirmagreg-team/kit/lib/datasets';
22
+ import type { GuideBookJson, GuideBookRef } from '@terrafirmagreg-team/guide/lib/data/dto';
23
+ import type { SectionTrees } from '@terrafirmagreg-team/kit/types';
24
+
25
+ function bookIcon(
26
+ dataset: DatasetRef,
27
+ raw: GuideBookJson | undefined,
28
+ ): React.ReactNode {
29
+ const cover = asRecord(raw?.cover);
30
+ const coverId = asString(cover?.id) ?? undefined;
31
+ if (!coverId) return undefined;
32
+ return createElement(
33
+ 'span',
34
+ {
35
+ 'data-icon': true,
36
+ className:
37
+ 'inline-flex size-3.5 shrink-0 items-center justify-center overflow-hidden',
38
+ },
39
+ createElement(Entry, {
40
+ dataset,
41
+ id: coverId,
42
+ kind: 'item',
43
+ size: 14,
44
+ href: false,
45
+ tooltip: false,
46
+ missing: 'hide',
47
+ }),
48
+ );
49
+ }
50
+
51
+ function bookLeaf(
52
+ dataset: DatasetRef,
53
+ ref: GuideBookRef,
54
+ slug: string,
55
+ raw: GuideBookJson | undefined,
56
+ map: Record<string, string>,
57
+ ): PageTree.Item {
58
+ const book = toBook(raw, ref, slug, map);
59
+ const bookId = bookOptionId(ref.modId, ref.bookPath);
60
+ return keyTreeNode({
61
+ type: 'page',
62
+ name: book.name,
63
+ url: `${sectionUrl('guide', dataset.id)}?book=${encodeQueryValue(bookId)}${versionSuffix(dataset)}`,
64
+ icon: bookIcon(dataset, raw),
65
+ $id: `${dataset.id}/${dataset.version ?? ''}/book/${bookId}`,
66
+ }) as PageTree.Item;
67
+ }
68
+
69
+ async function datasetBookLeaves(
70
+ dataset: DatasetRef,
71
+ langMap: Record<string, string>,
72
+ ): Promise<PageTree.Node[]> {
73
+ const dump = await loadDatasetDump(dataset);
74
+ if (!dump) return [];
75
+ const taken = new Set<string>();
76
+ const leaves: PageTree.Node[] = [];
77
+ for (const mod of dump.mods) {
78
+ const raws = await Promise.all(
79
+ mod.books.map((ref) =>
80
+ clientDumpJson(dataset, `${ref.dir}/book.json`).then(
81
+ (raw) => asRecord(raw) as GuideBookJson | undefined,
82
+ ),
83
+ ),
84
+ );
85
+ for (const [i, ref] of mod.books.entries()) {
86
+ const raw = raws[i];
87
+ if (!raw) continue;
88
+ let slug = bookSlug(raw, ref);
89
+ if (taken.has(slug)) {
90
+ let n = 2;
91
+ while (taken.has(`${slug}-${n}`)) n += 1;
92
+ slug = `${slug}-${n}`;
93
+ }
94
+ taken.add(slug);
95
+ leaves.push(bookLeaf(dataset, ref, slug, raw, langMap));
96
+ }
97
+ }
98
+ return leaves;
99
+ }
100
+
101
+ const guideTreesCache = createAsyncCache(
102
+ async (): Promise<SectionTrees> => buildGuideTrees(),
103
+ () => 'guide-trees',
104
+ DUMP_POINTER_TTL,
105
+ );
106
+
107
+ export function guideTrees(): Promise<SectionTrees> {
108
+ return guideTreesCache();
109
+ }
110
+
111
+ async function buildGuideTrees(): Promise<SectionTrees> {
112
+ const { defaultLanguage } = loadLocaleRegistry();
113
+ const datasets = await sectionDatasets('guide');
114
+ const trees: SectionTrees = {};
115
+ for (const { id, title: datasetTitle, languages } of datasets) {
116
+ for (const lang of languages ?? [defaultLanguage]) {
117
+ const map = await loadMergedLangMap({ id }, lang);
118
+ (trees[lang] ??= {})[id] = {
119
+ name: datasetTitle ?? id,
120
+ children: await datasetBookLeaves({ id }, map),
121
+ $id: `guide/${id}/${lang}`,
122
+ } satisfies PageTree.Root;
123
+ }
124
+ }
125
+ return trees;
126
+ }
127
+
@@ -0,0 +1,133 @@
1
+ 'use client';
2
+ import { useEffect, useMemo } from 'react';
3
+ import { useRouter } from 'next/navigation';
4
+ import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
5
+ import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
6
+ import { useVersionedDataset } from '@terrafirmagreg-team/kit/hooks/use-versioned-dataset';
7
+ import { useLangMap } from '@terrafirmagreg-team/game/hooks/use-lang-map';
8
+ import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
9
+ import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
10
+ import {
11
+ Workspace,
12
+ WorkspaceBody,
13
+ } from '@terrafirmagreg-team/kit/components/workspace';
14
+ import { GuidePageView } from '@terrafirmagreg-team/guide/components/views/page/view';
15
+ import {
16
+ firstGuideEntryId,
17
+ guideQueryFromParams,
18
+ } from '@terrafirmagreg-team/guide/lib/query';
19
+ import { loadClientGuidePayload } from '@terrafirmagreg-team/guide/lib/client-payload';
20
+ import { listGuideBooks } from '@terrafirmagreg-team/guide/lib/books';
21
+ import { loadClientGuideBook } from '@terrafirmagreg-team/guide/lib/client/guides';
22
+ import { guideHref } from '@terrafirmagreg-team/guide/lib/links';
23
+ import { guideConfig } from '@terrafirmagreg-team/guide/lib/config';
24
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
25
+ import type { GuideMessages } from '@terrafirmagreg-team/guide/types';
26
+
27
+ export function GuideBrowser({
28
+ dataset: id,
29
+ datasetTitle,
30
+ lang,
31
+ versions,
32
+ }: {
33
+ dataset: string;
34
+ datasetTitle: string;
35
+ lang: Lang;
36
+ versions?: readonly string[];
37
+ }) {
38
+ const t = useTranslations();
39
+ const router = useRouter();
40
+ const params = useLocationSearchParams();
41
+ const dataset = useVersionedDataset(id, { versions }) ?? { id };
42
+ const { bookId, entryId, categoryId } = useMemo(
43
+ () => guideQueryFromParams(params),
44
+ [params],
45
+ );
46
+ const books = useAsyncValue(
47
+ () => listGuideBooks(dataset, lang),
48
+ [dataset, lang],
49
+ );
50
+ const langMap = useLangMap(dataset, lang);
51
+ const messages = useMemo<GuideMessages>(
52
+ () => ({
53
+ 'guide.related': t('Related', { note: 'guide.related' }),
54
+ 'guide.objective': t('Objective', { note: 'guide.objective' }),
55
+ 'guide.link': t('Open', { note: 'guide.link' }),
56
+ 'guide.quest': t('Quest', { note: 'guide.quest' }),
57
+ 'guide.recipe_empty': t('No recipes', { note: 'guide.recipe_empty' }),
58
+ 'guide.entity_unavailable': t('Entity preview unavailable', {
59
+ note: 'guide.entity_unavailable',
60
+ }),
61
+ 'guide.entry_missing': t('Entry not found', {
62
+ note: 'guide.entry_missing',
63
+ }),
64
+ 'guide.entry_count': t('{count} entries', {
65
+ note: 'guide.entry_count',
66
+ }),
67
+ 'guide.page_count': t('{count} pages', { note: 'guide.page_count' }),
68
+ }),
69
+ [t],
70
+ );
71
+
72
+ const canonical = useAsyncValue(
73
+ books && !bookId
74
+ ? async () => {
75
+ const book = books[0];
76
+ if (!book) return undefined;
77
+ const index = await loadClientGuideBook(dataset, lang, book.bookId);
78
+ const first = index ? firstGuideEntryId(index) : undefined;
79
+ return guideHref(dataset, lang, book.bookId, first);
80
+ }
81
+ : undefined,
82
+ [books, bookId, dataset, lang],
83
+ );
84
+
85
+ useEffect(() => {
86
+ if (canonical) router.replace(canonical);
87
+ }, [canonical, router]);
88
+
89
+ const payload = useAsyncValue(
90
+ langMap && bookId
91
+ ? () =>
92
+ loadClientGuidePayload(
93
+ dataset,
94
+ lang,
95
+ bookId,
96
+ entryId,
97
+ categoryId,
98
+ langMap,
99
+ messages,
100
+ )
101
+ : undefined,
102
+ [dataset, lang, bookId, entryId, categoryId, langMap, messages],
103
+ );
104
+
105
+ useEffect(() => {
106
+ if (payload?.title) {
107
+ document.title = `${payload.title} · ${datasetTitle} | ${guideConfig().appName}`;
108
+ }
109
+ }, [payload?.title, datasetTitle]);
110
+
111
+ if (!bookId || !payload || !langMap) {
112
+ return (
113
+ <Workspace toc={[]}>
114
+ <WorkspaceBody className="flex flex-col gap-3">
115
+ <Skeleton className="h-3.5 w-44" />
116
+ <Skeleton className="mt-1 h-8 w-2/3 max-w-sm" />
117
+ <Skeleton className="h-4 w-full max-w-md" />
118
+ <Skeleton className="h-4 w-4/5 max-w-sm" />
119
+ <div className="mt-6 flex flex-col gap-2.5">
120
+ <Skeleton className="h-4 w-full" />
121
+ <Skeleton className="h-4 w-[92%]" />
122
+ <Skeleton className="h-4 w-[96%]" />
123
+ <Skeleton className="h-32 w-full" />
124
+ <Skeleton className="h-4 w-[88%]" />
125
+ <Skeleton className="h-4 w-3/4" />
126
+ </div>
127
+ </WorkspaceBody>
128
+ </Workspace>
129
+ );
130
+ }
131
+ return <GuidePageView payload={payload} langMap={langMap} />;
132
+ }
133
+