v3: added site-wide categories, added admin category management, various visual fixes
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m52s

This commit is contained in:
khannurien
2026-06-28 16:23:44 +00:00
parent c8c7b05c25
commit fae25f3e6c
44 changed files with 2144 additions and 399 deletions

View File

@@ -0,0 +1,18 @@
import { createContext } from "react";
import type { Category } from "../model.ts";
export interface CategoriesContextValue {
categories: Category[];
// Lookup by id for resolving dump category chips.
byId: Map<string, Category>;
isLoaded: boolean;
// Re-fetch the list (used after admin create/update/delete).
refresh: () => Promise<void>;
}
export const CategoriesContext = createContext<CategoriesContextValue>({
categories: [],
byId: new Map(),
isLoaded: false,
refresh: async () => {},
});

View File

@@ -0,0 +1,53 @@
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import {
CategoriesContext,
type CategoriesContextValue,
} from "./CategoriesContext.ts";
import { API_URL } from "../config/api.ts";
import {
type Category,
deserializeCategory,
type RawCategory,
} from "../model.ts";
export function CategoriesProvider({ children }: { children: ReactNode }) {
const [categories, setCategories] = useState<Category[]>([]);
const [isLoaded, setIsLoaded] = useState(false);
const load = useCallback(async (signal?: AbortSignal) => {
try {
const res = await fetch(`${API_URL}/api/categories`, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.json();
if (!body.success) return;
setCategories((body.data as RawCategory[]).map(deserializeCategory));
setIsLoaded(true);
} catch (err) {
if ((err as Error).name !== "AbortError") setIsLoaded(true);
}
}, []);
useEffect(() => {
const controller = new AbortController();
load(controller.signal);
return () => controller.abort();
}, [load]);
const refresh = useCallback(() => load(), [load]);
const byId = useMemo(
() => new Map(categories.map((c) => [c.id, c])),
[categories],
);
const value: CategoriesContextValue = useMemo(
() => ({ categories, byId, isLoaded, refresh }),
[categories, byId, isLoaded, refresh],
);
return (
<CategoriesContext.Provider value={value}>
{children}
</CategoriesContext.Provider>
);
}