Refactor UI imports and update component paths
Some checks failed
Build Frontend / build (push) Failing after 7s

Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
This commit is contained in:
g
2026-01-13 05:02:13 +00:00
parent a6e6cd0757
commit fe01f31538
173 changed files with 1512 additions and 867 deletions

View File

@@ -2,18 +2,18 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Package, ShoppingBag, Info, ExternalLink, Loader2, AlertTriangle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/common/badge";
import { Button } from "@/components/common/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
} from "@/components/common/tooltip";
import { getCookie } from "@/lib/api";
import axios from "axios";
import { useRouter } from "next/navigation";
import { cacheUtils } from "@/lib/api-client";
import { cacheUtils } from "@/lib/api/api-client";
interface Order {
_id: string;
@@ -299,4 +299,5 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps)
</Tooltip>
</TooltipProvider>
);
}
}

View File

@@ -2,10 +2,10 @@
import React, { useState, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Avatar, AvatarFallback } from "@/components/common/avatar";
import { cn } from "@/lib/utils/general";
import { formatDistanceToNow } from "date-fns";
import axios from "axios";
@@ -15,9 +15,9 @@ import { getCookie, clientFetch } from "@/lib/api";
import { ImageViewerModal } from "@/components/modals/image-viewer-modal";
import Image from "next/image";
import BuyerOrderInfo from "./BuyerOrderInfo";
import { useIsTouchDevice } from "@/hooks/use-mobile";
import { useChromebookScroll, useSmoothScrollToBottom } from "@/hooks/use-chromebook-scroll";
import { useChromebookKeyboard, useChatFocus } from "@/hooks/use-chromebook-keyboard";
import { useIsTouchDevice } from "@/lib/hooks/use-mobile";
import { useChromebookScroll, useSmoothScrollToBottom } from "@/lib/hooks/use-chromebook-scroll";
import { useChromebookKeyboard, useChatFocus } from "@/lib/hooks/use-chromebook-keyboard";
interface Message {
_id: string;
@@ -844,4 +844,5 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
/>
</div>
);
}
}

View File

@@ -10,11 +10,11 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
} from "@/components/common/table";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import { Button } from "@/components/common/button";
import { Avatar, AvatarFallback } from "@/components/common/avatar";
import { formatDistanceToNow } from "date-fns";
import {
Plus,
@@ -41,7 +41,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
} from "@/components/common/select";
import { toast } from "sonner";
import { clientFetch, getCookie } from "@/lib/api";
import { formatDistance } from 'date-fns';
@@ -474,3 +474,5 @@ export default function ChatTable() {
</div>
);
}

View File

@@ -8,11 +8,11 @@ import {
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
} from "@/components/common/card";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Label } from "@/components/common/label";
import { Textarea } from "@/components/common/textarea";
import { ArrowLeft, Send, RefreshCw, Search, User } from "lucide-react";
import axios from "axios";
import { toast } from "sonner";
@@ -376,3 +376,5 @@ export default function NewChatForm() {
</Card>
);
}

View File

@@ -0,0 +1,139 @@
"use client"
import * as React from "react"
import {
Calculator,
Calendar,
CreditCard,
Settings,
Smile,
User,
LayoutDashboard,
Package,
ShoppingCart,
Users,
BarChart3,
RefreshCcw,
RotateCcw,
Search,
MessageSquare,
Truck
} from "lucide-react"
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
} from "@/components/common/command"
import { useRouter } from "next/navigation"
interface CommandPaletteProps {
onResetLayout?: () => void
onToggleWidget?: (id: string) => void
availableWidgets?: Array<{ id: string; title: string; visible: boolean }>
}
export function CommandPalette({ onResetLayout, onToggleWidget, availableWidgets }: CommandPaletteProps) {
const [open, setOpen] = React.useState(false)
const router = useRouter()
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
const runCommand = React.useCallback((command: () => void) => {
setOpen(false)
command()
}, [])
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList className="max-h-[450px]">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Navigation">
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard"))}>
<LayoutDashboard className="mr-2 h-4 w-4" />
<span>Dashboard</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/orders"))}>
<ShoppingCart className="mr-2 h-4 w-4" />
<span>Manage Orders</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/stock"))}>
<Package className="mr-2 h-4 w-4" />
<span>Inventory & Stock</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/customers"))}>
<Users className="mr-2 h-4 w-4" />
<span>Customers</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/analytics"))}>
<BarChart3 className="mr-2 h-4 w-4" />
<span>Advanced Analytics</span>
</CommandItem>
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/shipping"))}>
<Truck className="mr-2 h-4 w-4" />
<span>Shipping Options</span>
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Dashboard Actions">
<CommandItem onSelect={() => runCommand(() => window.location.reload())}>
<RefreshCcw className="mr-2 h-4 w-4" />
<span>Reload Data</span>
</CommandItem>
{onResetLayout && (
<CommandItem onSelect={() => runCommand(onResetLayout)}>
<RotateCcw className="mr-2 h-4 w-4" />
<span>Reset Widget Layout</span>
</CommandItem>
)}
</CommandGroup>
{availableWidgets && availableWidgets.length > 0 && (
<>
<CommandSeparator />
<CommandGroup heading="Toggle Widgets">
{availableWidgets.map((widget) => (
<CommandItem
key={widget.id}
onSelect={() => runCommand(() => onToggleWidget?.(widget.id))}
>
<div className={`mr-2 h-2 w-2 rounded-full ${widget.visible ? "bg-primary" : "bg-muted"}`} />
<span>{widget.visible ? "Hide" : "Show"} {widget.title}</span>
</CommandItem>
))}
</CommandGroup>
</>
)}
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/settings"))}>
<Settings className="mr-2 h-4 w-4" />
<span>General Settings</span>
<CommandShortcut>S</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
)
}

View File

@@ -8,43 +8,47 @@ import { WidgetSettings } from "./widget-settings"
import { WidgetSettingsModal } from "./widget-settings-modal"
import { DashboardEditor } from "./dashboard-editor"
import { DraggableWidget } from "./draggable-widget"
import { CommandPalette } from "./command-palette"
import RevenueWidget from "./revenue-widget"
import LowStockWidget from "./low-stock-widget"
import RecentCustomersWidget from "./recent-customers-widget"
import { ProductPeek } from "./product-peek"
import PendingChatsWidget from "./pending-chats-widget"
import { getGreeting } from "@/lib/utils/general"
import { statsConfig } from "@/config/dashboard"
import { getRandomQuote } from "@/config/quotes"
import type { OrderStatsData } from "@/lib/types"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
import { ShoppingCart, RefreshCcw, ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useToast } from "@/components/ui/use-toast"
import { Skeleton } from "@/components/ui/skeleton"
import { Button } from "@/components/common/button"
import { useToast } from "@/components/common/use-toast"
import { Skeleton } from "@/components/common/skeleton"
import { clientFetch } from "@/lib/api"
import { motion } from "framer-motion"
import Link from "next/link"
import { useWidgetLayout, WidgetConfig } from "@/hooks/useWidgetLayout"
import { useWidgetLayout } from "@/lib/hooks/useWidgetLayout"
import type { TopProduct, WidgetConfig } from "@/lib/types/dashboard"
interface ContentProps {
username: string
orderStats: OrderStatsData
}
interface TopProduct {
id: string;
name: string;
price: number | number[];
image: string;
count: number;
revenue: number;
}
// TopProduct interface moved to @/lib/types/dashboard
export default function Content({ username, orderStats }: ContentProps) {
const [greeting, setGreeting] = useState("");
const [topProducts, setTopProducts] = useState<TopProduct[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [errorQuery, setErrorQuery] = useState(false);
const [selectedProductId, setSelectedProductId] = useState<string | null>(null);
const [isProductPeekOpen, setIsProductPeekOpen] = useState(false);
const [trends, setTrends] = useState<any[]>([]);
const handleProductPeek = (id: string) => {
setSelectedProductId(id);
setIsProductPeekOpen(true);
};
const { toast } = useToast();
const { widgets, toggleWidget, moveWidget, reorderWidgets, resetLayout, isWidgetVisible, updateWidgetSettings } = useWidgetLayout();
const [configuredWidget, setConfiguredWidget] = useState<WidgetConfig | null>(null);
@@ -59,14 +63,25 @@ export default function Content({ username, orderStats }: ContentProps) {
const fetchTopProducts = async () => {
try {
setIsLoading(true);
setLoading(true);
const data = await clientFetch('/orders/top-products');
setTopProducts(data);
} catch (err) {
console.error("Error fetching top products:", err);
setError(err instanceof Error ? err.message : "Failed to fetch top products");
setErrorQuery(true);
} finally {
setIsLoading(false);
setLoading(false);
}
};
const fetchTrends = async () => {
try {
const data = await clientFetch('/analytics/revenue-trends?period=30');
if (Array.isArray(data)) {
setTrends(data);
}
} catch (err) {
console.error("Error fetching trends:", err);
}
};
@@ -88,16 +103,35 @@ export default function Content({ username, orderStats }: ContentProps) {
<section className="space-y-4">
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground ml-1">Overview</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{statsConfig.map((stat, index) => (
<OrderStats
key={stat.title}
title={stat.title}
value={orderStats[stat.key as keyof OrderStatsData]?.toLocaleString() || "0"}
icon={stat.icon}
index={index}
filterStatus={stat.filterStatus}
/>
))}
{statsConfig.map((stat, index) => {
const colors = ["#8884d8", "#10b981", "#3b82f6", "#f59e0b"];
const val = Number(orderStats[stat.key as keyof OrderStatsData]) || 0;
// Map real trend data if available, otherwise fallback to empty (or subtle random if just started)
let trendData = trends.map(t => ({
value: stat.key === "revenue" ? t.revenue : t.orders
})).slice(-12); // Last 12 points
// Fallback for demo/new stores if no trends yet
if (trendData.length === 0) {
trendData = Array.from({ length: 12 }, (_, i) => ({
value: Math.max(0, val * (0.8 + Math.random() * 0.4 + (i / 20)))
}));
}
return (
<OrderStats
key={stat.title}
title={stat.title}
value={orderStats[stat.key as keyof OrderStatsData]?.toLocaleString() || "0"}
icon={stat.icon}
index={index}
filterStatus={stat.filterStatus}
trendData={trendData}
trendColor={colors[index % colors.length]}
/>
);
})}
</div>
</section>
);
@@ -118,7 +152,7 @@ export default function Content({ username, orderStats }: ContentProps) {
<CardTitle>Top Performing Listings</CardTitle>
<CardDescription>Your products with the highest sales volume</CardDescription>
</div>
{error && (
{errorQuery && (
<Button
variant="outline"
size="sm"
@@ -131,7 +165,7 @@ export default function Content({ username, orderStats }: ContentProps) {
)}
</CardHeader>
<CardContent>
{isLoading ? (
{loading ? (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center gap-4">
@@ -144,7 +178,7 @@ export default function Content({ username, orderStats }: ContentProps) {
</div>
))}
</div>
) : error ? (
) : errorQuery ? (
<div className="py-12 text-center">
<div className="text-muted-foreground mb-4">Failed to load product insights</div>
</div>
@@ -164,7 +198,8 @@ export default function Content({ username, orderStats }: ContentProps) {
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + index * 0.05 }}
className="flex items-center gap-4 p-3 rounded-xl hover:bg-muted/50 transition-colors group"
className="flex items-center gap-4 p-3 rounded-xl hover:bg-white/5 transition-colors group cursor-pointer"
onClick={() => handleProductPeek(product.id)}
>
<div
className="h-14 w-14 bg-muted bg-cover bg-center rounded-xl border flex-shrink-0 flex items-center justify-center overflow-hidden group-hover:scale-105 transition-transform"
@@ -179,15 +214,19 @@ export default function Content({ username, orderStats }: ContentProps) {
)}
</div>
<div className="flex-grow min-w-0">
<h4 className="font-semibold text-lg truncate group-hover:text-primary transition-colors">{product.name}</h4>
<div className="flex items-center gap-3 mt-0.5">
<span className="text-sm text-muted-foreground font-medium">£{(Number(Array.isArray(product.price) ? product.price[0] : product.price) || 0).toFixed(2)}</span>
</div>
<h4 className="font-semibold text-lg truncate group-hover:text-primary transition-colors pr-4">{product.name}</h4>
</div>
<div className="text-right">
<div className="text-xl font-bold">{product.count}</div>
<div className="text-xs text-muted-foreground font-medium uppercase tracking-tighter mb-1">Units Sold</div>
<div className="text-sm font-semibold text-primary">£{product.revenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div>
<div className="flex shrink-0 items-center gap-6 pr-4 text-right">
<div className="flex flex-col items-end">
<span className={`text-xl font-bold leading-none ${product.currentStock && product.currentStock <= 5 ? 'text-amber-500' : 'text-muted-foreground'}`}>
{product.currentStock ?? 0}
</span>
<span className="text-[10px] text-muted-foreground font-bold uppercase tracking-widest mt-1">Stock</span>
</div>
<div className="flex flex-col items-end">
<span className="text-2xl font-black text-white leading-none">{product.count}</span>
<span className="text-[10px] text-muted-foreground font-bold uppercase tracking-widest mt-1">Sold</span>
</div>
</div>
</motion.div>
))}
@@ -213,10 +252,21 @@ export default function Content({ username, orderStats }: ContentProps) {
useEffect(() => {
setGreeting(getGreeting());
fetchTopProducts();
fetchTrends();
}, []);
return (
<div className="space-y-10 pb-10">
<div className="space-y-8 pb-10">
<ProductPeek
productId={selectedProductId}
open={isProductPeekOpen}
onOpenChange={setIsProductPeekOpen}
/>
<CommandPalette
onResetLayout={resetLayout}
onToggleWidget={toggleWidget}
availableWidgets={widgets}
/>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
@@ -248,7 +298,7 @@ export default function Content({ username, orderStats }: ContentProps) {
onReorder={reorderWidgets}
onReset={resetLayout}
>
<div className="space-y-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{widgets.map((widget) => {
if (!widget.visible) return null;
@@ -280,3 +330,5 @@ export default function Content({ username, orderStats }: ContentProps) {
);
}

View File

@@ -18,9 +18,9 @@ import {
verticalListSortingStrategy,
arrayMove,
} from "@dnd-kit/sortable"
import { Button } from "@/components/ui/button"
import { Button } from "@/components/common/button"
import { Edit3, X, Check, RotateCcw } from "lucide-react"
import { WidgetConfig } from "@/hooks/useWidgetLayout"
import { WidgetConfig } from "@/lib/types/dashboard"
import { motion, AnimatePresence } from "framer-motion"
interface DashboardEditorProps {
@@ -126,3 +126,5 @@ export function DashboardEditor({
)
}

View File

@@ -4,9 +4,9 @@ import React, { useState, useEffect, useRef } from "react"
import { useSortable } from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { GripVertical, Settings, X, Eye, EyeOff } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Button } from "@/components/common/button"
import { cn } from "@/lib/utils/styles"
import { WidgetConfig } from "@/hooks/useWidgetLayout"
import { WidgetConfig } from "@/lib/types/dashboard"
interface DraggableWidgetProps {
widget: WidgetConfig
@@ -47,6 +47,7 @@ export function DraggableWidget({
style={style}
className={cn(
"relative group",
widget.colSpan === 2 ? "lg:col-span-2" : "lg:col-span-1",
isEditMode && "ring-2 ring-primary ring-offset-2 ring-offset-background rounded-lg",
isDragging && "z-50 shadow-2xl"
)}
@@ -110,3 +111,5 @@ export function DraggableWidget({
</div>
)
}

View File

@@ -1,9 +1,9 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
import { Button } from "@/components/common/button"
import { Skeleton } from "@/components/common/skeleton"
import { AlertCircle, Package, ArrowRight, ShoppingCart } from "lucide-react"
import { clientFetch } from "@/lib/api"
import Image from "next/image"
@@ -165,3 +165,5 @@ export default function LowStockWidget({ settings }: LowStockWidgetProps) {
</Card>
)
}

View File

@@ -0,0 +1,226 @@
"use client"
import { useState, useEffect } from "react"
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription
} from "@/components/common/sheet"
import { clientFetch } from "@/lib/api"
import { Badge } from "@/components/common/badge"
import { Separator } from "@/components/common/separator"
import { ScrollArea } from "@/components/common/scroll-area"
import { Package, Truck, CreditCard, Calendar, User, MapPin, ExternalLink } from "lucide-react"
import { RelativeTime } from "@/components/common/relative-time"
import { formatGBP } from "@/lib/utils/format"
import Link from "next/link"
import { Button } from "@/components/common/button"
import { Skeleton } from "@/components/common/skeleton"
interface OrderPeekProps {
orderId: string | null
open: boolean
onOpenChange: (open: boolean) => void
}
export function OrderPeek({ orderId, open, onOpenChange }: OrderPeekProps) {
const [order, setOrder] = useState<any>(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
if (open && orderId) {
fetchOrderDetails()
} else if (!open) {
// Don't clear order immediately to avoid flicker during closing
// setOrder(null)
}
}, [open, orderId])
const fetchOrderDetails = async () => {
setLoading(true)
try {
const data = await clientFetch(`/orders/${orderId}`)
setOrder(data.order)
} catch (error) {
console.error("Failed to fetch order details for peek:", error)
} finally {
setLoading(false)
}
}
const getStatusStyle = (status: string) => {
switch (status) {
case 'paid': return 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20';
case 'shipped': return 'bg-blue-500/10 text-blue-500 border-blue-500/20';
case 'completed': return 'bg-green-500/10 text-green-500 border-green-500/20';
case 'cancelled': return 'bg-red-500/10 text-red-500 border-red-500/20';
case 'unpaid': return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20';
default: return 'bg-gray-500/10 text-gray-500 border-gray-500/20';
}
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-md border-white/5 bg-[#0a0a0a]/80 backdrop-blur-2xl p-0 overflow-hidden flex flex-col shadow-2xl shadow-black/50">
<SheetHeader className="p-6 border-b border-white/5 bg-white/5">
<div className="flex items-center justify-between mb-2">
<SheetTitle className="text-xl font-bold flex items-center gap-2">
<Package className="h-5 w-5 text-primary" />
Order Details
</SheetTitle>
{order && (
<Link href={`/dashboard/orders/${order._id}`} onClick={() => onOpenChange(false)}>
<Button variant="ghost" size="sm" className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-primary">
Open Full <ExternalLink className="h-3 w-3" />
</Button>
</Link>
)}
</div>
<SheetDescription asChild>
<div className="flex items-center gap-2 text-white font-mono bg-white/5 px-2.5 py-1 rounded-lg border border-white/10 w-fit mt-1">
{loading ? <Skeleton className="h-4 w-32" /> : order ? `Order #${order.orderId}` : "Loading..."}
</div>
</SheetDescription>
</SheetHeader>
<ScrollArea className="flex-1">
{loading ? (
<div className="p-6 space-y-6">
<div className="space-y-3">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-full" />
</div>
<Separator className="bg-white/5" />
<div className="space-y-4">
{[1, 2, 3].map(i => <Skeleton key={i} className="h-12 w-full" />)}
</div>
</div>
) : order ? (
<div className="p-6 space-y-8">
{/* Summary Stats */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Status</span>
<div className="flex pt-1">
<Badge variant="outline" className={`${getStatusStyle(order.status)} border-none px-0 text-3xl font-black capitalize tracking-tight`}>
{order.status}
</Badge>
</div>
</div>
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Total Value</span>
<div className="text-lg font-bold text-primary tabular-nums">
{formatGBP(order.totalPrice)}
</div>
</div>
</div>
{/* Customer Info */}
<div className="space-y-4">
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
<User className="h-4 w-4" /> Customer Information
</h3>
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-3">
<div className="flex justify-between items-start text-sm">
<span className="text-muted-foreground pt-1">Customer</span>
<div className="flex flex-col items-end bg-white/5 p-3 rounded-xl border border-white/5 min-w-[160px]">
<span className="font-black text-white text-base">
{order.telegramUsername ? `@${order.telegramUsername}` : "Anonymous"}
</span>
<span className="text-[10px] text-muted-foreground font-mono uppercase tracking-tighter mt-1 opacity-60">
ID: {order.telegramBuyerId?.slice(0, 16) || "N/A"}
</span>
</div>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-muted-foreground">Order Date</span>
<span className="text-white">
{order.orderDate ? <RelativeTime date={order.orderDate} /> : "N/A"}
</span>
</div>
</div>
</div>
{/* Order Items */}
<div className="space-y-4">
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
<Package className="h-4 w-4" /> Order Items
</h3>
<div className="space-y-2">
{order.products?.map((item: any, idx: number) => (
<div key={idx} className="flex items-center justify-between p-3 rounded-xl bg-white/5 border border-white/5 hover:bg-white/10 transition-colors">
<div className="flex flex-col">
<span className="text-sm font-medium text-white truncate max-w-[200px]">
{item.name || `Item ${idx + 1}`}
</span>
<span className="text-xs text-muted-foreground">Qty: {item.quantity}</span>
</div>
<span className="text-sm font-bold tabular-nums">{formatGBP(item.totalItemPrice)}</span>
</div>
))}
{order.shippingMethod && (
<div className="flex items-center justify-between p-3 rounded-xl bg-primary/5 border border-primary/10">
<div className="flex flex-col">
<span className="text-sm font-medium text-primary">Shipping: {order.shippingMethod.type}</span>
<span className="text-xs text-primary/60 italic">Standard Delivery</span>
</div>
<span className="text-sm font-bold text-primary tabular-nums">{formatGBP(order.shippingMethod.price)}</span>
</div>
)}
</div>
</div>
{/* Shipping Address Peek */}
<div className="space-y-4">
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
<MapPin className="h-4 w-4" /> Delivery Address
</h3>
<div className="p-4 rounded-2xl bg-[#050505] border border-white/10 overflow-hidden shadow-inner">
<p className="text-xs text-muted-foreground leading-relaxed break-all font-mono opacity-70 selection:bg-primary/30">
{order.pgpAddress || "No address provided or encrypted."}
</p>
</div>
</div>
{/* Timeline Indicator */}
<div className="space-y-4">
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
<Calendar className="h-4 w-4" /> Next Milestone
</h3>
<div className="p-4 rounded-2xl bg-amber-500/5 border border-amber-500/20">
<div className="flex items-center gap-3">
<div className="p-2 rounded-full bg-amber-500/20 text-amber-500">
<Truck className="h-4 w-4" />
</div>
<div>
<p className="text-sm font-bold text-amber-500">Pending Shipment</p>
<p className="text-xs text-amber-500/60">Awaiting vendor processing and label creation.</p>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="p-12 text-center text-muted-foreground">
Failed to load order data.
</div>
)}
</ScrollArea>
{order && (
<div className="p-6 border-t border-white/5 bg-white/5 mt-auto">
<Link href={`/dashboard/orders/${order._id}`} className="w-full" onClick={() => onOpenChange(false)}>
<Button className="w-full font-bold h-12 shadow-xl shadow-primary/10">
Full Order Management
</Button>
</Link>
</div>
)}
</SheetContent>
</Sheet>
)
}

View File

@@ -1,7 +1,8 @@
import type { LucideIcon } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card"
import { motion } from "framer-motion"
import Link from "next/link"
import { ResponsiveContainer, LineChart, Line } from "recharts"
interface OrderStatsProps {
title: string
@@ -12,6 +13,10 @@ interface OrderStatsProps {
filterStatus?: string
/** Custom href if not using filterStatus */
href?: string
/** Data for sparkline [ { value: number }, ... ] */
trendData?: { value: number }[]
/** Color for the sparkline */
trendColor?: string
}
export default function OrderStats({
@@ -20,7 +25,9 @@ export default function OrderStats({
icon: Icon,
index = 0,
filterStatus,
href
href,
trendData,
trendColor = "#8884d8"
}: OrderStatsProps) {
const linkHref = href || (filterStatus ? `/dashboard/orders?status=${filterStatus}` : undefined)
@@ -45,8 +52,26 @@ export default function OrderStats({
</div>
</CardHeader>
<CardContent className="relative z-10">
<div className="text-3xl font-bold tracking-tight">{value}</div>
<div className="mt-1 h-1 w-0 bg-primary/20 group-hover:w-full transition-all duration-500 rounded-full" />
<div className="flex items-end justify-between gap-2">
<div className="text-3xl font-bold tracking-tight">{value}</div>
{trendData && trendData.length > 0 && (
<div className="h-10 w-24 mb-1">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={trendData}>
<Line
type="monotone"
dataKey="value"
stroke={trendColor}
strokeWidth={2}
dot={false}
isAnimationActive={true}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
<div className="mt-2 h-1 w-0 bg-primary/20 group-hover:w-full transition-all duration-500 rounded-full" />
{linkHref && (
<div className="mt-2 text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
Click to view
@@ -59,3 +84,4 @@ export default function OrderStats({
)
}

View File

@@ -1,7 +1,7 @@
"use client"
import { Skeleton } from "@/components/ui/skeleton";
import { Card } from "@/components/ui/card";
import { Skeleton } from "@/components/common/skeleton";
import { Card } from "@/components/common/card";
import { Loader2 } from "lucide-react";
import { SnowLoader } from "@/components/snow-loader";
@@ -113,4 +113,4 @@ export default function PageLoading({
</Card>
</div>
);
}
}

View File

@@ -1,14 +1,14 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
import { Button } from "@/components/common/button"
import { Skeleton } from "@/components/common/skeleton"
import { MessageSquare, MessageCircle, ArrowRight, Clock } from "lucide-react"
import { clientFetch, getCookie } from "@/lib/api"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Avatar, AvatarFallback } from "@/components/common/avatar"
import Link from "next/link"
import { RelativeTime } from "@/components/ui/relative-time"
import { RelativeTime } from "@/components/common/relative-time"
interface PendingChatsWidgetProps {
settings?: {
@@ -170,3 +170,5 @@ export default function PendingChatsWidget({ settings }: PendingChatsWidgetProps
</Card>
)
}

View File

@@ -0,0 +1,191 @@
"use client"
import { useState, useEffect } from "react"
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription
} from "@/components/common/sheet"
import { clientFetch } from "@/lib/api"
import { Badge } from "@/components/common/badge"
import { Separator } from "@/components/common/separator"
import { ScrollArea } from "@/components/common/scroll-area"
import { ShoppingCart, TrendingUp, BarChart3, Package, History, ExternalLink, AlertCircle } from "lucide-react"
import { formatGBP } from "@/lib/utils/format"
import Link from "next/link"
import { Button } from "@/components/common/button"
import { Skeleton } from "@/components/common/skeleton"
interface ProductPeekProps {
productId: string | null
open: boolean
onOpenChange: (open: boolean) => void
}
export function ProductPeek({ productId, open, onOpenChange }: ProductPeekProps) {
const [product, setProduct] = useState<any>(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
if (open && productId) {
fetchProductDetails()
}
}, [open, productId])
const fetchProductDetails = async () => {
setLoading(true)
try {
// In this app, productId might be the ID used in the list,
// which corresponds to the mongo _id or a semantic ID.
const data = await clientFetch(`/products/${productId}`)
setProduct(data)
} catch (error) {
console.error("Failed to fetch product details for peek:", error)
} finally {
setLoading(false)
}
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-md border-white/5 bg-[#0a0a0a]/80 backdrop-blur-2xl p-0 overflow-hidden flex flex-col">
<SheetHeader className="p-6 border-b border-white/5 bg-white/5">
<div className="flex items-center justify-between mb-2">
<SheetTitle className="text-xl font-bold flex items-center gap-2">
<ShoppingCart className="h-5 w-5 text-primary" />
Product Insight
</SheetTitle>
{product && (
<Link href={`/dashboard/stock`} onClick={() => onOpenChange(false)}>
<Button variant="ghost" size="sm" className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-primary">
Manage <ExternalLink className="h-3 w-3" />
</Button>
</Link>
)}
</div>
<SheetDescription asChild>
<div className="flex items-center gap-2 text-white font-medium bg-white/5 px-2.5 py-1 rounded-lg border border-white/10 w-fit mt-1">
{loading ? <Skeleton className="h-4 w-32" /> : product ? product.name : "Loading..."}
</div>
</SheetDescription>
</SheetHeader>
<ScrollArea className="flex-1">
{loading ? (
<div className="p-6 space-y-6">
<div className="flex gap-4">
<Skeleton className="h-24 w-24 rounded-2xl" />
<div className="space-y-2 flex-1">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
</div>
</div>
<Separator className="bg-white/5" />
<div className="grid grid-cols-2 gap-4">
<Skeleton className="h-20 w-full rounded-2xl" />
<Skeleton className="h-20 w-full rounded-2xl" />
</div>
</div>
) : product ? (
<div className="p-6 space-y-8">
{/* Product Visual & Basic Info */}
<div className="flex gap-6 items-start">
<div
className="h-24 w-24 bg-muted bg-cover bg-center rounded-2xl border border-white/10 shadow-xl flex-shrink-0"
style={{
backgroundImage: product._id
? `url(/api/products/${product._id}/image)`
: 'none'
}}
/>
<div className="space-y-2">
<Badge variant="outline" className="bg-primary/10 text-primary border-primary/20 capitalize">
{product.type || "Physical"}
</Badge>
<h2 className="text-lg font-bold text-white leading-tight">{product.name}</h2>
</div>
</div>
{/* Inventory Stats */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Current Stock</span>
<div className="flex items-center gap-2">
<span className={`text-xl font-bold ${(product.currentStock || 0) <= 5 ? 'text-rose-500' : 'text-white'}`}>
{product.currentStock || 0}
</span>
{(product.currentStock || 0) <= 5 && <AlertCircle className="h-4 w-4 text-rose-500 animate-pulse" />}
</div>
</div>
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Unit Type</span>
<div className="text-lg font-bold text-white capitalize">
{product.unitType || "Units"}
</div>
</div>
</div>
{/* Analytics Section */}
<div className="space-y-4">
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
<BarChart3 className="h-4 w-4" /> Performance Metrics
</h3>
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-4">
<div className="flex justify-between items-center text-sm">
<span className="text-muted-foreground">Type</span>
<span className="font-medium text-white">{product.type || "Physical"}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-muted-foreground">Rating</span>
<div className="flex items-center gap-1.5">
<div className="flex text-amber-500">
{"★".repeat(Math.round(product.rating || 5))}
</div>
<span className="font-bold text-white">({product.rating || "5.0"})</span>
</div>
</div>
</div>
</div>
{/* Status & Settings */}
<div className="space-y-4">
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
<TrendingUp className="h-4 w-4" /> Market Visibility
</h3>
<div className="p-4 rounded-2xl bg-emerald-500/5 border border-emerald-500/20">
<div className="flex items-center gap-3">
<div className="p-2 rounded-full bg-emerald-500/20 text-emerald-500">
<TrendingUp className="h-4 w-4" />
</div>
<div>
<p className="text-sm font-bold text-emerald-500">Active Listing</p>
<p className="text-xs text-emerald-500/60">This product is currently visible to all buyers.</p>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="grid grid-cols-1 gap-3">
<Link href={`/dashboard/stock`} onClick={() => onOpenChange(false)} className="w-full">
<Button variant="outline" className="w-full justify-start gap-3 h-12 border-white/5 bg-white/5 hover:bg-white/10 group">
<Package className="h-4 w-4 text-primary group-hover:scale-110 transition-transform" />
Update Inventory Levels
</Button>
</Link>
</div>
</div>
) : (
<div className="p-12 text-center text-muted-foreground">
Failed to load product details.
</div>
)}
</ScrollArea>
</SheetContent>
</Sheet>
)
}

View File

@@ -5,7 +5,7 @@ import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Save, X, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Button } from '@/components/common/button';
import {
Form,
FormControl,
@@ -14,12 +14,12 @@ import {
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { toast } from '@/components/ui/use-toast';
} from '@/components/common/form';
import { Input } from '@/components/common/input';
import { RadioGroup, RadioGroupItem } from "@/components/common/radio-group";
import { Switch } from '@/components/common/switch';
import { Textarea } from '@/components/common/textarea';
import { toast } from '@/components/common/use-toast';
import { Promotion, PromotionFormData } from '@/lib/types/promotion';
import { fetchClient } from '@/lib/api';
import dynamic from 'next/dynamic';
@@ -464,4 +464,5 @@ export default function EditPromotionForm({ promotion, onSuccess, onCancel }: Ed
</form>
</Form>
);
}
}

View File

@@ -5,7 +5,7 @@ import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Plus, Save, X, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Button } from '@/components/common/button';
import {
Form,
FormControl,
@@ -14,15 +14,15 @@ import {
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { toast } from '@/components/ui/use-toast';
} from '@/components/common/form';
import { Input } from '@/components/common/input';
import { RadioGroup, RadioGroupItem } from "@/components/common/radio-group";
import { Switch } from '@/components/common/switch';
import { Textarea } from '@/components/common/textarea';
import { toast } from '@/components/common/use-toast';
import { PromotionFormData } from '@/lib/types/promotion';
import { fetchClient } from '@/lib/api';
import { DatePicker } from '@/components/ui/date-picker';
import { DatePicker } from '@/components/common/date-picker';
import dynamic from 'next/dynamic';
const ProductSelector = dynamic(() => import('./ProductSelector'));
@@ -459,4 +459,5 @@ export default function NewPromotionForm({ onSuccess, onCancel }: NewPromotionFo
</form>
</Form>
);
}
}

View File

@@ -2,12 +2,12 @@
import { useState, useEffect, useRef } from 'react';
import { Check, ChevronDown, X, Search } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '@/components/ui/command';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/common/button';
import { Input } from '@/components/common/input';
import { Badge } from '@/components/common/badge';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/common/popover';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '@/components/common/command';
import { ScrollArea } from '@/components/common/scroll-area';
import { Product } from '@/lib/types/promotion';
import { fetchClient } from '@/lib/api';
@@ -248,4 +248,5 @@ export default function ProductSelector({
)}
</div>
);
}
}

View File

@@ -7,11 +7,11 @@ import {
DialogHeader,
DialogTitle,
DialogDescription
} from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { ScrollArea } from '@/components/ui/scroll-area';
} from '@/components/common/dialog';
import { Badge } from '@/components/common/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/common/card';
import { Separator } from '@/components/common/separator';
import { ScrollArea } from '@/components/common/scroll-area';
import {
Calendar,
Target,
@@ -28,7 +28,7 @@ import {
} from 'lucide-react';
import { Promotion, Product } from '@/lib/types/promotion';
import { fetchClient } from '@/lib/api';
import { toast } from '@/components/ui/use-toast';
import { toast } from '@/components/common/use-toast';
interface PromotionDetailsModalProps {
promotion: Promotion | null;
@@ -382,4 +382,5 @@ export default function PromotionDetailsModal({
</DialogContent>
</Dialog>
);
}
}

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react';
import { Plus, Tag, RefreshCw, Trash, Edit, Check, X, Eye } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Button } from '@/components/common/button';
import {
Table,
TableBody,
@@ -11,14 +11,14 @@ import {
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
} from '@/components/common/table';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
} from '@/components/common/card';
import {
Dialog,
DialogContent,
@@ -27,9 +27,9 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { toast } from '@/components/ui/use-toast';
import { Badge } from '@/components/ui/badge';
} from '@/components/common/dialog';
import { toast } from '@/components/common/use-toast';
import { Badge } from '@/components/common/badge';
import { Promotion } from '@/lib/types/promotion';
import { fetchClient } from '@/lib/api';
import dynamic from 'next/dynamic';
@@ -306,4 +306,5 @@ export default function PromotionsList() {
/>
</>
);
}
}

View File

@@ -5,9 +5,9 @@ import {
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Card, CardContent } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
} from '@/components/common/table';
import { Card, CardContent } from '@/components/common/card';
import { Skeleton } from '@/components/common/skeleton';
export default function PromotionsPageSkeleton() {
return (
@@ -59,4 +59,4 @@ export default function PromotionsPageSkeleton() {
</Card>
</div>
);
}
}

View File

@@ -9,10 +9,10 @@ import {
BarChart3,
MessageSquare,
} from "lucide-react"
import { Card, CardContent } from "@/components/ui/card"
import { Card, CardContent } from "@/components/common/card"
import dynamic from "next/dynamic"
import { Product } from "@/models/products"
import { Category } from "@/models/categories"
import { Product } from "@/lib/models/products"
import { Category } from "@/lib/models/categories"
import { clientFetch } from "@/lib/api"
import { toast } from "sonner"
@@ -217,3 +217,5 @@ export default function QuickActions() {
</>
)
}

View File

@@ -3,11 +3,12 @@
import { useState, useEffect } from "react"
import { motion } from "framer-motion"
import { ShoppingBag, CreditCard, Truck, MessageSquare, AlertCircle } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/common/card"
import { clientFetch } from "@/lib/api"
import { Skeleton } from "@/components/ui/skeleton"
import { RelativeTime } from "@/components/ui/relative-time"
import { Skeleton } from "@/components/common/skeleton"
import { RelativeTime } from "@/components/common/relative-time"
import Link from "next/link"
import { OrderPeek } from "./order-peek"
interface ActivityItem {
_id: string;
@@ -21,6 +22,13 @@ interface ActivityItem {
export default function RecentActivity() {
const [activities, setActivities] = useState<ActivityItem[]>([]);
const [loading, setLoading] = useState(true);
const [selectedOrderId, setSelectedOrderId] = useState<string | null>(null);
const [isPeekOpen, setIsPeekOpen] = useState(false);
const handlePeek = (id: string) => {
setSelectedOrderId(id);
setIsPeekOpen(true);
};
useEffect(() => {
async function fetchRecentOrders() {
@@ -89,16 +97,19 @@ export default function RecentActivity() {
className="flex items-start gap-4 relative"
>
{index !== activities.length - 1 && (
<div className="absolute left-[15px] top-8 bottom-[-24px] w-[2px] bg-border/50" />
<div className="absolute left-[17px] top-8 bottom-[-24px] w-[2px] bg-border/50" />
)}
<div className={`mt-1 p-2 rounded-full z-10 ${getStatusColor(item.status)}`}>
{getStatusIcon(item.status)}
</div>
<div className="flex-1 space-y-1">
<div className="flex items-center justify-between">
<Link href={`/dashboard/orders/${item._id}`} className="font-medium hover:underline">
<button
onClick={() => handlePeek(item._id)}
className="font-medium hover:text-primary transition-colors text-left"
>
Order #{item.orderId}
</Link>
</button>
<span className="text-xs text-muted-foreground">
<RelativeTime date={item.orderDate} />
</span>
@@ -106,14 +117,21 @@ export default function RecentActivity() {
<p className="text-sm text-muted-foreground">
{item.status === "paid" ? "Payment received" :
item.status === "shipped" ? "Order marked as shipped" :
`Order status: ${item.status}`} for £{item.totalPrice.toFixed(2)}
`Order status: ${item.status}`}
</p>
</div>
</motion.div>
))}
</div>
)}
<OrderPeek
orderId={selectedOrderId}
open={isPeekOpen}
onOpenChange={setIsPeekOpen}
/>
</CardContent>
</Card>
)
}

View File

@@ -1,12 +1,12 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
import { Button } from "@/components/common/button"
import { Skeleton } from "@/components/common/skeleton"
import { Users, User, ArrowRight, DollarSign } from "lucide-react"
import { getCustomerInsightsWithStore, formatGBP } from "@/lib/api"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Avatar, AvatarFallback } from "@/components/common/avatar"
import Link from "next/link"
interface RecentCustomersWidgetProps {
@@ -152,3 +152,5 @@ export default function RecentCustomersWidget({ settings }: RecentCustomersWidge
</Card>
)
}

View File

@@ -1,13 +1,13 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
import { Button } from "@/components/common/button"
import { Skeleton } from "@/components/common/skeleton"
import { TrendingUp, DollarSign, RefreshCcw } from "lucide-react"
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
import { getRevenueTrendsWithStore, type RevenueData, formatGBP } from "@/lib/api"
import { useToast } from "@/components/ui/use-toast"
import { useToast } from "@/components/common/use-toast"
interface RevenueWidgetProps {
settings?: {
@@ -63,7 +63,6 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
})
// Summary stats
const totalRevenue = data.reduce((sum, item) => sum + (item.revenue || 0), 0)
const totalOrders = data.reduce((sum, item) => sum + (item.orders || 0), 0)
const CustomTooltip = ({ active, payload }: any) => {
@@ -74,9 +73,6 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">{data.formattedDate}</p>
<div className="space-y-1">
<p className="text-sm font-bold text-primary">
Revenue: {formatGBP(data.revenue)}
</p>
<p className="text-xs text-muted-foreground">
Orders: <span className="font-medium text-foreground">{data.orders}</span>
</p>
</div>
@@ -106,7 +102,7 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
<div className="space-y-1">
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5 text-primary" />
Revenue Insights
Order Insights
</CardTitle>
<CardDescription>
Performance over the last {days} days
@@ -122,15 +118,15 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
{error ? (
<div className="h-[300px] flex flex-col items-center justify-center text-center p-6">
<DollarSign className="h-12 w-12 text-muted-foreground/20 mb-4" />
<p className="text-sm text-muted-foreground mb-4">Could not load revenue trends</p>
<p className="text-sm text-muted-foreground mb-4">Could not load order trends</p>
<Button variant="outline" size="sm" onClick={fetchRevenueData}>Retry</Button>
</div>
) : chartData.length === 0 ? (
<div className="h-[300px] flex flex-col items-center justify-center text-center p-6">
<DollarSign className="h-12 w-12 text-muted-foreground/20 mb-4" />
<h3 className="text-lg font-medium">No revenue data</h3>
<h3 className="text-lg font-medium">No order data</h3>
<p className="text-sm text-muted-foreground max-w-xs mt-2">
Start making sales to see your revenue trends here.
Start making sales to see your order trends here.
</p>
</div>
) : (
@@ -139,7 +135,7 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="colorRevenueWidget" x1="0" y1="0" x2="0" y2="1">
<linearGradient id="colorOrderWidget" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
</linearGradient>
@@ -156,15 +152,15 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
axisLine={false}
tickLine={false}
tickFormatter={(value) => `£${value >= 1000 ? (value / 1000).toFixed(1) + 'k' : value}`}
tickFormatter={(value) => `${value}`}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="revenue"
dataKey="orders"
stroke="hsl(var(--primary))"
fillOpacity={1}
fill="url(#colorRevenueWidget)"
fill="url(#colorOrderWidget)"
strokeWidth={2.5}
activeDot={{ r: 6, strokeWidth: 0, fill: "hsl(var(--primary))" }}
/>
@@ -172,14 +168,10 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
</ResponsiveContainer>
</div>
<div className="grid grid-cols-2 gap-4 pb-2">
<div className="p-4 rounded-2xl bg-primary/5 border border-primary/10">
<div className="text-sm text-muted-foreground font-medium mb-1">Total Revenue</div>
<div className="text-2xl font-bold text-primary">{formatGBP(totalRevenue)}</div>
</div>
<div className="p-4 rounded-2xl bg-muted/50 border border-border">
<div className="flex justify-center pb-2">
<div className="p-4 rounded-2xl bg-muted/50 border border-border w-full max-w-sm text-center">
<div className="text-sm text-muted-foreground font-medium mb-1">Total Orders</div>
<div className="text-2xl font-bold">{totalOrders}</div>
<div className="text-3xl font-bold">{totalOrders}</div>
</div>
</div>
</div>
@@ -188,3 +180,5 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
</Card>
)
}

View File

@@ -1,7 +1,7 @@
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Button } from "@/components/common/button"
import {
Dialog,
DialogContent,
@@ -9,20 +9,20 @@ import {
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
import { Switch } from "@/components/ui/switch"
} from "@/components/common/dialog"
import { Label } from "@/components/common/label"
import { Input } from "@/components/common/input"
import { Switch } from "@/components/common/switch"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { WidgetConfig } from "@/hooks/useWidgetLayout"
} from "@/components/common/select"
import { WidgetConfig } from "@/lib/types/dashboard"
import { Settings2 } from "lucide-react"
import { ScrollArea } from "@/components/ui/scroll-area"
import { ScrollArea } from "@/components/common/scroll-area"
interface WidgetSettingsModalProps {
widget: WidgetConfig | null
@@ -269,3 +269,5 @@ export function WidgetSettingsModal({ widget, open, onOpenChange, onSave }: Widg
</Dialog>
)
}

View File

@@ -1,6 +1,6 @@
"use client"
import { Button } from "@/components/ui/button"
import { Button } from "@/components/common/button"
import {
DropdownMenu,
DropdownMenuContent,
@@ -9,9 +9,9 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/components/ui/dropdown-menu"
} from "@/components/common/dropdown-menu"
import { Settings2, ChevronUp, ChevronDown, RotateCcw, Eye, EyeOff, Cog } from "lucide-react"
import { WidgetConfig } from "@/hooks/useWidgetLayout"
import { WidgetConfig } from "@/lib/types/dashboard"
interface WidgetSettingsProps {
widgets: WidgetConfig[]
@@ -99,3 +99,5 @@ export function WidgetSettings({ widgets, onToggle, onMove, onReset, onConfigure
</DropdownMenu>
)
}