Initial commit: Apothecary v0.4.0

This commit is contained in:
2026-05-03 20:19:26 -04:00
commit 027cf032be
55 changed files with 14678 additions and 0 deletions
+273
View File
@@ -0,0 +1,273 @@
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "../../api.js";
import { Btn, Field, Input } from "../primitives/index.js";
import { ModalBackdrop, ModalHeader, ModalFooter } from "./AddProductFlow.js";
export function AddBrandModal({ onClose }: { onClose: () => void }) {
const qc = useQueryClient();
const [name, setName] = useState("");
const create = useMutation({
mutationFn: () => api.createBrand(name.trim()),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["bootstrap"] });
onClose();
},
});
return (
<ModalBackdrop onClose={onClose}>
<div
style={{
width: "min(480px, 96vw)",
margin: "40px 20px",
background: "var(--bg)",
border: "1px solid var(--line)",
borderRadius: "var(--r-lg)",
boxShadow: "var(--shadow-lg)",
}}
>
<ModalHeader title="Add a brand" eyebrow="Catalog" onClose={onClose} />
<div style={{ padding: 32 }}>
<Field label="Brand name">
<Input
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Foxglove Farms"
/>
</Field>
</div>
<ModalFooter>
<div />
<div style={{ display: "flex", gap: 8 }}>
<Btn variant="ghost" onClick={onClose}>Cancel</Btn>
<Btn
variant="primary"
icon="check"
disabled={!name.trim() || create.isPending}
onClick={() => create.mutate()}
>
{create.isPending ? "Saving…" : "Add brand"}
</Btn>
</div>
</ModalFooter>
</div>
</ModalBackdrop>
);
}
export function EditBinModal({
bin,
onClose,
}: {
bin: { id: string; name: string; location: string | null; capacity: number };
onClose: () => void;
}) {
const qc = useQueryClient();
const [name, setName] = useState(bin.name);
const [location, setLocation] = useState(bin.location ?? "");
const [capacity, setCapacity] = useState(bin.capacity);
const update = useMutation({
mutationFn: () =>
api.updateBin(bin.id, { name: name.trim(), location: location.trim(), capacity }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["bootstrap"] });
onClose();
},
});
return (
<ModalBackdrop onClose={onClose}>
<div
style={{
width: "min(560px, 96vw)",
margin: "40px 20px",
background: "var(--bg)",
border: "1px solid var(--line)",
borderRadius: "var(--r-lg)",
boxShadow: "var(--shadow-lg)",
}}
>
<ModalHeader title="Edit bin" eyebrow="Storage" onClose={onClose} />
<div style={{ padding: 32, display: "grid", gap: 16 }}>
<Field label="Bin name">
<Input
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Top Drawer"
/>
</Field>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16 }}>
<Field label="Location (optional)">
<Input
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="e.g. Bedroom"
/>
</Field>
<Field label="Capacity">
<Input
type="number"
min={1}
step={1}
value={capacity}
onChange={(e) => setCapacity(Math.max(1, Math.floor(+e.target.value || 1)))}
/>
</Field>
</div>
</div>
<ModalFooter>
<div />
<div style={{ display: "flex", gap: 8 }}>
<Btn variant="ghost" onClick={onClose}>Cancel</Btn>
<Btn
variant="primary"
icon="check"
disabled={!name.trim() || update.isPending}
onClick={() => update.mutate()}
>
{update.isPending ? "Saving…" : "Save changes"}
</Btn>
</div>
</ModalFooter>
</div>
</ModalBackdrop>
);
}
export function AddBinModal({ onClose }: { onClose: () => void }) {
const qc = useQueryClient();
const [name, setName] = useState("");
const [location, setLocation] = useState("");
const [capacity, setCapacity] = useState(10);
const create = useMutation({
mutationFn: () =>
api.createBin({ name: name.trim(), location: location.trim(), capacity }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["bootstrap"] });
onClose();
},
});
return (
<ModalBackdrop onClose={onClose}>
<div
style={{
width: "min(560px, 96vw)",
margin: "40px 20px",
background: "var(--bg)",
border: "1px solid var(--line)",
borderRadius: "var(--r-lg)",
boxShadow: "var(--shadow-lg)",
}}
>
<ModalHeader title="Add a bin" eyebrow="Storage" onClose={onClose} />
<div style={{ padding: 32, display: "grid", gap: 16 }}>
<Field label="Bin name">
<Input
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Top Drawer"
/>
</Field>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 16 }}>
<Field label="Location (optional)">
<Input
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="e.g. Bedroom"
/>
</Field>
<Field label="Capacity">
<Input
type="number"
min={1}
step={1}
value={capacity}
onChange={(e) => setCapacity(Math.max(1, Math.floor(+e.target.value || 1)))}
/>
</Field>
</div>
</div>
<ModalFooter>
<div />
<div style={{ display: "flex", gap: 8 }}>
<Btn variant="ghost" onClick={onClose}>Cancel</Btn>
<Btn
variant="primary"
icon="check"
disabled={!name.trim() || create.isPending}
onClick={() => create.mutate()}
>
{create.isPending ? "Saving…" : "Add bin"}
</Btn>
</div>
</ModalFooter>
</div>
</ModalBackdrop>
);
}
export function AddShopModal({ onClose }: { onClose: () => void }) {
const qc = useQueryClient();
const [name, setName] = useState("");
const [location, setLocation] = useState("");
const create = useMutation({
mutationFn: () => api.createShop({ name: name.trim(), location: location.trim() }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["bootstrap"] });
onClose();
},
});
return (
<ModalBackdrop onClose={onClose}>
<div
style={{
width: "min(560px, 96vw)",
margin: "40px 20px",
background: "var(--bg)",
border: "1px solid var(--line)",
borderRadius: "var(--r-lg)",
boxShadow: "var(--shadow-lg)",
}}
>
<ModalHeader title="Add a shop" eyebrow="Catalog" onClose={onClose} />
<div style={{ padding: 32, display: "grid", gap: 16 }}>
<Field label="Shop name">
<Input
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Greenleaf Co-op"
/>
</Field>
<Field label="Location (optional)">
<Input
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="e.g. Capitol Hill"
/>
</Field>
</div>
<ModalFooter>
<div />
<div style={{ display: "flex", gap: 8 }}>
<Btn variant="ghost" onClick={onClose}>Cancel</Btn>
<Btn
variant="primary"
icon="check"
disabled={!name.trim() || create.isPending}
onClick={() => create.mutate()}
>
{create.isPending ? "Saving…" : "Add shop"}
</Btn>
</div>
</ModalFooter>
</div>
</ModalBackdrop>
);
}