Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Artifacts is a mode in the rail, the shell and the phone's tab bar in place of Docs: its home lists everything you can open by day or as cards under All, Yours and Shared with you, with search, filters and tiles to start a doc (slides, designs and dashboards say they are coming soon), and its sidebar holds favorites, spaces, Private, Shared, projects' docs and the trash. | 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 | */ | |
| 6 | import type { Result } from "@g1t/contracts"; | |
| 7 | import { useCallback, useEffect, useState } from "react"; | |
| 8 | import { useRevalidator, useRouteLoaderData } from "react-router"; | |
| 9 | ||
| 10 | import { browserTimeZone } from "../../lib/time-zone"; | |
| 11 | import type { FoliosLayoutData } from "../../routes/workspace/folios/layout"; | |
| 12 | ||
| 13 | export 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 | */ | |
| 22 | export 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. */ | |
| 35 | export const FOLIOS_TIMEOUT_MS = 20_000; | |
| 36 | ||
| 37 | /** What a request that failed or took too long says. */ | |
| 38 | export 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 | ||
| 44 | export 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 | ||
| 58 | export 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. */ | |
| 71 | export 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 | } |