"use client"; import { useMemo, useState } from "react"; import Link from "next/link"; import { type LocationGroup, type Stop, formatTime12, getStopStatus, } from "./types"; type Props = { stops: Stop[]; }; function groupStopsByLocation(stops: Stop[]): LocationGroup[] { const map = new Map(); for (const s of stops) { const key = `${(s.city || "Unknown").trim().toUpperCase()}|${(s.state || "").trim().toUpperCase()}`; let group = map.get(key); if (!group) { group = { key, city: s.city || "Unknown", state: s.state || "", venueCount: 0, total: 0, active: 0, draft: 0, inactive: 0, upcoming: 0, nextDate: null, firstDate: null, lastDate: null, sampleVenue: s.location, stops: [], }; map.set(key, group); } group.stops.push(s); group.total += 1; const status = getStopStatus(s); if (status === "active") group.active += 1; else if (status === "draft") group.draft += 1; else group.inactive += 1; if (s.date) { if (!group.firstDate || s.date < group.firstDate) group.firstDate = s.date; if (!group.lastDate || s.date > group.lastDate) group.lastDate = s.date; } } // post-process: count distinct venues, upcoming stops const today = new Date(); today.setHours(0, 0, 0, 0); const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; for (const g of map.values()) { const venues = new Set( g.stops.flatMap((s) => { const trimmed = (s.location || "").trim().toLowerCase(); return trimmed ? [trimmed] : []; }) ); g.venueCount = Math.max(1, venues.size); g.upcoming = g.stops.filter((s) => s.date >= todayStr && getStopStatus(s) !== "inactive").length; const future: string[] = []; for (const s of g.stops) { if (s.date >= todayStr && getStopStatus(s) !== "inactive") { future.push(s.date); } } future.sort(); g.nextDate = future[0] ?? null; g.stops.sort((a, b) => (a.date || "").localeCompare(b.date || "")); } return Array.from(map.values()).sort((a, b) => { // active locations with upcoming stops first, sorted by next date; then by city if (a.nextDate && !b.nextDate) return -1; if (!a.nextDate && b.nextDate) return 1; if (a.nextDate && b.nextDate) return a.nextDate.localeCompare(b.nextDate); return a.city.localeCompare(b.city); }); } function formatDateLabel(ymd: string | null): string { if (!ymd) return "—"; const d = new Date(ymd + "T00:00:00"); return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } function formatRange(a: string | null, b: string | null): string { if (!a) return "—"; if (!b || a === b) return formatDateLabel(a); return `${formatDateLabel(a)} → ${formatDateLabel(b)}`; } export default function StopsLocations({ stops }: Props) { const [expandedKey, setExpandedKey] = useState(null); const [query, setQuery] = useState(""); const [showOnlyActive, setShowOnlyActive] = useState(false); const groups = useMemo(() => groupStopsByLocation(stops), [stops]); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); return groups.filter((g) => { if (q) { const hay = `${g.city} ${g.state} ${g.sampleVenue}`.toLowerCase(); if (!hay.includes(q)) return false; } if (showOnlyActive && g.active === 0) return false; return true; }); }, [groups, query, showOnlyActive]); // Aggregated stats const stats = useMemo(() => { const totalLocations = groups.length; const totalStops = stops.length; const totalActive = groups.reduce((sum, g) => sum + g.active, 0); const totalUpcoming = groups.reduce((sum, g) => sum + g.upcoming, 0); const totalDrafts = groups.reduce((sum, g) => sum + g.draft, 0); const totalCities = new Set(groups.map((g) => g.city.toLowerCase())).size; const totalVenues = groups.reduce((sum, g) => sum + g.venueCount, 0); return { totalLocations, totalStops, totalActive, totalUpcoming, totalDrafts, totalCities, totalVenues }; }, [groups, stops]); if (stops.length === 0) { return (

No locations yet

Create your first stop to see pickup locations here.

); } return (
{/* Stats strip — almanac style */}
{[ { numeral: "I", label: "Locations", value: stats.totalLocations, hint: `${stats.totalVenues} venues` }, { numeral: "II", label: "Cities", value: stats.totalCities, hint: "covered" }, { numeral: "III", label: "Stops", value: stats.totalStops, hint: "all-time" }, { numeral: "IV", label: "Active", value: stats.totalActive, hint: "live" }, { numeral: "V", label: "Upcoming", value: stats.totalUpcoming, hint: "in queue" }, { numeral: "VI", label: "Drafts", value: stats.totalDrafts, hint: "unpublished" }, ].map((s) => (

{s.numeral} · {s.label}

{s.value}

{s.hint}

{s.numeral}
))}
{/* Filter bar */}
setQuery(e.target.value)} placeholder="Search city, state, venue..." className="w-full rounded-lg border border-[var(--admin-border)] bg-white pl-9 pr-3 py-2 text-sm text-[var(--admin-text-primary)] placeholder:text-[var(--admin-text-muted)] focus:border-[var(--admin-accent)] focus:outline-none focus:ring-2 focus:ring-[var(--admin-accent)]/15" />
{filtered.length} of {groups.length} locations
{/* Location grid */} {filtered.length === 0 ? (

No locations match.

Try a different search.

) : (
{filtered.map((g, idx) => { const isOpen = expandedKey === g.key; return (
{/* Decorative route number */}
{String(idx + 1).padStart(2, "0")}
{/* Expanded stop list */} {isOpen && (

All stops · {g.stops.length}

+ Add at this city
    {g.stops.map((s) => { const status = getStopStatus(s); const dot = status === "active" ? "bg-[var(--admin-accent-dot)]" : status === "draft" ? "bg-amber-500" : "bg-stone-400"; return (
  • {s.date ? new Date(s.date + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric" }) : "—"} {formatTime12(s.time)} {s.location} {status}
  • ); })}
)}
); })}
)}
); } function Stat({ label, value, accent }: { label: string; value: number; accent?: boolean }) { return (

{label}

{value}

); }