feat(stops): align with products page design
Deploy to route.crispygoat.com / deploy (push) Successful in 4m4s

- Use StopTableClient in server page component
- Add view mode toggle (table/card) matching products page
- Match stats cards styling to products page
- Add consistent filter bar layout with AdminViewModeTabs
- Add card view for stops with inline delete confirm
- Improve row actions with Edit button + dropdown menu
- Add 'Today' badge and past stop dimming in card view
This commit is contained in:
Tyler
2026-06-10 14:59:42 -06:00
parent b1d4174721
commit 244551ce70
2 changed files with 529 additions and 302 deletions
+23 -77
View File
@@ -2,7 +2,7 @@ import { pool } from "@/lib/db";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
import { PageHeader } from "@/components/admin/design-system";
import StopTableClient from "@/components/admin/StopTableClient";
import { redirect } from "next/navigation";
const StopIcon = () => (
@@ -25,7 +25,6 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
let stops: unknown[] = [];
let error: string | null = null;
let totalCount = 0;
// Resolve active brand from cookie/URL (respects platform_admin "All brands" = null)
let activeBrandId: string | null = null;
@@ -40,11 +39,6 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
? "1=1"
: `brand_id IN ('${(adminUser.brand_ids ?? []).join("','")}')`;
const countResult = await pool.query<{ count: string }>(
`SELECT count(*) as count FROM stops WHERE ${brandCondition}`
);
totalCount = parseInt(countResult.rows[0]?.count ?? "0", 10);
const { rows } = await pool.query(
`SELECT s.id, s.city, s.state, s.date, s."time", s.location, s.status,
s.brand_id, s.address, s.zip, s.cutoff_date,
@@ -56,7 +50,7 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
LIMIT 200`
);
stops = rows;
console.log("[admin/stops] Fetched", rows.length, "stops, total:", totalCount);
console.log("[admin/stops] Fetched", rows.length, "stops");
} catch (e) {
error = e instanceof Error ? e.message : String(e);
console.error("[admin/stops] Query error:", error);
@@ -88,76 +82,28 @@ export default async function AdminStopsPage({ searchParams }: PageProps) {
);
}
// Transform stops for the client component
const stopsForClient = stops.map((s: any) => ({
id: s.id,
city: s.city,
state: s.state,
date: s.date instanceof Date ? s.date.toISOString().split("T")[0] : String(s.date).split("T")[0],
time: s.time || "",
location: s.location,
active: s.status === "active",
brand_id: s.brand_id,
status: s.status,
address: s.address,
zip: s.zip,
cutoff_time: s.cutoff_date,
brands: s.brand_name ? { name: s.brand_name } : null,
}));
return (
<main className="min-h-screen bg-[var(--admin-bg)]">
<div className="min-h-screen bg-[var(--admin-bg)]">
<div className="px-4 sm:px-6 md:px-8 py-6 sm:py-8">
<PageHeader
breadcrumb={[
{ label: "Admin", href: "/admin" },
{ label: "Stops & Routes" }
]}
icon={<StopIcon />}
title="Stops & Routes"
subtitle={`${totalCount} stops total`}
/>
<StopTableClient stops={stopsForClient} />
</div>
<div className="px-4 sm:px-6 md:px-8">
{stops.length === 0 ? (
<div className="rounded-2xl border border-dashed border-stone-300 bg-white p-16 text-center">
<div className="mx-auto mb-4 h-14 w-14 rounded-full bg-stone-100 flex items-center justify-center">
<svg className="h-7 w-7 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
</svg>
</div>
<h3 className="font-display text-2xl font-medium text-stone-950">No stops found</h3>
<p className="mt-2 text-stone-500">
{activeBrandId
? "No stops for the selected brand. Upload a schedule or add stops manually."
: "No stops in the database."}
</p>
<div className="mt-6 flex justify-center gap-3">
<a
href="/admin/stops/new"
className="rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white hover:bg-emerald-500"
>
Add Stop
</a>
</div>
</div>
) : (
<div className="rounded-2xl border border-stone-200 bg-white overflow-hidden">
<table className="w-full text-left text-sm">
<thead className="bg-stone-50 border-b border-stone-200">
<tr>
<th className="px-4 py-3 font-semibold text-stone-600">City</th>
<th className="px-4 py-3 font-semibold text-stone-600">Location</th>
<th className="px-4 py-3 font-semibold text-stone-600">Date</th>
<th className="px-4 py-3 font-semibold text-stone-600">Status</th>
<th className="px-4 py-3 font-semibold text-stone-600">Brand</th>
</tr>
</thead>
<tbody className="divide-y divide-stone-100">
{stops.map((s: any) => (
<tr key={s.id} className="hover:bg-stone-50">
<td className="px-4 py-3 font-medium text-stone-900">{s.city}, {s.state}</td>
<td className="px-4 py-3 text-stone-600">{s.location}</td>
<td className="px-4 py-3 font-mono text-stone-500">{String(s.date)}</td>
<td className="px-4 py-3">
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${
s.status === "active" ? "bg-emerald-100 text-emerald-700" :
s.status === "draft" ? "bg-amber-100 text-amber-700" :
"bg-stone-100 text-stone-500"
}`}>{s.status}</span>
</td>
<td className="px-4 py-3 text-stone-500 text-xs">{s.brand_name ?? "—"}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</main>
</div>
);
}
}