-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseStoriesData.js
More file actions
113 lines (100 loc) · 3.3 KB
/
Copy pathuseStoriesData.js
File metadata and controls
113 lines (100 loc) · 3.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { useEffect, useRef, useState } from 'react';
import piml from 'piml';
/**
* Shared loaders for the "From Serfs and Frauds" archive data in public/stories.
* Both the medieval /stories view and the /snf terminal read the same files;
* these hooks centralise fetching + PIML parsing with a module-level cache so
* navigating between terminal pages doesn't refetch.
*/
const cache = new Map();
function loadPiml(url) {
if (cache.has(url)) return cache.get(url);
const promise = (async () => {
const res = await fetch(url);
if (!res.ok) throw new Error(`Failed to load ${url} (${res.status})`);
return piml.parse(await res.text());
})();
cache.set(url, promise);
promise.catch(() => cache.delete(url)); // don't cache failures
return promise;
}
function usePimlResource(url, select) {
const selectRef = useRef(select);
selectRef.current = select;
const [state, setState] = useState({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let active = true;
setState((s) => ({ ...s, loading: true, error: null }));
loadPiml(url)
.then((parsed) => {
if (!active) return;
const data = selectRef.current ? selectRef.current(parsed) : parsed;
setState({ data, loading: false, error: null });
})
.catch((error) => {
if (active) setState({ data: null, loading: false, error });
});
return () => {
active = false;
};
}, [url]);
return state;
}
export function useBooks(language = 'en') {
return usePimlResource(`/stories/books_${language || 'en'}.piml`, (d) =>
(d.books || []).slice().sort((a, b) => Number(a.bookId) - Number(b.bookId)),
);
}
export function useCharacters() {
return usePimlResource('/stories/characters.piml', (d) => d.characters || []);
}
export function usePlaces() {
return usePimlResource('/stories/places.piml', (d) => d.places || []);
}
export function useItems() {
return usePimlResource('/stories/meta-items/items.piml', (d) => d.items || []);
}
export function useAuthors() {
return usePimlResource('/stories/authors.piml', (d) => d.authors || []);
}
/** Fetches a single episode's raw text body by its `filename`. */
export function useEpisodeText(filename) {
const [state, setState] = useState({ text: '', loading: true, error: null });
useEffect(() => {
if (!filename) return undefined;
let active = true;
setState({ text: '', loading: true, error: null });
fetch(`/stories/${filename}`)
.then((res) => {
if (!res.ok) throw new Error(`Failed to load ${filename} (${res.status})`);
return res.text();
})
.then((text) => active && setState({ text, loading: false, error: null }))
.catch(
(error) => active && setState({ text: '', loading: false, error }),
);
return () => {
active = false;
};
}, [filename]);
return state;
}
/** Cross-reference: list of {bookId, bookTitle} an author contributed to. */
export function getBooksByAuthor(books, name, alias) {
const out = [];
(books || []).forEach((book) => {
(book.episodes || []).forEach((ep) => {
if (
(ep.author === name || ep.author === alias) &&
!out.some((b) => b.bookId === book.bookId)
) {
out.push({ bookId: book.bookId, bookTitle: book.bookTitle });
}
});
});
return out;
}