feat(admin): add OrdersFilterButton (status filter via native dialog)

This commit is contained in:
Tyler
2026-06-17 14:45:33 -06:00
parent 1577f6363b
commit 37998aad46
@@ -0,0 +1,114 @@
"use client";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
/**
* Filter button for the Orders list. Opens a native <dialog> as a
* right-edge sheet (matching the MoreSheet pattern) with status options.
* Picking a status pushes a new URL with the `status` search param —
* the server component re-renders with the filter applied.
*
* The status values here are the v2 StatusPill vocabulary
* (placed/ready/picked-up/cancelled) which is what the orders page
* filters on. Empty string = clear filter (all orders).
*/
export function OrdersFilterButton() {
const router = useRouter();
const searchParams = useSearchParams();
const dialogRef = useRef<HTMLDialogElement>(null);
const [open, setOpen] = useState(false);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (open && !dialog.open) dialog.showModal();
if (!open && dialog.open) dialog.close();
}, [open]);
// Sync the dialog's open state when the user closes via Escape
// (native <dialog> behavior — fires a "close" event, not a React event).
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
const onClose = () => setOpen(false);
dialog.addEventListener("close", onClose);
return () => dialog.removeEventListener("close", onClose);
}, []);
const currentStatus = searchParams.get("status") ?? "";
function apply(status: string) {
const params = new URLSearchParams(searchParams.toString());
if (status) params.set("status", status);
else params.delete("status");
router.push(`/admin/v2/orders?${params.toString()}`);
setOpen(false);
}
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
aria-label="Filter orders"
className="rounded-xl flex items-center gap-2"
style={{
minHeight: "56px",
padding: "0 16px",
backgroundColor: "var(--color-surface-2)",
color: "var(--color-text)",
fontWeight: 600,
letterSpacing: "0.02em",
}}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<path d="M3 6h18M6 12h12M10 18h4" strokeLinecap="round" />
</svg>
Filter
</button>
<dialog
ref={dialogRef}
onClick={(e) => { if (e.target === dialogRef.current) setOpen(false); }}
className="w-full max-w-[640px] p-0 m-0 ml-auto h-full max-h-screen backdrop:bg-black/40"
style={{ backgroundColor: "var(--color-bg)", color: "var(--color-text)" }}
>
<div className="flex flex-col h-full overflow-y-auto">
<div
className="sticky top-0 px-6 py-4 border-b flex items-center justify-between"
style={{ backgroundColor: "var(--color-bg)", borderColor: "var(--color-surface-3)" }}
>
<h2 className="text-h1 font-display" style={{ fontWeight: 600 }}>Filter</h2>
<button
type="button"
onClick={() => setOpen(false)}
aria-label="Close"
className="p-2"
>
</button>
</div>
<div className="px-6 py-6 space-y-3">
{["", "placed", "ready", "picked-up", "cancelled"].map((s) => (
<button
key={s || "all"}
type="button"
onClick={() => apply(s)}
className="w-full text-left rounded-xl px-4 py-4"
style={{
minHeight: "56px",
backgroundColor: currentStatus === s ? "var(--color-accent-soft)" : "var(--color-surface-2)",
fontWeight: 600,
}}
>
{s ? s.charAt(0).toUpperCase() + s.slice(1) : "All orders"}
</button>
))}
</div>
</div>
</dialog>
</>
);
}
export default OrdersFilterButton;