feat(admin): apply design system to Stops pages
- Stops list, new, and detail pages use new PageHeader with Operations eyebrow, Stops & Routes title, and editorial subtitle. - StopsCalendarClient: replace hardcoded amber (#f59e0b) and brown (#92400e) draft markers with --admin-warning / --admin-warning-soft tokens. Calendar structure unchanged (drag/click/edit preserved). - Add/Edit/StopDetail modals: replace hardcoded red rgba with --admin-danger-soft and color-mix derived borders. - StopsLocationsTabs: swap hardcoded emerald-* for --admin-primary / --admin-primary-soft tokens. - StopMessagingForm: align with design system (ha-field, ha-eyebrow, tokens). - StopProductAssignment error surface: tokens instead of red rgba. - StopsHeaderActions already tokenized via AdminButton; no change. TS clean. Calendar functionality unchanged.
This commit is contained in:
+170
-132
@@ -4,9 +4,17 @@ import StopProductAssignment from "@/components/admin/StopProductAssignment";
|
||||
import MessageCustomersSection from "@/components/admin/MessageCustomersSection";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
const StopIcon = () => (
|
||||
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-current" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
type StopDetailPageProps = {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
@@ -68,18 +76,30 @@ export default async function StopDetailPage({ params }: StopDetailPageProps) {
|
||||
|
||||
if (!stopRow) {
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-12" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<h1 className="text-3xl font-bold text-red-600">Stop not found</h1>
|
||||
<pre className="mt-4 rounded-xl bg-white border border-stone-200 p-4 text-sm text-stone-600">
|
||||
{id}
|
||||
</pre>
|
||||
<Link
|
||||
href="/admin/stops"
|
||||
className="mt-4 inline-block text-stone-500 hover:text-stone-700"
|
||||
>
|
||||
← Back to Stops
|
||||
</Link>
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="px-4 sm:px-6 md:px-8 py-6 sm:py-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<p className="ha-eyebrow mb-2">Operations</p>
|
||||
<PageHeader
|
||||
title="Stop not found"
|
||||
subtitle="This stop may have been removed or you don't have access."
|
||||
icon={<StopIcon />}
|
||||
/>
|
||||
<div className="rounded-2xl border border-[var(--admin-border)] bg-white p-5">
|
||||
<pre className="rounded-xl bg-[var(--admin-bg-subtle)] border border-[var(--admin-border-light)] p-4 text-sm text-[var(--admin-text-secondary)] overflow-auto">
|
||||
{id}
|
||||
</pre>
|
||||
<Link
|
||||
href="/admin/stops"
|
||||
className="ha-btn-ghost mt-4 inline-flex"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Stops
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
@@ -148,138 +168,156 @@ export default async function StopDetailPage({ params }: StopDetailPageProps) {
|
||||
},
|
||||
}));
|
||||
|
||||
const detailSubtitle = stop.brands?.name
|
||||
? `${stop.brands.name} · ${stop.location}`
|
||||
: stop.location;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-12" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<Link
|
||||
href="/admin/stops"
|
||||
className="text-sm text-stone-500 hover:text-stone-700"
|
||||
>
|
||||
← Back to Stops
|
||||
</Link>
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
|
||||
<p className="ha-eyebrow mb-2">Operations</p>
|
||||
<PageHeader
|
||||
title={`${stop.city}, ${stop.state}`}
|
||||
subtitle={detailSubtitle}
|
||||
icon={<StopIcon />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-2xl bg-white p-8 border border-stone-200 shadow-lg">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-stone-500">
|
||||
{stop.brands?.name}
|
||||
</p>
|
||||
<h1 className="mt-2 text-4xl font-bold text-stone-950">
|
||||
{stop.city}, {stop.state}
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-stone-600">{stop.location}</p>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`shrink-0 rounded-full px-4 py-2 text-sm font-medium ${
|
||||
stop.active
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: "bg-stone-200 text-stone-500"
|
||||
}`}
|
||||
>
|
||||
{stop.active ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-500">Date</p>
|
||||
<p className="mt-1 text-lg font-semibold text-stone-950">
|
||||
{stop.date}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-500">Time</p>
|
||||
<p className="mt-1 text-lg font-semibold text-stone-950">
|
||||
{stop.time}
|
||||
</p>
|
||||
</div>
|
||||
{stop.address && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-500">Address</p>
|
||||
<p className="mt-1 text-lg font-semibold text-stone-950">
|
||||
{stop.address}
|
||||
{stop.zip ? `, ${stop.zip}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{stop.cutoff_date && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-stone-500">Cutoff</p>
|
||||
<p className="mt-1 text-lg font-semibold text-stone-950">
|
||||
{new Date(stop.cutoff_date + "T00:00:00").toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-end">
|
||||
<a
|
||||
href={`/admin/stops/new?duplicate=${stop.id}`}
|
||||
className="rounded-xl border border-stone-300 bg-white px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50"
|
||||
<div className="px-4 sm:px-6 md:px-8 pb-8 sm:pb-10">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<Link
|
||||
href="/admin/stops"
|
||||
className="ha-btn-ghost mb-4 inline-flex"
|
||||
>
|
||||
Duplicate Stop
|
||||
</a>
|
||||
</div>
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Stops
|
||||
</Link>
|
||||
|
||||
<div className="mt-6 rounded-2xl bg-white p-8 border border-stone-200 shadow-lg">
|
||||
<h2 className="text-2xl font-bold text-stone-950">
|
||||
Assigned Products
|
||||
</h2>
|
||||
<p className="mt-1 text-stone-600">
|
||||
Manage which products are available at this stop.
|
||||
</p>
|
||||
<div className="rounded-2xl bg-white p-8 border border-[var(--admin-border)] shadow-sm">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-[var(--admin-text-muted)]">
|
||||
{stop.brands?.name}
|
||||
</p>
|
||||
<h1 className="mt-2 text-4xl font-bold text-[var(--admin-text-primary)]">
|
||||
{stop.city}, {stop.state}
|
||||
</h1>
|
||||
<p className="mt-2 text-lg text-[var(--admin-text-secondary)]">{stop.location}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<StopProductAssignment
|
||||
stopId={stop.id}
|
||||
allProducts={allProducts}
|
||||
assignedProducts={assignedProducts}
|
||||
callerUid={adminUser.user_id}
|
||||
/>
|
||||
<span
|
||||
className={`shrink-0 rounded-full px-4 py-2 text-sm font-medium ${
|
||||
stop.active
|
||||
? "bg-[var(--admin-success-soft)] text-[var(--admin-success)] border border-[var(--admin-success)]/30"
|
||||
: "bg-[var(--admin-bg-subtle)] text-[var(--admin-text-muted)] border border-[var(--admin-border)]"
|
||||
}`}
|
||||
>
|
||||
{stop.active ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--admin-text-muted)]">Date</p>
|
||||
<p className="mt-1 text-lg font-semibold text-[var(--admin-text-primary)]">
|
||||
{stop.date}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--admin-text-muted)]">Time</p>
|
||||
<p className="mt-1 text-lg font-semibold text-[var(--admin-text-primary)]">
|
||||
{stop.time}
|
||||
</p>
|
||||
</div>
|
||||
{stop.address && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--admin-text-muted)]">Address</p>
|
||||
<p className="mt-1 text-lg font-semibold text-[var(--admin-text-primary)]">
|
||||
{stop.address}
|
||||
{stop.zip ? `, ${stop.zip}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{stop.cutoff_date && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--admin-text-muted)]">Cutoff</p>
|
||||
<p className="mt-1 text-lg font-semibold text-[var(--admin-text-primary)]">
|
||||
{new Date(stop.cutoff_date + "T00:00:00").toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-2xl bg-white p-8 border border-stone-200 shadow-lg">
|
||||
<h2 className="text-2xl font-bold text-stone-950">Edit Stop</h2>
|
||||
<p className="mt-1 text-stone-600">
|
||||
Update stop details, location, and availability.
|
||||
</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<StopEditForm
|
||||
stop={{
|
||||
id: stop.id,
|
||||
city: stop.city,
|
||||
state: stop.state,
|
||||
date: stop.date,
|
||||
time: stop.time,
|
||||
location: stop.location,
|
||||
slug: stop.brands?.slug ?? "",
|
||||
active: stop.active,
|
||||
brand_id: stop.brand_id,
|
||||
address: stop.address,
|
||||
zip: stop.zip,
|
||||
cutoff_time: stop.cutoff_time,
|
||||
}}
|
||||
brands={brandRows ?? []}
|
||||
/>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<a
|
||||
href={`/admin/stops/new?duplicate=${stop.id}`}
|
||||
className="ha-btn-ghost"
|
||||
>
|
||||
Duplicate Stop
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message Customers */}
|
||||
<div className="mt-6 rounded-2xl bg-white p-8 border border-stone-200 shadow-lg">
|
||||
<h2 className="text-2xl font-bold text-stone-950">Message Customers</h2>
|
||||
<p className="mt-1 text-stone-600">
|
||||
Send updates to customers with pending pickups at this stop.
|
||||
</p>
|
||||
<div className="mt-6 rounded-2xl bg-white p-8 border border-[var(--admin-border)] shadow-sm">
|
||||
<h2 className="text-2xl font-bold text-[var(--admin-text-primary)]">
|
||||
Assigned Products
|
||||
</h2>
|
||||
<p className="mt-1 text-[var(--admin-text-secondary)]">
|
||||
Manage which products are available at this stop.
|
||||
</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<MessageCustomersSection stopId={stop.id} brandId={stop.brand_id} />
|
||||
<div className="mt-6">
|
||||
<StopProductAssignment
|
||||
stopId={stop.id}
|
||||
allProducts={allProducts}
|
||||
assignedProducts={assignedProducts}
|
||||
callerUid={adminUser.user_id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-2xl bg-white p-8 border border-[var(--admin-border)] shadow-sm">
|
||||
<h2 className="text-2xl font-bold text-[var(--admin-text-primary)]">Edit Stop</h2>
|
||||
<p className="mt-1 text-[var(--admin-text-secondary)]">
|
||||
Update stop details, location, and availability.
|
||||
</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<StopEditForm
|
||||
stop={{
|
||||
id: stop.id,
|
||||
city: stop.city,
|
||||
state: stop.state,
|
||||
date: stop.date,
|
||||
time: stop.time,
|
||||
location: stop.location,
|
||||
slug: stop.brands?.slug ?? "",
|
||||
active: stop.active,
|
||||
brand_id: stop.brand_id,
|
||||
address: stop.address,
|
||||
zip: stop.zip,
|
||||
cutoff_time: stop.cutoff_time,
|
||||
}}
|
||||
brands={brandRows ?? []}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message Customers */}
|
||||
<div className="mt-6 rounded-2xl bg-white p-8 border border-[var(--admin-border)] shadow-sm">
|
||||
<h2 className="text-2xl font-bold text-[var(--admin-text-primary)]">Message Customers</h2>
|
||||
<p className="mt-1 text-[var(--admin-text-secondary)]">
|
||||
Send updates to customers with pending pickups at this stop.
|
||||
</p>
|
||||
|
||||
<div className="mt-6">
|
||||
<MessageCustomersSection stopId={stop.id} brandId={stop.brand_id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,17 @@ import NewStopForm from "@/components/admin/NewStopForm";
|
||||
import StopProductAssignment from "@/components/admin/StopProductAssignment";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
const StopIcon = () => (
|
||||
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-current" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/>
|
||||
<circle cx="12" cy="10" r="3"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
type Stop = {
|
||||
city: string;
|
||||
state: string;
|
||||
@@ -62,32 +70,39 @@ export default async function NewStopPage({
|
||||
[brandId]
|
||||
);
|
||||
|
||||
const pageTitle = duplicateFrom ? "Duplicate Stop" : "Create Stop";
|
||||
const pageSubtitle = duplicateFrom
|
||||
? `Pre-filled from ${duplicateFrom.city}, ${duplicateFrom.state}. Edit and save to create.`
|
||||
: "Add a new tour stop for Tuxedo Corn or Indian River Direct.";
|
||||
|
||||
return (
|
||||
<main className="min-h-screen px-6 py-12" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="mb-8">
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
|
||||
<p className="ha-eyebrow mb-2">Operations</p>
|
||||
<PageHeader
|
||||
title={pageTitle}
|
||||
subtitle={pageSubtitle}
|
||||
icon={<StopIcon />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:px-6 md:px-8 pb-8 sm:pb-10">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<Link
|
||||
href="/admin/stops"
|
||||
className="text-sm text-stone-500 hover:text-stone-700"
|
||||
className="ha-btn-ghost mb-4 inline-flex"
|
||||
>
|
||||
← Back to Stops
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Back to Stops
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl bg-white p-8 border border-stone-200 shadow-lg">
|
||||
<h1 className="text-3xl font-bold text-stone-950">
|
||||
{duplicateFrom ? "Duplicate Stop" : "Create Stop"}
|
||||
</h1>
|
||||
|
||||
<p className="mt-2 text-stone-600">
|
||||
{duplicateFrom
|
||||
? `Pre-filled from ${duplicateFrom.city}, ${duplicateFrom.state}. Edit and save to create.`
|
||||
: "Add a new tour stop for Tuxedo Corn or Indian River Direct."}
|
||||
</p>
|
||||
|
||||
<NewStopForm duplicateFrom={duplicateFrom} />
|
||||
<div className="rounded-2xl bg-white p-8 border border-[var(--admin-border)] shadow-sm">
|
||||
<NewStopForm duplicateFrom={duplicateFrom} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { getActiveBrandId } from "@/lib/brand-scope";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
import StopTableClient from "@/components/admin/StopTableClient";
|
||||
import { PageHeader } from "@/components/admin/design-system";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const StopIcon = () => (
|
||||
@@ -75,20 +76,23 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="px-4 sm:px-6 md:px-8 py-6 sm:py-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<nav className="flex items-center gap-2 text-xs sm:text-sm mb-6">
|
||||
<a href="/admin" className="text-[var(--admin-text-muted)] hover:text-[var(--admin-text-secondary)]">Admin</a>
|
||||
<span className="text-[var(--admin-text-muted)]">/</span>
|
||||
<span className="text-[var(--admin-text-primary)] font-medium">Stops & Routes</span>
|
||||
</nav>
|
||||
<h1 className="text-2xl sm:text-3xl font-black text-red-600 tracking-tight">Error loading stops</h1>
|
||||
<pre className="mt-4 rounded-xl bg-white border border-[var(--admin-border)] p-4 text-sm text-[var(--admin-text-secondary)] overflow-auto">
|
||||
{error}
|
||||
</pre>
|
||||
<div className="mt-4 p-4 rounded-xl bg-stone-100 text-sm">
|
||||
<p className="font-semibold">Debug info:</p>
|
||||
<p>adminUser.brand_id: {adminUser.brand_id ?? "null"}</p>
|
||||
<p>adminUser.role: {adminUser.role}</p>
|
||||
<p>adminUser.email: {adminUser.email}</p>
|
||||
<p className="ha-eyebrow mb-2">Operations</p>
|
||||
<PageHeader
|
||||
title="Stops & Routes"
|
||||
subtitle="Schedule and manage pickup locations and dates."
|
||||
icon={<StopIcon />}
|
||||
/>
|
||||
<div className="rounded-2xl border border-[var(--admin-danger)]/30 bg-[var(--admin-danger-soft)] p-5">
|
||||
<h2 className="text-lg font-semibold text-[var(--admin-danger)]">Error loading stops</h2>
|
||||
<pre className="mt-3 rounded-xl bg-white border border-[var(--admin-border)] p-4 text-sm text-[var(--admin-text-secondary)] overflow-auto">
|
||||
{error}
|
||||
</pre>
|
||||
<div className="mt-4 p-4 rounded-xl bg-[var(--admin-bg-subtle)] text-sm text-[var(--admin-text-secondary)]">
|
||||
<p className="font-semibold text-[var(--admin-text-primary)]">Debug info:</p>
|
||||
<p>adminUser.brand_id: {adminUser.brand_id ?? "null"}</p>
|
||||
<p>adminUser.role: {adminUser.role}</p>
|
||||
<p>adminUser.email: {adminUser.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +119,15 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="px-4 sm:px-6 md:px-8 py-6 sm:py-8">
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6">
|
||||
<p className="ha-eyebrow mb-2">Operations</p>
|
||||
<PageHeader
|
||||
title="Stops & Routes"
|
||||
subtitle="Schedule and manage pickup locations and dates."
|
||||
icon={<StopIcon />}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 md:px-8 pb-6 sm:pb-8">
|
||||
<StopTableClient stops={stopsForClient} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user