Skip to content
88 linesCodeBlameRaw
1/**
2 * How Artifacts' pages change things: a JSON request to `-/artifacts/api`,
3 * then the sidebar and page load again so they show it. Returns the
4 * service's `{ ok, value }` or `{ ok: false, error }`.
5 */
6import type { Result } from "@g1t/contracts";
7import { useCallback, useEffect, useState } from "react";
8import { useRevalidator, useRouteLoaderData } from "react-router";
9
10import { browserTimeZone } from "../../lib/time-zone";
11import type { FoliosLayoutData } from "../../routes/workspace/folios/layout";
12
13export function useFoliosData(): FoliosLayoutData | undefined {
14 return useRouteLoaderData("routes/workspace/folios/layout") as FoliosLayoutData | undefined;
15}
16
17/**
18 * The viewer's time zone, for the home list's days. The server renders
19 * with the zone the browser last told it (the `g1t_tz` cookie, else UTC);
20 * the browser then uses its own and remembers it for next time.
21 */
22export function useViewerZone(): string {
23 const saved = useFoliosData()?.zone ?? null;
24 const [zone, setZone] = useState(saved ?? "UTC");
25 useEffect(() => {
26 const own = browserTimeZone();
27 if (!own) return;
28 setZone(own);
29 if (own !== saved) document.cookie = `g1t_tz=${encodeURIComponent(own)}; Path=/; Max-Age=31536000; SameSite=Lax`;
30 }, [saved]);
31 return zone;
32}
33
34/** How long a request may take before the page says so instead of waiting on. */
35export const FOLIOS_TIMEOUT_MS = 20_000;
36
37/** What a request that failed or took too long says. */
38export function foliosFailure<T>(error: unknown): Result<T> {
39 const late = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
40 const message = late ? "Artifacts took too long to answer. Try again in a moment." : "Artifacts didn't answer. Check your connection and try again.";
41 return { ok: false, error: { code: "conflict", message } };
42}
43
44export async function foliosRequest<T>(slug: string, intent: string, body: Record<string, unknown> = {}): Promise<Result<T>> {
45 try {
46 const response = await fetch(`/${slug}/-/artifacts/api`, {
47 method: "POST",
48 headers: { "content-type": "application/json", accept: "application/json" },
49 body: JSON.stringify({ intent, ...body }),
50 signal: AbortSignal.timeout(FOLIOS_TIMEOUT_MS),
51 });
52 return (await response.json()) as Result<T>;
53 } catch (error) {
54 return foliosFailure(error);
55 }
56}
57
58export async function foliosQuery<T>(slug: string, query: Record<string, string>): Promise<Result<T>> {
59 try {
60 const response = await fetch(`/${slug}/-/artifacts/api?${new URLSearchParams(query)}`, {
61 headers: { accept: "application/json" },
62 signal: AbortSignal.timeout(FOLIOS_TIMEOUT_MS),
63 });
64 return (await response.json()) as Result<T>;
65 } catch (error) {
66 return foliosFailure(error);
67 }
68}
69
70/** `send(intent, body)`: makes the change, then reloads what shows it. `busy` while it works; `error` when it failed. */
71export function useFoliosAction(slug: string) {
72 const revalidator = useRevalidator();
73 const [busy, setBusy] = useState(false);
74 const [error, setError] = useState<string | null>(null);
75 const send = useCallback(
76 async <T>(intent: string, body: Record<string, unknown> = {}, options: { reload?: boolean } = {}): Promise<Result<T>> => {
77 setBusy(true);
78 setError(null);
79 const result = await foliosRequest<T>(slug, intent, body);
80 setBusy(false);
81 if (!result.ok) setError(result.error.message);
82 else if (options.reload !== false) void revalidator.revalidate();
83 return result;
84 },
85 [slug, revalidator],
86 );
87 return { send, busy, error, setError };
88}