feat(admin/stops): editorial redesign + calendar/route view
Reimagine the admin stops surface under a 'Harvest Almanac' editorial direction. Establishes the type system (Fraunces serif display, Geist sans, JetBrains Mono numerics) loaded via next/font, and a compact modal pattern used across the new components. Stop modal (AddStopModal) - Compact 2-col grid: City|State, Date|Time, Address|ZIP|Cutoff - Status replaced with slim segmented control (Draft / Publish Now) - Tighter inputs (36px), single-line footer with Esc hint - Auto-focus on City field, reactive submit label GlassModal - New 'compact' prop: tighter padding, eyebrow text, no accent bar, capped max-height for dense forms Stop product assignment (StopProductAssignment) - Reimagine the <select> dropdown as an editorial card-grid picker - Big Fraunces numeral + small-caps eyebrow showing live count - Click-to-toggle assign/remove; green check for assigned, red X on hover-to-remove intent - Search + filter chips (All / Available / On this stop) with mono counters - '/' keyboard shortcut focuses search - Summary footer with 'Remove all' action Stops page - New StopsViewClient wrapper with shared search/status filter and Calendar/Table view toggle (default = Calendar) - New StopsCalendarClient: month grid almanac with editorial header, color-coded event chips, today highlight, prev/next/Today nav - Event popover with auto-edge-detection positioning, full stop details, Publish/View Route/Edit actions - Day-route drawer: slides in from right with route spine (numbered markers + connecting hairline), 3-card summary (stops/live/brands), per-stop Publish/Edit/Duplicate actions - StopsTableClient refactored to accept hideInternalFilterBar prop so the wrapper can own shared filtering Design tokens - New utility classes in admin-design-system.css: ha-display, ha-eyebrow, ha-field, ha-segment, ha-picker, ha-product-card, ha-calendar, ha-event-popover, ha-drawer, ha-route-stop, etc. - All uses CSS variables (--font-fraunces, --font-geist, --font-jetbrains-mono) so the type system cascades
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import StopTableClient from "@/components/admin/StopTableClient";
|
||||
import StopsCalendarClient from "@/components/admin/StopsCalendarClient";
|
||||
import { AdminSearchInput, AdminFilterTabs } from "@/components/admin/design-system";
|
||||
|
||||
export type StopStatusFilter = "all" | "active" | "inactive" | "draft";
|
||||
export type StopsViewMode = "calendar" | "table";
|
||||
|
||||
export type StopForView = {
|
||||
id: string;
|
||||
city: string;
|
||||
state: string;
|
||||
date: string;
|
||||
time: string;
|
||||
location: string;
|
||||
active: boolean;
|
||||
deleted_at?: string | null;
|
||||
brand_id: string;
|
||||
status?: string;
|
||||
address?: string | null;
|
||||
zip?: string | null;
|
||||
cutoff_time?: string | null;
|
||||
brands: { name: string } | { name: string }[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
stops: StopForView[];
|
||||
};
|
||||
|
||||
export default function StopsViewClient({ stops }: Props) {
|
||||
const [view, setView] = useState<StopsViewMode>("calendar");
|
||||
const [search, setSearch] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<StopStatusFilter>("all");
|
||||
|
||||
// Apply shared filter so both views agree on what's visible
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
return stops.filter((s) => {
|
||||
const matchesSearch =
|
||||
!q ||
|
||||
s.city.toLowerCase().includes(q) ||
|
||||
s.state.toLowerCase().includes(q) ||
|
||||
s.location.toLowerCase().includes(q);
|
||||
const isDraft = s.status === "draft";
|
||||
const matchesStatus =
|
||||
statusFilter === "all" ||
|
||||
(statusFilter === "active" && s.active && !isDraft) ||
|
||||
(statusFilter === "inactive" && !s.active && !isDraft) ||
|
||||
(statusFilter === "draft" && isDraft);
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
}, [stops, search, statusFilter]);
|
||||
|
||||
const counts = useMemo(
|
||||
() => ({
|
||||
all: stops.length,
|
||||
active: stops.filter((s) => s.active && s.status !== "draft").length,
|
||||
inactive: stops.filter((s) => !s.active && s.status !== "draft").length,
|
||||
draft: stops.filter((s) => s.status === "draft").length,
|
||||
}),
|
||||
[stops]
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{ value: "all", label: "All", count: counts.all },
|
||||
{ value: "active", label: "Active", count: counts.active },
|
||||
{ value: "inactive", label: "Inactive", count: counts.inactive },
|
||||
{ value: "draft", label: "Draft", count: counts.draft },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Filter / search / view-toggle bar — shared across both views */}
|
||||
<div className="flex flex-col gap-3 mb-4 sm:flex-row sm:items-center">
|
||||
<AdminFilterTabs
|
||||
activeTab={statusFilter}
|
||||
onTabChange={(v) => setStatusFilter(v as StopStatusFilter)}
|
||||
tabs={tabs}
|
||||
size="sm"
|
||||
showCounts
|
||||
/>
|
||||
<AdminSearchInput
|
||||
placeholder="Search city, state, or venue…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onClear={() => setSearch("")}
|
||||
showClear
|
||||
className="flex-1 min-w-48 max-w-72"
|
||||
/>
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">
|
||||
{filtered.length} {filtered.length === 1 ? "stop" : "stops"}
|
||||
</span>
|
||||
<div className="sm:ml-auto">
|
||||
<div className="ha-viewtoggle" role="tablist" aria-label="Stops view mode">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={view === "calendar"}
|
||||
onClick={() => setView("calendar")}
|
||||
className={`ha-viewtoggle-btn ${view === "calendar" ? "ha-viewtoggle-btn--active" : ""}`}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
||||
<path d="M16 2v4M8 2v4M3 10h18" strokeLinecap="round" />
|
||||
</svg>
|
||||
Calendar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={view === "table"}
|
||||
onClick={() => setView("table")}
|
||||
className={`ha-viewtoggle-btn ${view === "table" ? "ha-viewtoggle-btn--active" : ""}`}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path d="M3 6h18M3 12h18M3 18h18" strokeLinecap="round" />
|
||||
</svg>
|
||||
Table
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{view === "calendar" ? (
|
||||
<StopsCalendarClient stops={filtered} />
|
||||
) : (
|
||||
<StopTableClient stops={filtered} hideInternalFilterBar />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user