Files
ember-market-frontend/components/dashboard/content.tsx
g 7ddcd7afb6
All checks were successful
Build Frontend / build (push) Successful in 1m10s
Update UI styles and dashboard product display
Refined color scheme in AnimatedStatsSection to use indigo instead of pink, and improved gradient backgrounds. In dashboard/content.tsx, updated TopProduct price to support arrays and display revenue per product. UnifiedNotifications received minor style and layout adjustments for better consistency and usability.
2026-01-12 08:03:19 +00:00

204 lines
8.0 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 | 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 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">£{(Number(Array.isArray(product.price) ? product.price[0] : product.price) || 0).toFixed(2)}</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 mb-1">Units Sold</div>
<div className="text-sm font-semibold text-primary">£{product.revenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div>
</div>
</motion.div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
);
}