@terrafirmagreg-team/guide 0.0.0-stage → 0.1.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 (66) hide show
  1. package/package.json +58 -3
  2. package/src/components/book-switcher.tsx +46 -0
  3. package/src/components/views/browser.tsx +124 -0
  4. package/src/components/views/entry/pages.tsx +186 -0
  5. package/src/components/views/entry/view.tsx +60 -0
  6. package/src/components/views/kind/links.tsx +111 -0
  7. package/src/components/views/kind/media.tsx +167 -0
  8. package/src/components/views/kind/recipe.tsx +134 -0
  9. package/src/components/views/kind/table.tsx +138 -0
  10. package/src/components/views/kind/unknown.tsx +23 -0
  11. package/src/components/views/page/category-body.tsx +56 -0
  12. package/src/components/views/page/entry-body.tsx +79 -0
  13. package/src/components/views/page/overview-body.tsx +97 -0
  14. package/src/components/views/page/text.tsx +57 -0
  15. package/src/components/views/page/view.tsx +279 -0
  16. package/src/components/views/text.tsx +130 -0
  17. package/src/hooks/use-left-sidebar-tree.ts +19 -0
  18. package/src/hooks/use-tree.ts +33 -0
  19. package/src/lib/book/convert.ts +74 -0
  20. package/src/lib/book/index-rows.ts +134 -0
  21. package/src/lib/book/index.ts +2 -0
  22. package/src/lib/book/localize.ts +76 -0
  23. package/src/lib/book/raw.ts +80 -0
  24. package/src/lib/books.ts +29 -0
  25. package/src/lib/breadcrumb.ts +54 -0
  26. package/src/lib/client/guides.ts +189 -0
  27. package/src/lib/client-payload/extras.ts +101 -0
  28. package/src/lib/client-payload/index.ts +112 -0
  29. package/src/lib/client-payload/links.ts +101 -0
  30. package/src/lib/config.ts +46 -0
  31. package/src/lib/data/dto.ts +77 -0
  32. package/src/lib/data/parse.ts +93 -0
  33. package/src/lib/dump.ts +5 -0
  34. package/src/lib/format.ts +231 -0
  35. package/src/lib/links.ts +2 -0
  36. package/src/lib/markdown/book.ts +73 -0
  37. package/src/lib/markdown/ctx.ts +26 -0
  38. package/src/lib/markdown/index.ts +8 -0
  39. package/src/lib/markdown/page.ts +154 -0
  40. package/src/lib/markdown/table.ts +35 -0
  41. package/src/lib/markdown/text.ts +39 -0
  42. package/src/lib/markdown-page.ts +72 -0
  43. package/src/lib/messages.ts +13 -0
  44. package/src/lib/meta.ts +9 -0
  45. package/src/lib/multiblock/index.ts +8 -0
  46. package/src/lib/multiblock/url.ts +61 -0
  47. package/src/lib/names.ts +35 -0
  48. package/src/lib/page-kind.ts +117 -0
  49. package/src/lib/paths.ts +52 -0
  50. package/src/lib/preview.ts +33 -0
  51. package/src/lib/query.ts +28 -0
  52. package/src/lib/recipe.ts +30 -0
  53. package/src/lib/reference/index.ts +40 -0
  54. package/src/lib/reference/resolver.ts +88 -0
  55. package/src/lib/search/filter.ts +17 -0
  56. package/src/lib/search/index.ts +156 -0
  57. package/src/lib/sprites.ts +17 -0
  58. package/src/lib/table.ts +10 -0
  59. package/src/lib/toc.ts +137 -0
  60. package/src/lib/tree/children.ts +70 -0
  61. package/src/lib/tree/index.ts +117 -0
  62. package/src/locales/en_us.json +22 -0
  63. package/src/locales/ru_ru.json +22 -0
  64. package/src/locales.ts +5 -0
  65. package/src/types/index.ts +215 -0
  66. package/README.md +0 -3
@@ -0,0 +1,167 @@
1
+ import { useMemo } from 'react';
2
+ import { GuideText } from '@terrafirmagreg-team/guide/components/views/text';
3
+ import { useDumpLocation } from '@terrafirmagreg-team/game/hooks/use-dump-location';
4
+ import { asMultiblock } from '@terrafirmagreg-team/guide/lib/meta';
5
+ import {
6
+ createDumpAssetResolver,
7
+ texturePublicUrl,
8
+ } from '@terrafirmagreg-team/game/lib/dump/assets/urls';
9
+ import { dumpRelPublicUrl } from '@terrafirmagreg-team/game/lib/dump/paths';
10
+ import { parseStackId } from '@terrafirmagreg-team/game/lib/dump/contract/ref';
11
+ import { langString } from '@terrafirmagreg-team/game/lib/dump/assets/lang/names';
12
+ import { matchVariant } from '@terrafirmagreg-team/game/lib/dump/data/registry/parse';
13
+ import { GlbViewer } from '@terrafirmagreg-team/ui/components/blocks/media/glb/viewer';
14
+ import { multiblockKey } from '@terrafirmagreg-team/guide/lib/multiblock';
15
+ import { guideFormatOpts } from '@terrafirmagreg-team/guide/lib/format';
16
+ import { spriteName } from '@terrafirmagreg-team/guide/lib/names';
17
+ import type { PageRendererProps } from '@terrafirmagreg-team/guide/types';
18
+ import { GlbPlaceholder } from '@terrafirmagreg-team/ui/components/blocks/media/glb/viewer';
19
+ import { Slideshow } from '@terrafirmagreg-team/ui/components/blocks/media/slideshow';
20
+ import type { GuideMultiblock } from '@terrafirmagreg-team/guide/types';
21
+
22
+ export function ImagePage(props: PageRendererProps) {
23
+ const { dataset, page } = props;
24
+ useDumpLocation(dataset);
25
+ const images = [
26
+ ...(page.images ?? []),
27
+ ...(typeof page.image === 'string' ? [page.image] : []),
28
+ ];
29
+ const opts = guideFormatOpts(props);
30
+ const title =
31
+ typeof page.title === 'string' && page.title ? (
32
+ <GuideText text={page.title} {...opts} />
33
+ ) : null;
34
+ return (
35
+ <Slideshow
36
+ images={
37
+ images.length
38
+ ? images.map((id) => ({
39
+ id,
40
+ url: texturePublicUrl(dataset, id) ?? null,
41
+ }))
42
+ : [{ id: 'missing', url: null }]
43
+ }
44
+ alt={typeof page.title === 'string' ? page.title : ''}
45
+ border={page.border !== false}
46
+ scale={page.scale}
47
+ offset={page.offset}
48
+ rotate={page.rotate}
49
+ caption={title ?? undefined}
50
+ />
51
+ );
52
+ }
53
+
54
+ export function EntityPage({
55
+ dataset,
56
+ page,
57
+ langMap,
58
+ atlases,
59
+ extras,
60
+ }: PageRendererProps) {
61
+ const id = typeof page.entity === 'string' ? page.entity : undefined;
62
+ useDumpLocation(dataset);
63
+ const resolveResource = useMemo(
64
+ () => createDumpAssetResolver(dataset),
65
+ [dataset],
66
+ );
67
+ const parsed = id ? parseStackId(id) : undefined;
68
+ const entry = parsed?.id ? extras?.entities?.[parsed.id] : undefined;
69
+ const variant = matchVariant(entry?.variants, parsed?.nbt);
70
+ const model = variant?.model ?? entry?.model;
71
+ const ns = parsed?.id.split(':', 1)[0];
72
+ const glb =
73
+ model && ns
74
+ ? dumpRelPublicUrl(dataset, `assets/${ns}/${model}`)
75
+ : undefined;
76
+ const name = id
77
+ ? (langString(langMap, variant?.display?.name) ??
78
+ langString(langMap, entry?.display?.name) ??
79
+ spriteName(dataset, langMap, atlases, id))
80
+ : undefined;
81
+ return (
82
+ <div className="space-y-2">
83
+ {glb ? (
84
+ <GlbViewer
85
+ src={glb}
86
+ title={name}
87
+ scale={typeof page.scale === 'number' ? page.scale : undefined}
88
+ offset={typeof page.offset === 'number' ? page.offset : undefined}
89
+ resolveResource={resolveResource}
90
+ />
91
+ ) : (
92
+ <GlbPlaceholder title={name} />
93
+ )}
94
+ </div>
95
+ );
96
+ }
97
+
98
+ function MissingVisual({ title }: { title?: string }) {
99
+ return <GlbPlaceholder title={title} />;
100
+ }
101
+
102
+ export function MultiblockVisual({
103
+ value,
104
+ title,
105
+ glbs,
106
+ resolveResource,
107
+ }: {
108
+ value: string | GuideMultiblock | undefined;
109
+ title?: string;
110
+ glbs?: Record<string, string>;
111
+ resolveResource?: (
112
+ resourceUrl: string,
113
+ modelUrl: string,
114
+ ) => string | undefined;
115
+ }) {
116
+ const spec = asMultiblock(value);
117
+ const key = multiblockKey(spec);
118
+ const src = key ? glbs?.[key] : undefined;
119
+ return src ? (
120
+ <GlbViewer src={src} title={title} resolveResource={resolveResource} />
121
+ ) : (
122
+ <MissingVisual title={title} />
123
+ );
124
+ }
125
+
126
+ export function MultiblockPage({ page, dataset, extras }: PageRendererProps) {
127
+ const modelId = typeof page.model === 'string' ? page.model : undefined;
128
+ useDumpLocation(dataset);
129
+ const resolveResource = useMemo(
130
+ () => createDumpAssetResolver(dataset),
131
+ [dataset],
132
+ );
133
+ return (
134
+ <MultiblockVisual
135
+ value={modelId ?? page.multiblock}
136
+ title={modelId ?? page.name}
137
+ glbs={extras?.glbs}
138
+ resolveResource={resolveResource}
139
+ />
140
+ );
141
+ }
142
+
143
+ export function MultiMultiblockPage({
144
+ page,
145
+ dataset,
146
+ extras,
147
+ }: PageRendererProps) {
148
+ useDumpLocation(dataset);
149
+ const resolveResource = useMemo(
150
+ () => createDumpAssetResolver(dataset),
151
+ [dataset],
152
+ );
153
+ const specs = (page.multiblocks ?? []).map(asMultiblock);
154
+ const glbs = specs
155
+ .map((spec) => multiblockKey(spec))
156
+ .map((key) => (key ? extras?.glbs?.[key] : undefined))
157
+ .filter((url): url is string => Boolean(url));
158
+ return glbs.length ? (
159
+ <GlbViewer
160
+ sources={glbs}
161
+ title={page.name}
162
+ resolveResource={resolveResource}
163
+ />
164
+ ) : (
165
+ <MissingVisual title={page.name} />
166
+ );
167
+ }
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { Card } from '@terrafirmagreg-team/ui/components/ui/card';
6
+ import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
7
+ import { PagedCard } from '@terrafirmagreg-team/recipe/components/cards/paged-card';
8
+ import { FitCard } from '@terrafirmagreg-team/recipe/components/cards/fit-card';
9
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
10
+ import { EntryList } from '@terrafirmagreg-team/game/components/entry/list';
11
+ import { guideMessage } from '@terrafirmagreg-team/guide/lib/messages';
12
+ import { collectItemIds } from '@terrafirmagreg-team/guide/lib/data/parse';
13
+ import { pageTitle } from '@terrafirmagreg-team/guide/lib/toc';
14
+ import { spriteName } from '@terrafirmagreg-team/guide/lib/names';
15
+ import { recipeIds, recipeNameScope } from '@terrafirmagreg-team/guide/lib/recipe';
16
+ import { recipeHref } from '@terrafirmagreg-team/recipe/lib/links';
17
+ import { recipeCardImageUrl } from '@terrafirmagreg-team/recipe/lib/links';
18
+ import { recipeImageUrl } from '@terrafirmagreg-team/game/lib/dump/assets/urls';
19
+ import { recipeTitle } from '@terrafirmagreg-team/recipe/lib/data/parse';
20
+ import type { Recipe } from '@terrafirmagreg-team/recipe/lib/data/dto';
21
+ import type { PageRendererProps } from '@terrafirmagreg-team/guide/types';
22
+
23
+ function GuideRecipeCard({
24
+ id,
25
+ recipe,
26
+ dataset,
27
+ lang,
28
+ langMap,
29
+ }: {
30
+ id: string;
31
+ recipe: Recipe;
32
+ dataset: string;
33
+ lang: string;
34
+ langMap: Record<string, string>;
35
+ }) {
36
+ const [failed, setFailed] = useState(false);
37
+ if (failed) return null;
38
+ const src =
39
+ recipeImageUrl(dataset, recipe) ?? recipeCardImageUrl(dataset, id);
40
+ if (!src) return null;
41
+ const title = recipeTitle(recipe, langMap);
42
+ return (
43
+ <Link
44
+ href={recipeHref(dataset, lang, { id })}
45
+ className="not-prose block w-fit max-w-full"
46
+ >
47
+ <FitCard w={recipe.w ?? 0}>
48
+ <PagedCard
49
+ recipe={recipe}
50
+ dataset={dataset}
51
+ lang={lang}
52
+ langMap={langMap}
53
+ title={title}
54
+ src={src}
55
+ onFail={() => setFailed(true)}
56
+ />
57
+ </FitCard>
58
+ </Link>
59
+ );
60
+ }
61
+
62
+ export function RecipePage({
63
+ dataset,
64
+ page,
65
+ lang,
66
+ langMap,
67
+ atlases,
68
+ extras,
69
+ messages,
70
+ }: PageRendererProps) {
71
+ const ids = recipeIds(page);
72
+ if (!ids.length) {
73
+ return (
74
+ <EmptyState variant="empty">
75
+ {guideMessage(messages, 'guide.recipe_empty')}
76
+ </EmptyState>
77
+ );
78
+ }
79
+ return (
80
+ <div className="space-y-4">
81
+ {ids.map((id) => {
82
+ const recipe = extras?.recipes?.[id];
83
+ if (recipe) {
84
+ return (
85
+ <GuideRecipeCard
86
+ key={id}
87
+ id={id}
88
+ recipe={recipe}
89
+ dataset={dataset}
90
+ lang={lang}
91
+ langMap={recipeNameScope(langMap, recipe)}
92
+ />
93
+ );
94
+ }
95
+ const name = spriteName(dataset, langMap, atlases, id);
96
+ return (
97
+ <Card key={id} className="flex-row items-center gap-2 px-3 py-2">
98
+ {id.includes(':') ? (
99
+ <Entry
100
+ id={id}
101
+ kind="item"
102
+ dataset={dataset}
103
+ name={name}
104
+ labeled
105
+ label={name}
106
+ />
107
+ ) : (
108
+ <span className="text-sm">{name}</span>
109
+ )}
110
+ </Card>
111
+ );
112
+ })}
113
+ </div>
114
+ );
115
+ }
116
+
117
+ export function SpotlightPage({
118
+ dataset,
119
+ page,
120
+ langMap,
121
+ extras,
122
+ }: PageRendererProps) {
123
+ if (pageTitle(page, langMap, {}, extras?.entityNames)) return null;
124
+ const ids = [...collectItemIds(page.item), ...collectItemIds(page.icon)];
125
+ if (!ids.length) return null;
126
+ return (
127
+ <EntryList
128
+ variant="row"
129
+ labeled
130
+ dataset={dataset}
131
+ items={ids.map((id) => ({ id, kind: 'item' as const }))}
132
+ />
133
+ );
134
+ }
@@ -0,0 +1,138 @@
1
+ import { GuideText } from '@terrafirmagreg-team/guide/components/views/text';
2
+ import { Card } from '@terrafirmagreg-team/ui/components/ui/card';
3
+ import { ItemEntry } from '@terrafirmagreg-team/game/components/entry/entry';
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from '@terrafirmagreg-team/ui/components/ui/table';
12
+ import { collectItemIds } from '@terrafirmagreg-team/guide/lib/data/parse';
13
+ import { cssColor, cellTexts } from '@terrafirmagreg-team/guide/lib/table';
14
+ import { guideFormatOpts } from '@terrafirmagreg-team/guide/lib/format';
15
+ import type { GuideTableCell, PageRendererProps } from '@terrafirmagreg-team/guide/types';
16
+
17
+ export function TablePage(props: PageRendererProps) {
18
+ const { page } = props;
19
+ const raw = page.strings ?? page.rows ?? [];
20
+ const cells = Array.isArray(raw) ? raw : [];
21
+ const columnCount =
22
+ typeof page.columns === 'number' ? page.columns : undefined;
23
+ const rows =
24
+ columnCount && columnCount > 0
25
+ ? Array.from(
26
+ { length: Math.ceil(cells.length / (columnCount + 1)) },
27
+ (_, index) =>
28
+ cells.slice(
29
+ index * (columnCount + 1),
30
+ (index + 1) * (columnCount + 1),
31
+ ),
32
+ )
33
+ : cells.map((cell) => (Array.isArray(cell) ? cell : [cell]));
34
+ const opts = guideFormatOpts(props);
35
+ if (!rows.length) {
36
+ return <Card className="min-h-16 bg-secondary p-0" aria-hidden />;
37
+ }
38
+ const legend = Array.isArray(page.legend) ? page.legend : [];
39
+ const legendItems = legend
40
+ .map((entry) => ({
41
+ color: cssColor(entry.color),
42
+ lines: (Array.isArray(entry.texts)
43
+ ? entry.texts.filter(Boolean)
44
+ : entry.text
45
+ ? [entry.text]
46
+ : []
47
+ ) as string[],
48
+ }))
49
+ .filter((entry) => entry.lines.length);
50
+
51
+ return (
52
+ <>
53
+ <Table className="border-collapse text-left">
54
+ {Array.isArray(page.columns) && page.columns.length ? (
55
+ <TableHeader>
56
+ <TableRow className="border-0 hover:bg-transparent">
57
+ {page.columns.map((column) => (
58
+ <TableHead
59
+ key={column}
60
+ className="h-auto border border-border bg-secondary px-2 py-1 whitespace-normal"
61
+ >
62
+ <GuideText text={column} {...opts} />
63
+ </TableHead>
64
+ ))}
65
+ </TableRow>
66
+ </TableHeader>
67
+ ) : null}
68
+ <TableBody>
69
+ {rows.map((row, rowIndex) => (
70
+ <TableRow key={rowIndex} className="border-0 hover:bg-transparent">
71
+ {row.map((cell, columnIndex) => {
72
+ const value: GuideTableCell | undefined =
73
+ cell && typeof cell === 'object' && !Array.isArray(cell)
74
+ ? (cell as GuideTableCell)
75
+ : undefined;
76
+ const items = value ? collectItemIds(value.item) : [];
77
+ const lines = value
78
+ ? cellTexts(value)
79
+ : typeof cell === 'string'
80
+ ? [cell]
81
+ : [];
82
+ return (
83
+ <TableCell
84
+ key={columnIndex}
85
+ className={
86
+ value?.bold
87
+ ? 'border border-border px-2 py-1 font-semibold whitespace-normal'
88
+ : 'border border-border px-2 py-1 whitespace-normal'
89
+ }
90
+ style={{ backgroundColor: cssColor(value?.fill) }}
91
+ >
92
+ <span className="inline-flex flex-wrap items-center gap-1">
93
+ {items.map((id) => (
94
+ <ItemEntry
95
+ key={id}
96
+ id={id}
97
+ name={id}
98
+ size="sm"
99
+ href={false}
100
+ />
101
+ ))}
102
+ {lines.map((line, index) => (
103
+ <span key={index}>
104
+ <GuideText text={line} {...opts} />
105
+ </span>
106
+ ))}
107
+ </span>
108
+ </TableCell>
109
+ );
110
+ })}
111
+ </TableRow>
112
+ ))}
113
+ </TableBody>
114
+ </Table>
115
+ {legendItems.length ? (
116
+ <ul className="mt-2 space-y-1 text-sm text-muted-foreground">
117
+ {legendItems.map((entry, index) => (
118
+ <li key={index} className="flex items-center gap-2">
119
+ {entry.color ? (
120
+ <span
121
+ className="inline-block size-3 rounded-sm"
122
+ style={{ backgroundColor: entry.color }}
123
+ />
124
+ ) : null}
125
+ <span className="flex flex-wrap gap-x-2">
126
+ {entry.lines.map((line, lineIndex) => (
127
+ <span key={lineIndex}>
128
+ <GuideText text={line} {...opts} />
129
+ </span>
130
+ ))}
131
+ </span>
132
+ </li>
133
+ ))}
134
+ </ul>
135
+ ) : null}
136
+ </>
137
+ );
138
+ }
@@ -0,0 +1,23 @@
1
+ import { Alert, AlertTitle } from '@terrafirmagreg-team/ui/components/ui/alert';
2
+ import type { GuidePage } from '@terrafirmagreg-team/guide/lib/data/dto';
3
+ import type { PageRendererProps } from '@terrafirmagreg-team/guide/types';
4
+
5
+ export function UnknownPage({
6
+ page,
7
+ index,
8
+ }: { page: GuidePage; index?: number } & PageRendererProps) {
9
+ const keys = Object.keys(page).filter((key) => key !== 'type');
10
+ return (
11
+ <Alert variant="destructive">
12
+ <AlertTitle>
13
+ Unknown page type &quot;{page.type ?? 'text'}&quot;
14
+ {index !== undefined ? ` on page ${index + 1}` : ''}
15
+ </AlertTitle>
16
+ {keys.length ? (
17
+ <pre className="mt-2 overflow-auto text-xs">
18
+ {JSON.stringify(page, null, 2)}
19
+ </pre>
20
+ ) : null}
21
+ </Alert>
22
+ );
23
+ }
@@ -0,0 +1,56 @@
1
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
2
+ import { ItemRow } from '@terrafirmagreg-team/ui/components/blocks/list/row';
3
+ import { guideBookHref } from '@terrafirmagreg-team/guide/lib/paths';
4
+ import { guideMessage } from '@terrafirmagreg-team/guide/lib/messages';
5
+ import type { GuideIndex, GuideMessages } from '@terrafirmagreg-team/guide/types';
6
+
7
+ type GuideCategoryBodyProps = {
8
+ dataset: string;
9
+ lang: string;
10
+ modId?: string;
11
+ bookPath?: string;
12
+ overview?: GuideIndex;
13
+ categoryId?: string;
14
+ messages: GuideMessages;
15
+ };
16
+
17
+ export function GuideCategoryBody({
18
+ dataset,
19
+ lang,
20
+ modId,
21
+ bookPath,
22
+ overview,
23
+ categoryId,
24
+ messages,
25
+ }: GuideCategoryBodyProps) {
26
+ const cat = overview?.categories.find((c) => c.id === categoryId);
27
+ return (
28
+ <div className="not-prose grid gap-3 sm:grid-cols-2">
29
+ {(cat?.entries ?? []).map((entry) => (
30
+ <ItemRow
31
+ key={entry.id}
32
+ href={guideBookHref(
33
+ dataset,
34
+ lang,
35
+ modId ?? overview?.modId ?? '',
36
+ bookPath ?? '',
37
+ entry.id,
38
+ )}
39
+ variant="outline"
40
+ className="rounded-xl bg-card p-4"
41
+ media={entry.icon?.id ? (
42
+ <Entry id={entry.icon.id} kind={entry.icon.kind} dataset={dataset} href={false} tooltip={false} />
43
+ ) : null}
44
+ title={entry.name}
45
+ description={
46
+ entry.pageCount
47
+ ? guideMessage(messages, 'guide.page_count', {
48
+ count: entry.pageCount,
49
+ })
50
+ : undefined
51
+ }
52
+ />
53
+ ))}
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,79 @@
1
+ import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
2
+ import { EntryView } from '@terrafirmagreg-team/guide/components/views/entry/view';
3
+ import { guideMessage } from '@terrafirmagreg-team/guide/lib/messages';
4
+ import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
5
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
6
+ import type {
7
+ GuideEntry,
8
+ GuideEntryExtras,
9
+ GuideIndex,
10
+ GuideMessages,
11
+ } from '@terrafirmagreg-team/guide/types';
12
+
13
+ type GuideEntryBodyProps = {
14
+ dataset: string;
15
+ lang: Lang;
16
+ bookPath?: string;
17
+ overview: GuideIndex;
18
+ entry?: GuideEntry;
19
+ categories: Map<string, string>;
20
+ langMap: Record<string, string>;
21
+ extras?: GuideEntryExtras;
22
+ questLinks?: Record<string, { filename: string; questId: string }>;
23
+ previews?: Record<string, PagePreviewData>;
24
+ messages: GuideMessages;
25
+ };
26
+
27
+ export function GuideEntryBody({
28
+ dataset,
29
+ lang,
30
+ bookPath,
31
+ overview,
32
+ entry,
33
+ categories,
34
+ langMap,
35
+ extras,
36
+ questLinks,
37
+ previews,
38
+ messages,
39
+ }: GuideEntryBodyProps) {
40
+ const catIconByEntry = new Map(
41
+ overview.categories.flatMap((category) =>
42
+ category.entries.map((item) => [item.id, category.icon] as const),
43
+ ),
44
+ );
45
+ const entryRefs = new Map(
46
+ overview.entries.map((item) => [
47
+ item.id,
48
+ {
49
+ id: item.id,
50
+ name: item.name ?? item.id,
51
+ icon: item.icon ?? catIconByEntry.get(item.id),
52
+ },
53
+ ]),
54
+ );
55
+ if (!entry || !bookPath) {
56
+ return (
57
+ <EmptyState variant="missing">
58
+ {guideMessage(messages, 'guide.entry_missing')}
59
+ </EmptyState>
60
+ );
61
+ }
62
+ return (
63
+ <EntryView
64
+ dataset={dataset}
65
+ lang={lang}
66
+ bookPath={bookPath}
67
+ entry={entry}
68
+ bookModId={overview.modId}
69
+ categoryByEntry={categories}
70
+ book={overview.book}
71
+ langMap={langMap}
72
+ entryRefs={entryRefs}
73
+ extras={extras}
74
+ questLinks={questLinks}
75
+ previews={previews}
76
+ messages={messages}
77
+ />
78
+ );
79
+ }
@@ -0,0 +1,97 @@
1
+ import { Entry } from '@terrafirmagreg-team/game/components/entry/entry';
2
+ import { ItemRow } from '@terrafirmagreg-team/ui/components/blocks/list/row';
3
+ import { MediaImage } from '@terrafirmagreg-team/ui/components/blocks/media/image';
4
+ import { GuideText } from '@terrafirmagreg-team/guide/components/views/text';
5
+ import {
6
+ guideCategoryHref,
7
+ isRootCategory,
8
+ } from '@terrafirmagreg-team/guide/lib/paths';
9
+ import { texturePublicUrl } from '@terrafirmagreg-team/game/lib/dump/assets/urls';
10
+ import { useDumpLocation } from '@terrafirmagreg-team/game/hooks/use-dump-location';
11
+ import { guideMessage } from '@terrafirmagreg-team/guide/lib/messages';
12
+ import type { PagePreviewData } from '@terrafirmagreg-team/ui/types/preview';
13
+ import type { Lang } from '@terrafirmagreg-team/translate/types/locale';
14
+ import type { GuideIndex, GuideMessages } from '@terrafirmagreg-team/guide/types';
15
+
16
+ type GuideOverviewBodyProps = {
17
+ dataset: string;
18
+ lang: Lang;
19
+ modId?: string;
20
+ bookPath?: string;
21
+ overview: GuideIndex;
22
+ categories: Map<string, string>;
23
+ langMap: Record<string, string>;
24
+ questLinks?: Record<string, { filename: string; questId: string }>;
25
+ previews?: Record<string, PagePreviewData>;
26
+ messages: GuideMessages;
27
+ };
28
+
29
+ export function GuideOverviewBody({
30
+ dataset,
31
+ lang,
32
+ modId,
33
+ bookPath,
34
+ overview,
35
+ categories,
36
+ langMap,
37
+ questLinks,
38
+ previews,
39
+ messages,
40
+ }: GuideOverviewBodyProps) {
41
+ useDumpLocation(dataset);
42
+ if (overview.status === 'missing') return null;
43
+ return (
44
+ <div className="space-y-6">
45
+ {overview.book?.cover ? (
46
+ <MediaImage
47
+ src={texturePublicUrl(dataset, overview.book.cover.id)}
48
+ alt={overview.book.name}
49
+ eager
50
+ className="h-auto max-h-64 max-w-full rounded-lg border border-border object-contain"
51
+ pixelated
52
+ />
53
+ ) : null}
54
+ {overview.book?.landingText ? (
55
+ <p className="whitespace-pre-wrap text-muted-foreground">
56
+ <GuideText
57
+ text={overview.book.landingText}
58
+ dataset={dataset}
59
+ lang={lang}
60
+ modId={modId}
61
+ bookPath={bookPath}
62
+ categoryByEntry={categories}
63
+ macros={overview.book.macros}
64
+ langMap={langMap}
65
+ questLinks={questLinks}
66
+ previews={previews}
67
+ />
68
+ </p>
69
+ ) : null}
70
+ <div className="not-prose grid gap-3 sm:grid-cols-2">
71
+ {overview.categories
72
+ .filter((cat) => isRootCategory(cat, overview.categories))
73
+ .map((cat) => (
74
+ <ItemRow
75
+ key={cat.id}
76
+ href={guideCategoryHref(
77
+ dataset,
78
+ lang,
79
+ modId ?? overview.modId,
80
+ bookPath ?? '',
81
+ cat,
82
+ )}
83
+ variant="outline"
84
+ className="rounded-xl bg-card p-4"
85
+ media={cat.icon?.id ? (
86
+ <Entry id={cat.icon.id} kind={cat.icon.kind} dataset={dataset} href={false} tooltip={false} />
87
+ ) : null}
88
+ title={cat.name}
89
+ description={guideMessage(messages, 'guide.entry_count', {
90
+ count: cat.entries.length,
91
+ })}
92
+ />
93
+ ))}
94
+ </div>
95
+ </div>
96
+ );
97
+ }