@terrafirmagreg-team/guide 1.2.6 → 1.2.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/guide",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "type": "module",
5
5
  "description": "Guide/books section",
6
6
  "license": "LGPL-3.0-only",
@@ -22,17 +22,17 @@
22
22
  "./types": "./src/types/index.ts"
23
23
  },
24
24
  "dependencies": {
25
- "@terrafirmagreg-team/data": "^1.2.1",
26
- "@terrafirmagreg-team/discussions": "^1.2.3",
27
- "@terrafirmagreg-team/dump": "^1.2.2",
28
- "@terrafirmagreg-team/game": "^1.2.3",
29
- "@terrafirmagreg-team/kit": "^1.2.5",
30
- "@terrafirmagreg-team/mdx": "^1.2.2",
31
- "@terrafirmagreg-team/preview": "^1.2.1",
32
- "@terrafirmagreg-team/search": "^1.2.3",
25
+ "@terrafirmagreg-team/data": "^1.2.2",
26
+ "@terrafirmagreg-team/discussions": "^1.2.6",
27
+ "@terrafirmagreg-team/dump": "^1.2.4",
28
+ "@terrafirmagreg-team/game": "^1.2.4",
29
+ "@terrafirmagreg-team/kit": "^1.2.9",
30
+ "@terrafirmagreg-team/mdx": "^1.2.4",
31
+ "@terrafirmagreg-team/preview": "^1.2.3",
32
+ "@terrafirmagreg-team/search": "^1.2.5",
33
33
  "@terrafirmagreg-team/sites": "^1.2.1",
34
- "@terrafirmagreg-team/translate": "^1.2.4",
35
- "@terrafirmagreg-team/ui": "^1.2.4",
34
+ "@terrafirmagreg-team/translate": "^1.2.6",
35
+ "@terrafirmagreg-team/ui": "^1.2.7",
36
36
  "fumadocs-core": "16.17.10",
37
37
  "lucide-react": "^1.63.0"
38
38
  },
@@ -54,8 +54,9 @@ function bookLeaf(
54
54
  slug: string,
55
55
  raw: GuideBookJson | undefined,
56
56
  map: Record<string, string>,
57
- ): PageTree.Item {
57
+ ): PageTree.Item | undefined {
58
58
  const book = toBook(raw, ref, slug, map);
59
+ if (!book.name.trim()) return undefined;
59
60
  const bookId = bookOptionId(ref.modId, ref.bookPath);
60
61
  return keyTreeNode({
61
62
  type: 'page',
@@ -73,6 +74,7 @@ async function datasetBookLeaves(
73
74
  const dump = await loadDatasetDump(dataset);
74
75
  if (!dump) return [];
75
76
  const taken = new Set<string>();
77
+ const seen = new Set<string>();
76
78
  const leaves: PageTree.Node[] = [];
77
79
  for (const mod of dump.mods) {
78
80
  const raws = await Promise.all(
@@ -85,14 +87,19 @@ async function datasetBookLeaves(
85
87
  for (const [i, ref] of mod.books.entries()) {
86
88
  const raw = raws[i];
87
89
  if (!raw) continue;
90
+ const bookId = bookOptionId(ref.modId, ref.bookPath);
91
+ if (seen.has(bookId)) continue;
88
92
  let slug = bookSlug(raw, ref);
89
93
  if (taken.has(slug)) {
90
94
  let n = 2;
91
95
  while (taken.has(`${slug}-${n}`)) n += 1;
92
96
  slug = `${slug}-${n}`;
93
97
  }
98
+ const leaf = bookLeaf(dataset, ref, slug, raw, langMap);
99
+ if (!leaf) continue;
94
100
  taken.add(slug);
95
- leaves.push(bookLeaf(dataset, ref, slug, raw, langMap));
101
+ seen.add(bookId);
102
+ leaves.push(leaf);
96
103
  }
97
104
  }
98
105
  return leaves;
@@ -2,12 +2,20 @@
2
2
  import { useEffect, useMemo } from 'react';
3
3
  import { useRouter } from 'next/navigation';
4
4
  import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
5
- import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
5
+ import {
6
+ useAsyncResult,
7
+ useAsyncValue,
8
+ } from '@terrafirmagreg-team/ui/hooks/use-async-value';
9
+ import { useLastResult } from '@terrafirmagreg-team/ui/hooks/use-last-result';
6
10
  import { useVersionedDataset } from '@terrafirmagreg-team/kit/hooks/use-versioned-dataset';
11
+ import { useDumpRetryNonce } from '@terrafirmagreg-team/kit/hooks/use-dump-status';
12
+ import { DumpUnavailable } from '@terrafirmagreg-team/kit/components/views/unavailable';
13
+ import { StaleBadge } from '@terrafirmagreg-team/kit/components/dump-status/stale-badge';
7
14
  import { useLangMap } from '@terrafirmagreg-team/game/hooks/use-lang-map';
8
15
  import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
9
16
  import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
10
17
  import { Skeleton } from '@terrafirmagreg-team/ui/components/ui/skeleton';
18
+ import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
11
19
  import { WorkspaceBody } from '@terrafirmagreg-team/ui/components/layouts/workspace';
12
20
  import { GuidePageView } from '@terrafirmagreg-team/guide/components/views/page/view';
13
21
  import {
@@ -59,9 +67,13 @@ export function GuideBrowser({
59
67
  [params],
60
68
  );
61
69
  const source = getGuideSource();
62
- const books = useAsyncValue(
63
- () => source.books(dataset, lang),
64
- [dataset, lang],
70
+ const nonce = useDumpRetryNonce();
71
+ const books = useLastResult(
72
+ useAsyncResult(
73
+ () => source.books(dataset, lang),
74
+ [dataset, lang, nonce],
75
+ ),
76
+ dataset.id,
65
77
  );
66
78
  const langMap = useLangMap(dataset, lang);
67
79
  const messages = useMemo<GuideMessages>(
@@ -87,47 +99,79 @@ export function GuideBrowser({
87
99
 
88
100
  const resolved = useResolvedLang();
89
101
  const canonical = useAsyncValue(
90
- books && !bookId
102
+ books.value && !bookId
91
103
  ? async () => {
92
- const book = books[0];
104
+ const book = books.value?.[0];
93
105
  if (!book) return undefined;
94
106
  const index = await source.book(dataset, lang, book.bookId);
95
107
  const first = index ? firstGuideEntryId(index) : undefined;
96
108
  return guideHref(dataset, resolved, book.bookId, first);
97
109
  }
98
110
  : undefined,
99
- [books, bookId, dataset, lang, resolved],
111
+ [books.value, bookId, dataset, lang, resolved],
100
112
  );
101
113
 
102
114
  useEffect(() => {
103
115
  if (canonical) router.replace(canonical);
104
116
  }, [canonical, router]);
105
117
 
106
- const payload = useAsyncValue(
107
- langMap && bookId
108
- ? () =>
109
- source.payload(
110
- dataset,
111
- lang,
112
- bookId,
113
- entryId,
114
- categoryId,
115
- langMap,
116
- messages,
117
- )
118
- : undefined,
119
- [dataset, lang, bookId, entryId, categoryId, langMap, messages],
118
+ const payload = useLastResult(
119
+ useAsyncResult(
120
+ langMap && bookId
121
+ ? () =>
122
+ source.payload(
123
+ dataset,
124
+ lang,
125
+ bookId,
126
+ entryId,
127
+ categoryId,
128
+ langMap,
129
+ messages,
130
+ )
131
+ : undefined,
132
+ [dataset, lang, bookId, entryId, categoryId, langMap, messages, nonce],
133
+ ),
134
+ `${dataset.id}:${bookId ?? ''}`,
120
135
  );
121
136
 
122
137
  useEffect(() => {
123
- if (payload?.title) {
124
- document.title = `${payload.title} · ${datasetTitle} | ${guideConfig().appName}`;
138
+ if (payload.value?.title) {
139
+ document.title = `${payload.value.title} · ${datasetTitle} | ${guideConfig().appName}`;
125
140
  }
126
- }, [payload?.title, datasetTitle]);
141
+ }, [payload.value?.title, datasetTitle]);
142
+
143
+ const unavailable = (
144
+ <WorkspaceBody>
145
+ <DumpUnavailable>
146
+ {t('Guide data is temporarily unavailable. Try again later.', {
147
+ note: 'guide.unavailable',
148
+ })}
149
+ </DumpUnavailable>
150
+ </WorkspaceBody>
151
+ );
127
152
 
128
- if (!bookId || !payload || !langMap) {
153
+ if (books.error || (!books.pending && !books.value?.length)) {
154
+ return unavailable;
155
+ }
156
+ if (!bookId || !langMap || payload.pending) {
129
157
  return <GuideBrowserFallback />;
130
158
  }
131
- return <GuidePageView payload={payload} langMap={langMap} />;
159
+ if (!payload.value) {
160
+ return payload.error ? (
161
+ unavailable
162
+ ) : (
163
+ <WorkspaceBody>
164
+ <EmptyState variant="missing">
165
+ {t('Entry not found', { note: 'guide.entry_missing' })}
166
+ </EmptyState>
167
+ </WorkspaceBody>
168
+ );
169
+ }
170
+ return (
171
+ <div className="relative flex-1">
172
+ {books.stale || payload.stale ? <StaleBadge /> : null}
173
+ <GuidePageView payload={payload.value} langMap={langMap} />
174
+ </div>
175
+ );
132
176
  }
133
177
 
@@ -5,6 +5,7 @@ import type * as PageTree from 'fumadocs-core/page-tree';
5
5
  import { useLocationSearchParams } from '@terrafirmagreg-team/ui/hooks/page/use-location-search';
6
6
  import { useResolvedLang } from '@terrafirmagreg-team/translate/hooks/use-resolved-lang';
7
7
  import { useLastValue } from '@terrafirmagreg-team/ui/hooks/use-last-value';
8
+ import { skeletonTree } from '@terrafirmagreg-team/ui/lib/tree';
8
9
  import { useGuideTree } from '@terrafirmagreg-team/guide/hooks/use-tree';
9
10
  import { guideQueryFromParams } from '@terrafirmagreg-team/guide/lib/nav/query';
10
11
  import { parseSectionLocation } from '@terrafirmagreg-team/sites/location';
@@ -19,10 +20,8 @@ export function useGuideLeftSidebarTree(tree: PageTree.Root): PageTree.Root {
19
20
  () => (datasetId ? { id: datasetId, version } : undefined),
20
21
  [datasetId, version],
21
22
  );
22
- const runtime = useLastValue(
23
- useGuideTree(dataset, lang, bookId),
24
- dataset?.id ?? '',
25
- );
23
+ const result = useGuideTree(dataset, lang, bookId);
24
+ const runtime = useLastValue(result.value, dataset?.id ?? '');
25
+ if (!runtime && result.pending) return skeletonTree();
26
26
  return runtime ?? tree;
27
27
  }
28
-
@@ -2,7 +2,8 @@
2
2
  import type { DatasetRef } from '@terrafirmagreg-team/sites/location';
3
3
 
4
4
  import type * as PageTree from 'fumadocs-core/page-tree';
5
- import { useAsyncValue } from '@terrafirmagreg-team/ui/hooks/use-async-value';
5
+ import { useAsyncResult } from '@terrafirmagreg-team/ui/hooks/use-async-value';
6
+ import { useDumpRetryNonce } from '@terrafirmagreg-team/kit/hooks/use-dump-status';
6
7
  import { loadClientGuideBook } from '@terrafirmagreg-team/guide/lib/client/books';
7
8
  import { listGuideBooks } from '@terrafirmagreg-team/guide/lib/book/list';
8
9
  import { guideTreeChildren } from '@terrafirmagreg-team/guide/components/tree/children';
@@ -12,8 +13,9 @@ export function useGuideTree(
12
13
  dataset: DatasetRef | undefined,
13
14
  lang: Lang,
14
15
  bookId: string | undefined,
15
- ): PageTree.Root | undefined {
16
- return useAsyncValue(
16
+ ): { value: PageTree.Root | undefined; pending: boolean; error: unknown } {
17
+ const nonce = useDumpRetryNonce();
18
+ return useAsyncResult(
17
19
  dataset
18
20
  ? async () => {
19
21
  const summaries = await listGuideBooks(dataset, lang);
@@ -29,7 +31,6 @@ export function useGuideTree(
29
31
  } satisfies PageTree.Root;
30
32
  }
31
33
  : undefined,
32
- [dataset, lang, bookId],
34
+ [dataset, lang, bookId, nonce],
33
35
  );
34
36
  }
35
-
package/src/lib/config.ts CHANGED
@@ -31,7 +31,7 @@ export type GuideConfig = {
31
31
  const { register: registerGuideConfig, get: guideConfig } = createConfigRegistry<
32
32
  GuideConfigInput,
33
33
  GuideConfig
34
- >((overrides) => ({
34
+ >('guide', (overrides) => ({
35
35
  appName: overrides.appName ?? uiConfig().brand?.name ?? '',
36
36
  components: {
37
37
  PageActions: (overrides.components?.PageActions ??