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
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 2m52s
This commit is contained in:
18
src/contexts/CategoriesContext.ts
Normal file
18
src/contexts/CategoriesContext.ts
Normal 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 () => {},
|
||||
});
|
||||
53
src/contexts/CategoriesProvider.tsx
Normal file
53
src/contexts/CategoriesProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user