Files
ember-market-frontend/components/dashboard/content.tsx
g a6e6cd0757
Some checks failed
Build Frontend / build (push) Failing after 7s
Remove widget resizing and edit mode from dashboard
Eliminated the ability to resize dashboard widgets by removing colSpan from WidgetConfig, related UI, and logic. Removed edit mode functionality and the EditDashboardButton, simplifying the dashboard layout and widget management. Updated drag-and-drop strategy to vertical list and incremented the storage key version.
2026-01-12 11:13:25 +00:00

283 lines
11 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 { WidgetSettings } from "./widget-settings"
import { WidgetSettingsModal } from "./widget-settings-modal"
import { DashboardEditor } from "./dashboard-editor"
import { DraggableWidget } from "./draggable-widget"
import RevenueWidget from "./revenue-widget"
import LowStockWidget from "./low-stock-widget"
import RecentCustomersWidget from "./recent-customers-widget"
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 { 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"
import { useWidgetLayout, WidgetConfig } from "@/hooks/useWidgetLayout"
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 { widgets, toggleWidget, moveWidget, reorderWidgets, resetLayout, isWidgetVisible, updateWidgetSettings } = useWidgetLayout();
const [configuredWidget, setConfiguredWidget] = useState<WidgetConfig | null>(null);
// Initialize with a default quote to match server-side rendering, then randomize on client
const [randomQuote, setRandomQuote] = useState({ text: "Loading wisdom...", author: "..." });
useEffect(() => {
// Determine quote on client-side to avoid hydration mismatch
setRandomQuote(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);
}
};
const handleRetry = () => {
fetchTopProducts();
};
const renderWidget = (widget: WidgetConfig) => {
switch (widget.id) {
case "quick-actions":
return (
<section className="space-y-4">
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground ml-1">Quick Actions</h2>
<QuickActions />
</section>
);
case "overview":
return (
<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}
/>
))}
</div>
</section>
);
case "recent-activity":
return (
<section className="space-y-4">
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground ml-1">Recent Activity</h2>
<RecentActivity />
</section>
);
case "top-products":
return (
<section className="space-y-4">
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground ml-1">Top Performing Listings</h2>
<Card className="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>
</section>
);
case "revenue-chart":
return <RevenueWidget settings={widget.settings} />;
case "low-stock":
return <LowStockWidget settings={widget.settings} />;
case "recent-customers":
return <RecentCustomersWidget settings={widget.settings} />;
case "pending-chats":
return <PendingChatsWidget settings={widget.settings} />;
default:
return null;
}
};
useEffect(() => {
setGreeting(getGreeting());
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">
<WidgetSettings
widgets={widgets}
onToggle={toggleWidget}
onMove={moveWidget}
onReset={resetLayout}
onConfigure={(widget) => setConfiguredWidget(widget)}
/>
</div>
</motion.div>
<DashboardEditor
widgets={widgets}
isEditMode={false}
onToggleEditMode={() => { }}
onReorder={reorderWidgets}
onReset={resetLayout}
>
<div className="space-y-10">
{widgets.map((widget) => {
if (!widget.visible) return null;
return (
<DraggableWidget
key={widget.id}
widget={widget}
isEditMode={false}
onConfigure={() => setConfiguredWidget(widget)}
onToggleVisibility={() => toggleWidget(widget.id)}
>
{renderWidget(widget)}
</DraggableWidget>
);
})}
</div>
</DashboardEditor>
{/* Widget Settings Modal */}
<WidgetSettingsModal
widget={configuredWidget}
open={!!configuredWidget}
onOpenChange={(open) => !open && setConfiguredWidget(null)}
onSave={(widgetId, settings) => {
updateWidgetSettings(widgetId, settings);
}}
/>
</div>
);
}