v1.5 - history and timeline as well as many datastructure updates added, polished, fixes
This commit is contained in:
parent
1f0aa3078f
commit
1d84400aef
63 changed files with 7927 additions and 208 deletions
87
src/lib/useHistory.ts
Normal file
87
src/lib/useHistory.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* Loads the history timeline from `GET /api/history`.
|
||||
*
|
||||
* Goes through the shared client, so the request is deduped and cached
|
||||
* for 60s like every other read. Two consequences worth knowing:
|
||||
*
|
||||
* · `reload` has to invalidate before it refetches. Without that, the
|
||||
* retry button inside the TTL would hand back the same settled
|
||||
* promise and look like it did nothing. A *failed* request is
|
||||
* already evicted by the client, so this matters for the refresh
|
||||
* case rather than the error case.
|
||||
*
|
||||
* · there's no AbortController. `get` shares one promise between
|
||||
* callers, so aborting on unmount would cancel someone else's
|
||||
* request. The `live` flag drops the result instead.
|
||||
*
|
||||
* No `fallback` on purpose. Handing this the mock data would render a
|
||||
* plausible-looking history with no indication the server is down, and
|
||||
* the wrong history is worse than a visible error — the same reason
|
||||
* `fallback: EMPTY` came out elsewhere.
|
||||
*
|
||||
* `undated` is the number of published entries the API left out because
|
||||
* nothing gave them a date. Not rendered publicly — a visitor can't act
|
||||
* on it — but returned so it's reachable if you want a warning in the
|
||||
* admin later.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { get, invalidate, ApiError } from './api.js'
|
||||
import type { TimelineItem } from './timeline'
|
||||
|
||||
const PATH = '/history'
|
||||
|
||||
type HistoryResponse = {
|
||||
items?: TimelineItem[]
|
||||
undated?: number
|
||||
}
|
||||
|
||||
type State = {
|
||||
items: TimelineItem[]
|
||||
undated: number
|
||||
loading: boolean
|
||||
error: string | null
|
||||
reload: () => void
|
||||
}
|
||||
|
||||
export function useHistory(): State {
|
||||
const [items, setItems] = useState<TimelineItem[]>([])
|
||||
const [undated, setUndated] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [attempt, setAttempt] = useState(0)
|
||||
|
||||
const reload = useCallback(() => {
|
||||
invalidate(PATH)
|
||||
setAttempt((n) => n + 1)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let live = true
|
||||
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const data: HistoryResponse = await get(PATH)
|
||||
if (!live) return
|
||||
setItems(Array.isArray(data?.items) ? data.items : [])
|
||||
setUndated(Number(data?.undated) || 0)
|
||||
} catch (err) {
|
||||
if (!live) return
|
||||
setError(
|
||||
err instanceof ApiError ? err.message : "Couldn't reach the server.",
|
||||
)
|
||||
} finally {
|
||||
if (live) setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
return () => {
|
||||
live = false
|
||||
}
|
||||
}, [attempt])
|
||||
|
||||
return { items, undated, loading, error, reload }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue