All checks were successful
Build Frontend / build (push) Successful in 1m12s
Refactored dashboard pages for improved layout and visual consistency using Card components, motion animations, and updated color schemes. Added an OrderTimeline component to the order details page to visualize order lifecycle. Improved customer management page with better sorting, searching, and a detailed customer dialog. Updated storefront settings page with a modernized layout and clearer sectioning.
205 lines
7.9 KiB
TypeScript
205 lines
7.9 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import OrderStats from "./order-stats"
|
|
import QuickActions from "./quick-actions"
|
|
import RecentActivity from "./recent-activity"
|
|
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 { 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 { clientFetch } from "@/lib/api"
|
|
import { motion } from "framer-motion"
|
|
import Link from "next/link"
|
|
|
|
interface ContentProps {
|
|
username: string
|
|
orderStats: OrderStatsData
|
|
}
|
|
|
|
interface TopProduct {
|
|
id: string;
|
|
name: string;
|
|
price: number;
|
|
image: string;
|
|
count: number;
|
|
revenue: number;
|
|
}
|
|
|
|
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 { toast } = useToast();
|
|
|
|
const [randomQuote, setRandomQuote] = useState(getRandomQuote());
|
|
|
|
const fetchTopProducts = async () => {
|
|
try {
|
|
setIsLoading(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");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
setGreeting(getGreeting());
|
|
fetchTopProducts();
|
|
}, []);
|
|
|
|
const handleRetry = () => {
|
|
fetchTopProducts();
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-10 pb-10">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="flex flex-col md:flex-row md:items-end md:justify-between gap-4"
|
|
>
|
|
<div>
|
|
<h1 className="text-4xl font-bold tracking-tight text-foreground">
|
|
{greeting}, <span className="text-primary">{username}</span>!
|
|
</h1>
|
|
<p className="text-muted-foreground mt-2 max-w-2xl text-lg">
|
|
"{randomQuote.text}" — <span className="font-medium">{randomQuote.author}</span>
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Quick ActionsSection */}
|
|
<section className="space-y-4">
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground ml-1">Quick Actions</h2>
|
|
<QuickActions />
|
|
</section>
|
|
|
|
{/* Order Statistics */}
|
|
<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}
|
|
/>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
|
|
{/* Recent Activity Section */}
|
|
<div className="xl:col-span-1">
|
|
<RecentActivity />
|
|
</div>
|
|
|
|
{/* Best Selling Products Section */}
|
|
<div className="xl:col-span-2">
|
|
<Card className="h-full border-border/40 bg-background/50 backdrop-blur-sm">
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<div>
|
|
<CardTitle>Top Performing Listings</CardTitle>
|
|
<CardDescription>Your products with the highest sales volume</CardDescription>
|
|
</div>
|
|
{error && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleRetry}
|
|
className="flex items-center gap-1"
|
|
>
|
|
<RefreshCcw className="h-3 w-3" />
|
|
<span>Retry</span>
|
|
</Button>
|
|
)}
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
{isLoading ? (
|
|
<div className="space-y-4">
|
|
{[...Array(5)].map((_, i) => (
|
|
<div key={i} className="flex items-center gap-4">
|
|
<Skeleton className="h-14 w-14 rounded-xl" />
|
|
<div className="space-y-2 flex-1">
|
|
<Skeleton className="h-4 w-1/2" />
|
|
<Skeleton className="h-3 w-1/4" />
|
|
</div>
|
|
<Skeleton className="h-4 w-16" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : error ? (
|
|
<div className="py-12 text-center">
|
|
<div className="text-muted-foreground mb-4">Failed to load product insights</div>
|
|
</div>
|
|
) : topProducts.length === 0 ? (
|
|
<div className="py-12 text-center">
|
|
<ShoppingCart className="h-12 w-12 mx-auto text-muted-foreground/50 mb-4" />
|
|
<h3 className="text-lg font-medium mb-1">Begin your sales journey</h3>
|
|
<p className="text-muted-foreground text-sm max-w-xs mx-auto">
|
|
Your top performing listings will materialize here as you receive orders.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-1">
|
|
{topProducts.map((product, index) => (
|
|
<motion.div
|
|
key={product.id}
|
|
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"
|
|
>
|
|
<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"
|
|
style={{
|
|
backgroundImage: product.image
|
|
? `url(/api/products/${product.id}/image)`
|
|
: 'none'
|
|
}}
|
|
>
|
|
{!product.image && (
|
|
<ShoppingCart className="h-6 w-6 text-muted-foreground/50" />
|
|
)}
|
|
</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">£{product.price.toFixed(2)}</span>
|
|
<span className="h-1 w-1 rounded-full bg-muted-foreground/30" />
|
|
<span className="text-xs text-muted-foreground">ID: {product.id.slice(-6)}</span>
|
|
</div>
|
|
</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">Units Sold</div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|