Files
ember-market-frontend/components/dashboard/content.tsx
g fe01f31538
Some checks failed
Build Frontend / build (push) Failing after 7s
Refactor UI imports and update component paths
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.
2026-01-13 05:02:13 +00:00

335 lines
13 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 { 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/common/card"
import { ShoppingCart, RefreshCcw, ArrowRight } from "lucide-react"
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 } from "@/lib/hooks/useWidgetLayout"
import type { TopProduct, WidgetConfig } from "@/lib/types/dashboard"
interface ContentProps {
username: string
orderStats: OrderStatsData
}
// TopProduct interface moved to @/lib/types/dashboard
export default function Content({ username, orderStats }: ContentProps) {
const [greeting, setGreeting] = useState("");
const [topProducts, setTopProducts] = useState<TopProduct[]>([]);
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);
// 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 {
setLoading(true);
const data = await clientFetch('/orders/top-products');
setTopProducts(data);
} catch (err) {
console.error("Error fetching top products:", err);
setErrorQuery(true);
} finally {
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);
}
};
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) => {
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>
);
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>
{errorQuery && (
<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>
{loading ? (
<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>
) : errorQuery ? (
<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-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"
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 pr-4">{product.name}</h4>
</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>
))}
</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();
fetchTrends();
}, []);
return (
<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 }}
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="grid grid-cols-1 lg:grid-cols-2 gap-8">
{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>
);
}