import { useCallback, useEffect, useRef, useState } from 'react';
import {
addPhotos,
adminCategories,
adminProducts,
arrangePhotos,
createCategory,
createProduct,
deleteCategory,
deleteProduct,
describeProduct,
renameCategory,
reorderCategories,
reorderProducts,
type AdminCategory,
type AdminProduct,
} from '@/lib/api';
import Catering from '@/components/admin/Catering';
import {
ARROW_DOWN,
ARROW_LEFT,
ARROW_RIGHT,
ARROW_UP,
CHECK,
Icon,
PLUS,
TRASH,
X,
danger,
field,
iconButton,
primary,
secondary,
shift,
} from '@/components/admin/ui';
/**
* The catalogue, editable by the person who bakes it.
*
* The whole screen is built around one rule: nothing waits on the network to look like it happened.
* Reordering and deleting apply to the list on screen first and reconcile afterwards, because an
* editor tidying twenty items shouldn't be typing into a page that freezes between every click.
* Uploads are the exception — they genuinely take a moment (resize, convert, send), so they say so.
*
* Getting here at all means signing in: /admin is an authenticated path, so an unknown visitor is
* sent to the identity provider before this ever loads.
*/
// --- photos -----------------------------------------------------------------
/**
* The photos on one item. Order matters — the first is the one the products page leads with — so
* arranging is left/right rather than a drag target, which is far easier to hit on a phone.
*/
const Photos = ({
product,
onArrange,
busy,
}: {
product: AdminProduct;
onArrange: (keys: string[]) => void;
busy: boolean;
}) => (
{product.images.map((url, i) => (
))}
);
/** A file picker styled as a button, resetting itself so the same file can be chosen twice. */
const PhotoPicker = ({
label,
onPick,
disabled,
className = secondary,
}: {
label: string;
onPick: (files: File[]) => void;
disabled?: boolean;
className?: string;
}) => {
const input = useRef(null);
return (
<>
{
const files = Array.from(e.target.files ?? []);
e.target.value = '';
if (files.length) onPick(files);
}}
/>
>
);
};
// --- one item ---------------------------------------------------------------
const ProductCard = ({
product,
categories,
first,
last,
onChange,
onMove,
onDelete,
onError,
}: {
product: AdminProduct;
categories: string[];
first: boolean;
last: boolean;
onChange: (updated: AdminProduct) => void;
onMove: (delta: number) => void;
onDelete: () => void;
onError: (message: string) => void;
}) => {
const [name, setName] = useState(product.name);
const [category, setCategory] = useState(product.category);
const [busy, setBusy] = useState(false);
// A reorder or an upload re-fetches this product; the fields should follow unless they're being
// edited, which is what the dirty check below decides.
const dirty = name !== product.name || category !== product.category;
const [synced, setSynced] = useState(product);
if (synced !== product) {
setSynced(product);
if (!dirty) {
setName(product.name);
setCategory(product.category);
}
}
const run = async (work: () => Promise) => {
setBusy(true);
try {
onChange(await work());
} catch (e) {
onError(e instanceof Error ? e.message : 'That did not save.');
} finally {
setBusy(false);
}
};
return (