feat(admin): unified command center dashboard
- KPI strip uses new <KPIStat> component (4 cards) - New 'What needs attention' feed replaces 4 tabs of cards: * no orders today → suggest campaign * pending stops → link to stops * usage > 85% → warn * starter plan → upgrade prompt - Quick actions + plan usage consolidated into one card - Recent orders uses new <EmptyState> - All 15 sections in a single grid (not 4 tabs), with 'All / Operations / Fulfillment / Management / Tools' filter - 'Press ⌘K to search' hint under section grid - All inline SVGs replaced with lucide-react - All hardcoded colors replaced with new design tokens
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import dynamic from "next/dynamic";
|
||||
import { PageHeader, AdminButton, AdminFilterTabs, AdminBadge } from "@/components/admin/design-system";
|
||||
import { PageHeader, AdminButton, AdminFilterTabs, AdminBadge, KPIStat, EmptyState } from "@/components/admin/design-system";
|
||||
import { ShoppingCart, MapPin, Package, DollarSign, Plus, Send, AlertCircle, Calendar, Inbox, Sparkles, BarChart3, BrainCircuit, Upload, Clock, Droplets, Route, Truck, Receipt, Settings as SettingsIcon, Store, type LucideIcon } from "lucide-react";
|
||||
import type { DashboardStats } from "@/actions/dashboard";
|
||||
|
||||
// Lazy-load the upgrade modal — only needed when starter tier + brandId present
|
||||
@@ -20,69 +21,35 @@ type Section = {
|
||||
addonKey?: string;
|
||||
upgradeText?: string;
|
||||
prominent?: boolean;
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
const sections: Section[] = [
|
||||
{ title: "Orders", href: "/admin/orders", description: "View orders, pickup status, fulfillment, and customer details.", group: "operations", prominent: true },
|
||||
{ title: "Products", href: "/admin/products", description: "Manage products, pricing, shipping type, and availability.", group: "operations", prominent: true },
|
||||
{ title: "Tours & Stops", href: "/admin/stops", description: "Manage routes, pickup locations, dates, and cutoff times.", group: "fulfillment" },
|
||||
{ title: "Driver Pickup", href: "/admin/pickup", description: "Mobile pickup lookup, QR scanning, and completion tools.", group: "fulfillment" },
|
||||
{ title: "Shipping", href: "/admin/shipping", description: "FedEx integration, label creation, and shipment tracking.", group: "fulfillment" },
|
||||
{ title: "Reports", href: "/admin/reports", description: "Sales, route, product, pickup, and customer reports.", group: "management" },
|
||||
{ title: "Tax Dashboard", href: "/admin/taxes", description: "Sales tax collected, breakdown by state, and exportable reports.", group: "management" },
|
||||
{ title: "Settings", href: "/admin/settings", description: "Users, billing, brand, integrations, payments, and shipping.", group: "management" },
|
||||
{ title: "Harvest Reach", href: "/admin/communications", description: "Email campaigns, stop blast, templates, and audience segments.", group: "tools", addonKey: "harvest_reach", upgradeText: "Enable to access email & SMS marketing" },
|
||||
{ title: "Wholesale Portal", href: "/admin/wholesale", description: "Standalone B2B portal with custom pricing, credit limits, and net-30.", group: "tools", addonKey: "wholesale_portal", upgradeText: "Enable to unlock B2B buyer portal" },
|
||||
{ title: "Import Center", href: "/admin/import", description: "AI-powered import for products, orders, contacts, and stops.", group: "tools", addonKey: "ai_tools", upgradeText: "Enable AI import with smart column mapping" },
|
||||
{ title: "AI Intelligence", href: "/admin/settings/ai", description: "Campaign writer, pricing advisor, and report explainer.", group: "tools", addonKey: "ai_tools", upgradeText: "Enable AI tools for marketing and pricing" },
|
||||
{ title: "Time Tracking", href: "/admin/time-tracking", description: "Worker clock-in/out, hours tracking, and overtime management.", group: "tools", addonKey: "time_tracking", upgradeText: "Enable for field worker time tracking" },
|
||||
{ title: "Water Log", href: "/admin/water-log", description: "Irrigation tracking and water usage reporting.", group: "tools", addonKey: "water_log", upgradeText: "Enable for agricultural water tracking" },
|
||||
{ title: "Route Trace", href: "/admin/route-trace", description: "Lot tracking, QR stickers, hauling board, and supply chain traceability.", group: "tools", addonKey: "route_trace", upgradeText: "Enable for field-to-delivery traceability" },
|
||||
{ title: "Orders", href: "/admin/orders", description: "View orders, pickup status, fulfillment, and customer details.", group: "operations", prominent: true, icon: ShoppingCart },
|
||||
{ title: "Products", href: "/admin/products", description: "Manage products, pricing, shipping type, and availability.", group: "operations", prominent: true, icon: Package },
|
||||
{ title: "Tours & Stops", href: "/admin/stops", description: "Manage routes, pickup locations, dates, and cutoff times.", group: "fulfillment", icon: MapPin },
|
||||
{ title: "Driver Pickup", href: "/admin/pickup", description: "Mobile pickup lookup, QR scanning, and completion tools.", group: "fulfillment", icon: Truck },
|
||||
{ title: "Shipping", href: "/admin/shipping", description: "FedEx integration, label creation, and shipment tracking.", group: "fulfillment", icon: Receipt },
|
||||
{ title: "Reports", href: "/admin/reports", description: "Sales, route, product, pickup, and customer reports.", group: "management", icon: BarChart3 },
|
||||
{ title: "Tax Dashboard", href: "/admin/taxes", description: "Sales tax collected, breakdown by state, and exportable reports.", group: "management", icon: Receipt },
|
||||
{ title: "Settings", href: "/admin/settings", description: "Users, billing, brand, integrations, payments, and shipping.", group: "management", icon: SettingsIcon },
|
||||
{ title: "Harvest Reach", href: "/admin/communications", description: "Email campaigns, stop blast, templates, and audience segments.", group: "tools", addonKey: "harvest_reach", upgradeText: "Enable to access email & SMS marketing", icon: Send },
|
||||
{ title: "Wholesale Portal", href: "/admin/wholesale", description: "Standalone B2B portal with custom pricing, credit limits, and net-30.", group: "tools", addonKey: "wholesale_portal", upgradeText: "Enable to unlock B2B buyer portal", icon: Store },
|
||||
{ title: "Import Center", href: "/admin/import", description: "AI-powered import for products, orders, contacts, and stops.", group: "tools", addonKey: "ai_tools", upgradeText: "Enable AI import with smart column mapping", icon: Upload },
|
||||
{ title: "AI Intelligence", href: "/admin/settings/ai", description: "Campaign writer, pricing advisor, and report explainer.", group: "tools", addonKey: "ai_tools", upgradeText: "Enable AI tools for marketing and pricing", icon: BrainCircuit },
|
||||
{ title: "Time Tracking", href: "/admin/time-tracking", description: "Worker clock-in/out, hours tracking, and overtime management.", group: "tools", addonKey: "time_tracking", upgradeText: "Enable for field worker time tracking", icon: Clock },
|
||||
{ title: "Water Log", href: "/admin/water-log", description: "Irrigation tracking and water usage reporting.", group: "tools", addonKey: "water_log", upgradeText: "Enable for agricultural water tracking", icon: Droplets },
|
||||
{ title: "Route Trace", href: "/admin/route-trace", description: "Lot tracking, QR stickers, hauling board, and supply chain traceability.", group: "tools", addonKey: "route_trace", upgradeText: "Enable for field-to-delivery traceability", icon: Route },
|
||||
];
|
||||
|
||||
type Tab = "operations" | "fulfillment" | "management" | "tools";
|
||||
type GroupFilter = "all" | "operations" | "fulfillment" | "management" | "tools";
|
||||
|
||||
const TABS: { id: Tab; label: string; icon: ReactNode }[] = [
|
||||
{
|
||||
id: "operations",
|
||||
label: "Operations",
|
||||
icon: (
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "fulfillment",
|
||||
label: "Fulfillment",
|
||||
icon: (
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M16.5 9.4 7.55 4.24"/>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "management",
|
||||
label: "Management",
|
||||
icon: (
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "tools",
|
||||
label: "Tools",
|
||||
icon: (
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
const GROUP_FILTERS: { id: GroupFilter; label: string }[] = [
|
||||
{ id: "all", label: "All" },
|
||||
{ id: "operations", label: "Operations" },
|
||||
{ id: "fulfillment", label: "Fulfillment" },
|
||||
{ id: "management", label: "Management" },
|
||||
{ id: "tools", label: "Tools" },
|
||||
];
|
||||
|
||||
type Props = {
|
||||
@@ -96,6 +63,15 @@ type Props = {
|
||||
stats: DashboardStats;
|
||||
};
|
||||
|
||||
type AttentionItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
detail: string;
|
||||
href: string;
|
||||
tone: "primary" | "accent" | "warning" | "danger";
|
||||
icon: LucideIcon;
|
||||
};
|
||||
|
||||
export default function DashboardClient({
|
||||
brandId,
|
||||
brandName,
|
||||
@@ -106,11 +82,8 @@ export default function DashboardClient({
|
||||
limits,
|
||||
stats,
|
||||
}: Props) {
|
||||
const [activeTab, setActiveTab] = useState<Tab>("operations");
|
||||
const [isUpgradeOpen, setIsUpgradeOpen] = useState(false);
|
||||
|
||||
// Stats are pre-fetched server-side — no loading state needed.
|
||||
// All values are guaranteed to be present before this component renders.
|
||||
const [groupFilter, setGroupFilter] = useState<GroupFilter>("all");
|
||||
|
||||
const usagePct = {
|
||||
users: limits.max_users > 0 ? (usage.users / limits.max_users) * 100 : 0,
|
||||
@@ -118,8 +91,6 @@ export default function DashboardClient({
|
||||
products: limits.max_products > 0 ? (usage.products / limits.max_products) * 100 : 0,
|
||||
};
|
||||
|
||||
const tabSections = sections.filter((s) => s.group === activeTab);
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
|
||||
};
|
||||
@@ -133,27 +104,77 @@ export default function DashboardClient({
|
||||
return styles[status] || { bg: "var(--admin-bg)", text: "var(--admin-text-secondary)" };
|
||||
};
|
||||
|
||||
// "What needs attention" feed — composes a flat list of items that need action
|
||||
const attentionItems: AttentionItem[] = [];
|
||||
if (stats.todayOrders === 0) {
|
||||
attentionItems.push({
|
||||
id: "no-orders-today",
|
||||
title: "No orders today yet",
|
||||
detail: "Send a campaign or check that your storefront is reachable.",
|
||||
href: "/admin/communications/compose",
|
||||
tone: "accent",
|
||||
icon: Send,
|
||||
});
|
||||
}
|
||||
if (stats.pendingStops > 0) {
|
||||
attentionItems.push({
|
||||
id: "pending-stops",
|
||||
title: `${stats.pendingStops} stop${stats.pendingStops === 1 ? "" : "s"} pending`,
|
||||
detail: "Review upcoming routes and confirm pickup locations.",
|
||||
href: "/admin/stops",
|
||||
tone: "primary",
|
||||
icon: MapPin,
|
||||
});
|
||||
}
|
||||
if (usagePct.products > 85) {
|
||||
attentionItems.push({
|
||||
id: "products-near-limit",
|
||||
title: `${Math.round(usagePct.products)}% of product limit used`,
|
||||
detail: "Consider archiving inactive products or upgrading your plan.",
|
||||
href: "/admin/products",
|
||||
tone: "warning",
|
||||
icon: Package,
|
||||
});
|
||||
} else if (usagePct.users > 85) {
|
||||
attentionItems.push({
|
||||
id: "users-near-limit",
|
||||
title: `${Math.round(usagePct.users)}% of user limit used`,
|
||||
detail: "Add more seats or review who has access.",
|
||||
href: "/admin/users",
|
||||
tone: "warning",
|
||||
icon: AlertCircle,
|
||||
});
|
||||
}
|
||||
if (planTier === "starter" && brandId) {
|
||||
attentionItems.push({
|
||||
id: "upgrade-prompt",
|
||||
title: "Unlock unlimited stops & products",
|
||||
detail: "Upgrade from Starter to Farm for $149/mo.",
|
||||
href: "/admin/settings/billing",
|
||||
tone: "accent",
|
||||
icon: Sparkles,
|
||||
});
|
||||
}
|
||||
|
||||
const quickActions = [
|
||||
{ label: "New Order", href: "/admin/orders?new=true", icon: "plus" },
|
||||
{ label: "Add Stop", href: "/admin/stops/new", icon: "map" },
|
||||
{ label: "Add Product", href: "/admin/products/new", icon: "package" },
|
||||
{ label: "Send Blast", href: "/admin/communications/compose", icon: "mail" },
|
||||
{ label: "New Order", href: "/admin/orders?new=true", icon: Plus },
|
||||
{ label: "Add Stop", href: "/admin/stops/new", icon: MapPin },
|
||||
{ label: "Add Product", href: "/admin/products/new", icon: Package },
|
||||
{ label: "Send Blast", href: "/admin/communications/compose", icon: Send },
|
||||
];
|
||||
|
||||
const visibleSections = sections
|
||||
.filter((s) => !(s.title === "Water Log" && !isWaterLogVisible))
|
||||
.filter((s) => !(s.title === "Route Trace" && !enabledAddons["route_trace"]))
|
||||
.filter((s) => groupFilter === "all" || s.group === groupFilter);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen" style={{ backgroundColor: "var(--admin-bg)" }}>
|
||||
{/* Page Header */}
|
||||
<div className="px-4 sm:px-6 md:px-8 py-5 sm:py-6">
|
||||
<PageHeader
|
||||
icon={
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="3" width="7" height="7" rx="1"/>
|
||||
<rect x="3" y="14" width="7" height="7" rx="1"/>
|
||||
<rect x="14" y="14" width="7" height="7" rx="1"/>
|
||||
</svg>
|
||||
}
|
||||
title="Admin Dashboard"
|
||||
icon={<Inbox className="h-5 w-5" />}
|
||||
title="Dashboard"
|
||||
subtitle={brandName}
|
||||
actions={
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -174,154 +195,157 @@ export default function DashboardClient({
|
||||
{/* Main Content */}
|
||||
<div className="px-4 sm:px-6 md:px-8 pb-8 space-y-5">
|
||||
|
||||
{/* ── Stats Cards Row ────────────────────────────────────── */}
|
||||
{/* ── KPI Strip (top metrics) ──────────────────────────── */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 animate-fade-up" style={{ animationDelay: "0ms" }}>
|
||||
{/* Today's Orders */}
|
||||
<div className="admin-stat-card">
|
||||
<div className="admin-stat-card-inner">
|
||||
<div className="admin-stat-icon" style={{ backgroundColor: "var(--admin-accent-light)" }}>
|
||||
<svg className="w-4 h-4" style={{ color: "var(--admin-accent)" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM12.94 18.55l.276-.276a.75.75 0 011.06 0l.27.27a.75.75 0 010 1.06l-.27.27a.75.75 0 01-1.06 0l-.276-.276a.75.75 0 010-1.06z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="admin-stat-body">
|
||||
<span className="admin-stat-label">Today's Orders</span>
|
||||
<span className="admin-stat-value">{stats.todayOrders}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Today's Revenue */}
|
||||
<div className="admin-stat-card">
|
||||
<div className="admin-stat-card-inner">
|
||||
<div className="admin-stat-icon" style={{ backgroundColor: "var(--admin-accent-soft)" }}>
|
||||
<svg className="w-4 h-4" style={{ color: "var(--admin-accent)" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="admin-stat-body">
|
||||
<span className="admin-stat-label">Today's Revenue</span>
|
||||
<span className="admin-stat-value ha-num">{formatCurrency(stats.todayRevenue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pending Stops */}
|
||||
<div className="admin-stat-card">
|
||||
<div className="admin-stat-card-inner">
|
||||
<div className="admin-stat-icon" style={{ backgroundColor: "var(--admin-bg-subtle)" }}>
|
||||
<svg className="w-4 h-4" style={{ color: "var(--admin-text-secondary)" }} 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>
|
||||
<div className="admin-stat-body">
|
||||
<span className="admin-stat-label">Pending Stops</span>
|
||||
<span className="admin-stat-value">{stats.pendingStops}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active Products */}
|
||||
<div className="admin-stat-card">
|
||||
<div className="admin-stat-card-inner">
|
||||
<div className="admin-stat-icon" style={{ backgroundColor: "var(--admin-primary-soft)" }}>
|
||||
<svg className="w-4 h-4" style={{ color: "var(--admin-primary)" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="admin-stat-body">
|
||||
<span className="admin-stat-label">Active Products</span>
|
||||
<span className="admin-stat-value">{usage.products}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<KPIStat
|
||||
label="Today's Orders"
|
||||
value={stats.todayOrders}
|
||||
icon={<ShoppingCart className="w-4 h-4" />}
|
||||
tone="primary"
|
||||
/>
|
||||
<KPIStat
|
||||
label="Today's Revenue"
|
||||
value={formatCurrency(stats.todayRevenue)}
|
||||
icon={<DollarSign className="w-4 h-4" />}
|
||||
tone="accent"
|
||||
/>
|
||||
<KPIStat
|
||||
label="Pending Stops"
|
||||
value={stats.pendingStops}
|
||||
icon={<MapPin className="w-4 h-4" />}
|
||||
tone="default"
|
||||
/>
|
||||
<KPIStat
|
||||
label="Active Products"
|
||||
value={usage.products}
|
||||
icon={<Package className="w-4 h-4" />}
|
||||
tone="primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Quick Actions + Usage Row ─────────────────────────── */}
|
||||
{/* ── Command center: attention feed + quick actions ───── */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3 animate-fade-up" style={{ animationDelay: "60ms" }}>
|
||||
{/* Quick Actions */}
|
||||
<div className="admin-card-section">
|
||||
<div className="admin-section-header">
|
||||
<span className="admin-section-title">Quick Actions</span>
|
||||
</div>
|
||||
<div className="admin-quick-actions">
|
||||
{quickActions.map((action) => (
|
||||
<Link
|
||||
key={action.href}
|
||||
href={action.href}
|
||||
className="admin-quick-action"
|
||||
>
|
||||
<div className="admin-quick-action-icon" style={{ backgroundColor: "var(--admin-accent-light)" }}>
|
||||
{action.icon === "plus" && (
|
||||
<svg className="w-3.5 h-3.5" style={{ color: "var(--admin-accent)" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
)}
|
||||
{action.icon === "map" && (
|
||||
<svg className="w-3.5 h-3.5" style={{ color: "var(--admin-accent)" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<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>
|
||||
)}
|
||||
{action.icon === "package" && (
|
||||
<svg className="w-3.5 h-3.5" style={{ color: "var(--admin-accent)" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
||||
</svg>
|
||||
)}
|
||||
{action.icon === "mail" && (
|
||||
<svg className="w-3.5 h-3.5" style={{ color: "var(--admin-accent)" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-3-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="admin-quick-action-label">{action.label}</span>
|
||||
<svg className="w-3.5 h-3.5 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Usage Stats */}
|
||||
{/* Attention feed (left, 2 cols) */}
|
||||
<div className="lg:col-span-2 admin-card-section">
|
||||
<div className="admin-section-header">
|
||||
<div className="flex items-center gap-2">
|
||||
<AdminBadge variant={planTier === "enterprise" ? "warning" : planTier === "farm" ? "success" : "default"}>
|
||||
{planTier.charAt(0).toUpperCase() + planTier.slice(1)}
|
||||
</AdminBadge>
|
||||
<span className="text-xs" style={{ color: "var(--admin-text-muted)" }}>{brandId ? brandName : "All Brands"}</span>
|
||||
</div>
|
||||
<a href="/admin/settings/billing" className="text-xs font-medium hover:underline sm:hidden" style={{ color: "var(--admin-accent)" }}>
|
||||
Manage →
|
||||
</a>
|
||||
<span className="admin-section-title">What needs attention</span>
|
||||
<span className="text-xs" style={{ color: "var(--admin-text-muted)" }}>
|
||||
{attentionItems.length} item{attentionItems.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="admin-usage-grid">
|
||||
{[
|
||||
{ label: "Users", value: `${usage.users}/${limits.max_users}`, pct: usagePct.users, icon: "users" },
|
||||
{ label: "Stops", value: `${usage.stops_this_month}/${limits.max_stops_monthly}`, pct: usagePct.stops, icon: "map" },
|
||||
{ label: "Products", value: `${usage.products}/${limits.max_products}`, pct: usagePct.products, icon: "package" },
|
||||
].map(({ label, value, pct }) => (
|
||||
<div key={label} className="admin-usage-item">
|
||||
<div className="admin-usage-header">
|
||||
<span className="admin-usage-label">{label}</span>
|
||||
<span className="admin-usage-value ha-num">{value}</span>
|
||||
</div>
|
||||
<div className="admin-usage-bar">
|
||||
<div
|
||||
className="admin-usage-fill"
|
||||
style={{
|
||||
width: `${Math.min(pct, 100)}%`,
|
||||
backgroundColor: pct > 90 ? "var(--admin-danger)" : pct > 75 ? "var(--admin-warning)" : "var(--admin-accent)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{pct > 85 && (
|
||||
<span className="admin-usage-warning">Near limit</span>
|
||||
)}
|
||||
{attentionItems.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Sparkles className="w-8 h-8" />}
|
||||
title="All clear"
|
||||
description="Nothing needs your attention right now. Enjoy the calm."
|
||||
/>
|
||||
) : (
|
||||
<ul className="divide-y" style={{ borderColor: "var(--admin-border-light)" }}>
|
||||
{attentionItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const toneColor =
|
||||
item.tone === "accent" ? "var(--admin-accent)" :
|
||||
item.tone === "warning" ? "var(--admin-warning)" :
|
||||
item.tone === "danger" ? "var(--admin-danger)" :
|
||||
"var(--admin-primary)";
|
||||
const toneSoft =
|
||||
item.tone === "accent" ? "var(--admin-accent-soft)" :
|
||||
item.tone === "warning" ? "var(--admin-warning-soft)" :
|
||||
item.tone === "danger" ? "var(--admin-danger-soft)" :
|
||||
"var(--admin-primary-soft)";
|
||||
return (
|
||||
<li key={item.id}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="group flex items-start gap-3 px-4 py-3 transition-colors hover:bg-black/[0.02]"
|
||||
>
|
||||
<div
|
||||
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg"
|
||||
style={{ backgroundColor: toneSoft, color: toneColor }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold" style={{ color: "var(--admin-text-primary)" }}>
|
||||
{item.title}
|
||||
</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--admin-text-muted)" }}>
|
||||
{item.detail}
|
||||
</p>
|
||||
</div>
|
||||
<svg className="w-4 h-4 mt-2 flex-shrink-0 opacity-30 group-hover:opacity-100 transition-opacity" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick actions (right, 1 col) */}
|
||||
<div className="admin-card-section">
|
||||
<div className="admin-section-header">
|
||||
<span className="admin-section-title">Quick actions</span>
|
||||
</div>
|
||||
<div className="admin-quick-actions">
|
||||
{quickActions.map((action) => {
|
||||
const Icon = action.icon;
|
||||
return (
|
||||
<Link
|
||||
key={action.href}
|
||||
href={action.href}
|
||||
className="admin-quick-action"
|
||||
>
|
||||
<div className="admin-quick-action-icon" style={{ backgroundColor: "var(--admin-primary-soft)" }}>
|
||||
<Icon className="w-3.5 h-3.5" style={{ color: "var(--admin-primary)" }} />
|
||||
</div>
|
||||
<span className="admin-quick-action-label">{action.label}</span>
|
||||
<svg className="w-3.5 h-3.5 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Plan usage inside quick actions card */}
|
||||
<div className="px-4 py-3 border-t" style={{ borderColor: "var(--admin-border-light)" }}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AdminBadge tone={planTier === "enterprise" ? "info" : planTier === "farm" ? "success" : "neutral"}>
|
||||
{planTier.charAt(0).toUpperCase() + planTier.slice(1)}
|
||||
</AdminBadge>
|
||||
<span className="text-xs" style={{ color: "var(--admin-text-muted)" }}>
|
||||
{brandId ? brandName : "All Brands"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ label: "Users", value: `${usage.users}/${limits.max_users}`, pct: usagePct.users },
|
||||
{ label: "Stops", value: `${usage.stops_this_month}/${limits.max_stops_monthly}`, pct: usagePct.stops },
|
||||
{ label: "Products", value: `${usage.products}/${limits.max_products}`, pct: usagePct.products },
|
||||
].map(({ label, value, pct }) => (
|
||||
<div key={label} className="admin-usage-item">
|
||||
<div className="admin-usage-header">
|
||||
<span className="admin-usage-label">{label}</span>
|
||||
<span className="admin-usage-value ha-num">{value}</span>
|
||||
</div>
|
||||
<div className="admin-usage-bar">
|
||||
<div
|
||||
className="admin-usage-fill"
|
||||
style={{
|
||||
width: `${Math.min(pct, 100)}%`,
|
||||
backgroundColor: pct > 90 ? "var(--admin-danger)" : pct > 75 ? "var(--admin-warning)" : "var(--admin-primary)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,8 +353,8 @@ export default function DashboardClient({
|
||||
{/* ── Recent Orders ─────────────────────────────────────── */}
|
||||
<div className="admin-card-section animate-fade-up" style={{ animationDelay: "120ms" }}>
|
||||
<div className="admin-section-header">
|
||||
<span className="admin-section-title">Recent Orders</span>
|
||||
<Link href="/admin/orders" className="text-xs font-medium hover:underline" style={{ color: "var(--admin-accent)" }}>
|
||||
<span className="admin-section-title">Recent orders</span>
|
||||
<Link href="/admin/orders" className="text-xs font-medium hover:underline" style={{ color: "var(--admin-primary)" }}>
|
||||
View all →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -346,9 +370,7 @@ export default function DashboardClient({
|
||||
>
|
||||
<div className="admin-order-info">
|
||||
<div className="admin-order-icon">
|
||||
<svg className="w-4 h-4 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM12.94 18.55l.276-.276a.75.75 0 011.06 0l.27.27a.75.75 0 010 1.06l-.27.27a.75.75 0 01-1.06 0l-.276-.276a.75.75 0 010-1.06z" />
|
||||
</svg>
|
||||
<ShoppingCart className="w-4 h-4" style={{ color: "var(--admin-text-muted)" }} />
|
||||
</div>
|
||||
<div>
|
||||
<span className="admin-order-name">{order.customer_name}</span>
|
||||
@@ -366,108 +388,86 @@ export default function DashboardClient({
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="admin-orders-empty">
|
||||
<svg className="w-10 h-10 mb-2" style={{ color: "var(--admin-text-muted)" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
<p className="text-sm" style={{ color: "var(--admin-text-muted)" }}>No recent orders</p>
|
||||
<Link href="/admin/orders?new=true" className="text-xs font-medium hover:underline mt-1" style={{ color: "var(--admin-accent)" }}>
|
||||
Create your first order →
|
||||
</Link>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={<Inbox className="w-8 h-8" />}
|
||||
title="No recent orders"
|
||||
description="When customers place orders, they'll show up here."
|
||||
action={{ label: "Create your first order", href: "/admin/orders?new=true" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Section Navigation Tabs ───────────────────────────── */}
|
||||
{/* ── Section filter + grid ────────────────────────────── */}
|
||||
<div className="animate-fade-up" style={{ animationDelay: "180ms" }}>
|
||||
<AdminFilterTabs
|
||||
activeTab={activeTab}
|
||||
onTabChange={(value) => setActiveTab(value as Tab)}
|
||||
tabs={TABS.map((tab) => ({ value: tab.id, label: tab.label, icon: tab.icon }))}
|
||||
size="md"
|
||||
showCounts={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold" style={{ color: "var(--admin-text-primary)" }}>
|
||||
All sections
|
||||
</h2>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--admin-text-muted)" }}>
|
||||
Jump to any part of the admin. Press <kbd className="px-1.5 py-0.5 rounded text-[10px] font-mono" style={{ backgroundColor: "var(--admin-bg-subtle)", border: "1px solid var(--admin-border)" }}>⌘K</kbd> to search.
|
||||
</p>
|
||||
</div>
|
||||
<AdminFilterTabs
|
||||
activeTab={groupFilter}
|
||||
onTabChange={(value) => setGroupFilter(value as GroupFilter)}
|
||||
tabs={GROUP_FILTERS.map((f) => ({ value: f.id, label: f.label }))}
|
||||
size="sm"
|
||||
showCounts={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{visibleSections.map((section) => {
|
||||
const Icon = section.icon;
|
||||
const isAddon = Boolean(section.addonKey);
|
||||
const isEnabled = section.addonKey ? (enabledAddons[section.addonKey] ?? false) : true;
|
||||
const isProminent = section.prominent;
|
||||
|
||||
{/* ── Section Cards Grid ─────────────────────────────────── */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 gap-3 animate-fade-up" style={{ animationDelay: "240ms" }}>
|
||||
{tabSections.map((section) => {
|
||||
if (section.title === "Water Log" && !isWaterLogVisible) return null;
|
||||
if (section.title === "Route Trace" && !enabledAddons["route_trace"]) return null;
|
||||
|
||||
const isAddon = Boolean(section.addonKey);
|
||||
const isEnabled = section.addonKey ? (enabledAddons[section.addonKey] ?? false) : true;
|
||||
const isProminent = section.prominent;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={section.title}
|
||||
href={section.href}
|
||||
className={[
|
||||
"admin-section-card",
|
||||
isAddon && !isEnabled ? "admin-section-card--locked" : "",
|
||||
isProminent ? "admin-section-card--prominent" : "",
|
||||
].filter(Boolean).join(" ")}
|
||||
>
|
||||
<div className="admin-section-card-top">
|
||||
<div className={[
|
||||
"admin-section-card-icon",
|
||||
isAddon && !isEnabled ? "admin-section-card-icon--locked" : "",
|
||||
isProminent ? "admin-section-card-icon--prominent" : "",
|
||||
].filter(Boolean).join(" ")}>
|
||||
{section.title === "Orders" && (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM12.94 18.55l.276-.276a.75.75 0 011.06 0l.27.27a.75.75 0 010 1.06l-.27.27a.75.75 0 01-1.06 0l-.276-.276a.75.75 0 010-1.06z" />
|
||||
</svg>
|
||||
)}
|
||||
{section.title === "Products" && (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
||||
</svg>
|
||||
)}
|
||||
{section.title === "Tours & Stops" && (
|
||||
<svg className="w-4 h-4" 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>
|
||||
)}
|
||||
{section.title === "Driver Pickup" && (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12" />
|
||||
</svg>
|
||||
)}
|
||||
{!["Orders", "Products", "Tours & Stops", "Driver Pickup"].includes(section.title) && (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
)}
|
||||
return (
|
||||
<Link
|
||||
key={section.title}
|
||||
href={section.href}
|
||||
className={[
|
||||
"admin-section-card",
|
||||
isAddon && !isEnabled ? "admin-section-card--locked" : "",
|
||||
isProminent ? "admin-section-card--prominent" : "",
|
||||
].filter(Boolean).join(" ")}
|
||||
>
|
||||
<div className="admin-section-card-top">
|
||||
<div className={[
|
||||
"admin-section-card-icon",
|
||||
isAddon && !isEnabled ? "admin-section-card-icon--locked" : "",
|
||||
isProminent ? "admin-section-card-icon--prominent" : "",
|
||||
].filter(Boolean).join(" ")}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isAddon && !isEnabled && <AdminBadge tone="neutral">Add-on</AdminBadge>}
|
||||
{isAddon && isEnabled && <AdminBadge tone="success">Active</AdminBadge>}
|
||||
{isProminent && <AdminBadge tone="info">Core</AdminBadge>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isAddon && !isEnabled && <AdminBadge variant="warning">Add-on</AdminBadge>}
|
||||
{isAddon && isEnabled && <AdminBadge variant="success">Active</AdminBadge>}
|
||||
{isProminent && <AdminBadge variant="success">Core</AdminBadge>}
|
||||
|
||||
<div className="admin-section-card-body">
|
||||
<h3 className="admin-section-card-title">{section.title}</h3>
|
||||
<p className={["admin-section-card-desc", isAddon && !isEnabled ? "" : ""].filter(Boolean).join(" ")}>
|
||||
{section.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-section-card-body">
|
||||
<h3 className="admin-section-card-title">{section.title}</h3>
|
||||
<p className={["admin-section-card-desc", isAddon && !isEnabled ? "text-stone-400" : ""].filter(Boolean).join(" ")}>
|
||||
{section.description}
|
||||
</p>
|
||||
</div>
|
||||
{isAddon && !isEnabled && section.upgradeText && (
|
||||
<p className="admin-section-card-hint">{section.upgradeText}</p>
|
||||
)}
|
||||
|
||||
{isAddon && !isEnabled && section.upgradeText && (
|
||||
<p className="admin-section-card-hint">{section.upgradeText}</p>
|
||||
)}
|
||||
|
||||
<div className="admin-section-card-arrow">
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<div className="admin-section-card-arrow">
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -482,4 +482,4 @@ export default function DashboardClient({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user