+ | e.stopPropagation()}>
{}}
+ onClick={onToggleSelect}
+ className="h-4 w-4 rounded border-stone-300 text-emerald-600 focus:ring-emerald-500 cursor-pointer"
+ aria-label={`Select stop in ${stop.city}`}
/>
|
-
-
- {stop.city}, {stop.state}
-
+
+ |
+
+ {date}
+
+ {weekday} {time && `· ${time}`}
+
+
|
- {stop.location} |
-
- {stop.date} |
-
- {stop.time} |
-
-
- {Array.isArray(stop.brands)
- ? stop.brands[0]?.name
- : stop.brands?.name}
+ |
+
+ {stop.city}
+ {stop.state}
+
|
-
+ |
+
+ {stop.location}
+ {stop.address && (
+
+ {stop.address}
+
+ )}
+
+ |
+
+
+
{stop.status === "draft" ? "Draft" : stop.active ? "Active" : "Inactive"}
|
-
-
-
- Edit
-
+ e.stopPropagation()}>
+
{
e.preventDefault();
+ e.stopPropagation();
setOpenMenu(openMenu === stop.id ? null : stop.id);
}}
+ className="opacity-60 group-hover:opacity-100"
>
@@ -408,36 +549,42 @@ function StopRowBase({
<>
{ setOpenMenu(null); setConfirmDelete(null); }}
+ onClick={(e) => {
+ e.stopPropagation();
+ setOpenMenu(null);
+ setConfirmDelete(null);
+ }}
/>
{stop.status === "draft" && (
- handlePublish(stop.id)}
+
+ {publishingId === stop.id ? "Publishing…" : "Publish"}
+
)}
- e.stopPropagation()}
+ className="flex items-center px-4 py-2.5 text-sm text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg-subtle)] transition-colors"
>
Duplicate
-
- { setOpenMenu(null); setConfirmDelete(stop.id); }}
- className="!justify-start !text-[var(--admin-danger)] hover:!bg-[var(--admin-danger)]/10"
+
+
+
>
)}
@@ -446,7 +593,11 @@ function StopRowBase({
<>
{ setConfirmDelete(null); setOpenMenu(null); }}
+ onClick={(e) => {
+ e.stopPropagation();
+ setConfirmDelete(null);
+ setOpenMenu(null);
+ }}
/>
@@ -455,22 +606,29 @@ function StopRowBase({
This will remove the stop. If it has active orders, you must resolve those first.
-
+
{ setConfirmDelete(null); setOpenMenu(null); }}
+ onClick={(e) => {
+ e.stopPropagation();
+ setConfirmDelete(null);
+ setOpenMenu(null);
+ }}
>
Cancel
handleDelete(stop.id)}
+ onClick={(e) => {
+ e.stopPropagation();
+ handleDelete();
+ }}
disabled={deletingId === stop.id}
isLoading={deletingId === stop.id}
>
- {deletingId === stop.id ? "..." : "Delete"}
+ {deletingId === stop.id ? "Deleting…" : "Delete"}
@@ -482,4 +640,4 @@ function StopRowBase({
);
}
-const StopRow = React.memo(StopRowBase);
\ No newline at end of file
+const StopRow = React.memo(StopRowBase);
diff --git a/src/components/admin/StopsHeaderActions.tsx b/src/components/admin/StopsHeaderActions.tsx
deleted file mode 100644
index 4d09d5f..0000000
--- a/src/components/admin/StopsHeaderActions.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import ScheduleImportModal from "@/components/admin/ScheduleImportModal";
-import AddStopModal from "@/components/admin/AddStopModal";
-import { useRouter } from "next/navigation";
-import { AdminButton } from "@/components/admin/design-system";
-
-type Props = {
- brandId: string;
- /** Hide on the Locations tab — actions belong to the Stops tab only. */
- tab?: "stops" | "locations";
-};
-
-export default function StopsHeaderActions({ brandId, tab = "stops" }: Props) {
- const [showImport, setShowImport] = useState(false);
- const [showAdd, setShowAdd] = useState(false);
- const router = useRouter();
-
- if (tab !== "stops") return null;
-
- function handleImportComplete(count: number) {
- router.refresh();
- }
-
- function handleAddSuccess(stopId: string) {
- router.refresh();
- }
-
- return (
- <>
-
- setShowImport(true)}
- icon={
-
- }
- >
- Upload Schedule
-
- setShowAdd(true)}
- icon={
-
- }
- >
- Add Stop
-
-
-
- {showImport && (
- setShowImport(false)}
- onComplete={handleImportComplete}
- />
- )}
-
- setShowAdd(false)}
- brandId={brandId}
- onSuccess={handleAddSuccess}
- />
- >
- );
-}
\ No newline at end of file
diff --git a/src/components/admin/StopsLocationsTabs.tsx b/src/components/admin/StopsLocationsTabs.tsx
new file mode 100644
index 0000000..80974b1
--- /dev/null
+++ b/src/components/admin/StopsLocationsTabs.tsx
@@ -0,0 +1,107 @@
+"use client";
+
+import Link from "next/link";
+
+type Props = {
+ active: "stops" | "locations";
+ stopCount: number;
+ locationCount: number;
+ stopsSublabel?: string; // e.g. "269 · 7 cities"
+ locationsSublabel?: string; // e.g. "41 · 8 cities"
+};
+
+const StopIcon = () => (
+
+);
+
+const PinIcon = () => (
+
+);
+
+export default function StopsLocationsTabs({
+ active,
+ stopCount,
+ locationCount,
+ stopsSublabel,
+ locationsSublabel,
+}: Props) {
+ const tabs = [
+ {
+ value: "stops" as const,
+ label: "Stops",
+ count: stopCount,
+ sublabel: stopsSublabel,
+ icon: ,
+ href: "/admin/stops",
+ },
+ {
+ value: "locations" as const,
+ label: "Locations",
+ count: locationCount,
+ sublabel: locationsSublabel,
+ icon: ,
+ href: "/admin/stops?tab=locations",
+ },
+ ];
+
+ return (
+
+ {tabs.map((t) => {
+ const isActive = t.value === active;
+ return (
+
+
+ {t.icon}
+
+ {t.label}
+
+ {t.count}
+
+ {t.sublabel && (
+
+ · {t.sublabel}
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/admin/TabSwitcher.tsx b/src/components/admin/TabSwitcher.tsx
new file mode 100644
index 0000000..3615429
--- /dev/null
+++ b/src/components/admin/TabSwitcher.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { motion, AnimatePresence } from "framer-motion";
+
+type Props = {
+ tabKey: string;
+ children: React.ReactNode;
+};
+
+/**
+ * Fades the content of a tabbed panel when the active tab changes.
+ * The tab key drives the animation, so switching tabs triggers a quick
+ * fade-in of the new content. No motion when the tab key is stable
+ * (i.e. on first render of a given tab).
+ */
+export function TabSwitcher({ tabKey, children }: Props) {
+ return (
+
+
+ {children}
+
+
+ );
+}
| |