Update admin review and product flows
This commit is contained in:
@ -1,52 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLanguage } from "@/lib/i18n-context";
|
||||
|
||||
const recentOrders = [
|
||||
{
|
||||
id: "ORD-001",
|
||||
product: "Titan Minimalist V2",
|
||||
sku: "TM-9920",
|
||||
customer: "Sarah Jenkins",
|
||||
location: "London, UK",
|
||||
date: "Oct 24, 2023",
|
||||
amount: "$249.00",
|
||||
status: "Processing",
|
||||
statusColor: "text-primary bg-primary/10",
|
||||
},
|
||||
{
|
||||
id: "ORD-002",
|
||||
product: "Sonic Wave Pro",
|
||||
sku: "SW-1021",
|
||||
customer: "Marcus Thorne",
|
||||
location: "Berlin, DE",
|
||||
date: "Oct 23, 2023",
|
||||
amount: "$499.00",
|
||||
status: "Shipped",
|
||||
statusColor: "text-secondary bg-secondary/10",
|
||||
},
|
||||
{
|
||||
id: "ORD-003",
|
||||
product: "Pulse Runner X",
|
||||
sku: "PR-8821",
|
||||
customer: "Elena Rodriguez",
|
||||
location: "Madrid, ES",
|
||||
date: "Oct 23, 2023",
|
||||
amount: "$125.00",
|
||||
status: "Delivered",
|
||||
statusColor: "text-tertiary bg-tertiary/10",
|
||||
},
|
||||
];
|
||||
interface AnalyticsPoint {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const barHeights = [40, 65, 50, 85, 70, 60, 95, 45, 55, 80];
|
||||
interface DashboardOrder {
|
||||
id: string;
|
||||
product: string;
|
||||
sku: string;
|
||||
customer: string;
|
||||
location: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface SellerDashboardPayload {
|
||||
metrics: {
|
||||
totalProducts: number;
|
||||
soldProducts: number;
|
||||
refundProducts: number;
|
||||
internationalProducts: number;
|
||||
localProducts: number;
|
||||
};
|
||||
analytics: AnalyticsPoint[];
|
||||
recentOrders: DashboardOrder[];
|
||||
}
|
||||
|
||||
function getToken() {
|
||||
if (typeof window === "undefined") return "";
|
||||
return sessionStorage.getItem("token") || localStorage.getItem("token") || "";
|
||||
}
|
||||
|
||||
function formatCurrency(value: number) {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value || 0);
|
||||
}
|
||||
|
||||
function statusColor(status: string) {
|
||||
const normalized = status.toLowerCase();
|
||||
if (normalized.includes("ship")) return "text-secondary bg-secondary/10";
|
||||
if (normalized.includes("deliver")) return "text-tertiary bg-tertiary/10";
|
||||
if (normalized.includes("cancel") || normalized.includes("reject")) return "text-error bg-error/10";
|
||||
return "text-primary bg-primary/10";
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { t } = useLanguage();
|
||||
const d = t.dashboard.overview;
|
||||
const [data, setData] = useState<SellerDashboardPayload | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const res = await fetch("/api/dashboard/seller", {
|
||||
headers: { "x-auth-token": getToken() },
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(result?.responseDesc || result?.error || "Failed to load dashboard");
|
||||
}
|
||||
setData(result);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to load dashboard");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const analytics = data?.analytics || [];
|
||||
const maxAnalytics = Math.max(...analytics.map((item) => item.value), 1);
|
||||
const totalRevenue = (data?.recentOrders || []).reduce((sum, item) => sum + item.amount, 0);
|
||||
const totalOrders = data?.recentOrders.length || 0;
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
{/* Hero Title */}
|
||||
<div className="mb-10">
|
||||
<h1 className="text-5xl font-black font-headline tracking-tighter text-on-surface mb-2">
|
||||
{d.title}
|
||||
@ -54,88 +96,93 @@ export default function DashboardPage() {
|
||||
<p className="text-on-surface-variant font-medium">{d.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-10">
|
||||
{/* Total Products */}
|
||||
<div className="bg-surface-container-lowest p-8 rounded-xl magazine-shadow relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-primary"></div>
|
||||
<p className="text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">
|
||||
{d.totalProducts}
|
||||
</p>
|
||||
<h3 className="text-5xl font-black font-headline text-primary">1,284</h3>
|
||||
<h3 className="text-5xl font-black font-headline text-primary">
|
||||
{loading ? "—" : (data?.metrics.totalProducts ?? 0).toLocaleString("en-US")}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-4 text-secondary font-bold text-sm">
|
||||
<span className="material-symbols-outlined text-sm">trending_up</span>
|
||||
<span>+12.5% {d.vsLastMonth}</span>
|
||||
<span>{loading ? "Loading..." : `${data?.metrics.internationalProducts ?? 0} international`}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total Buyers */}
|
||||
<div className="bg-surface-container-lowest p-8 rounded-xl magazine-shadow relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-secondary"></div>
|
||||
<p className="text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">
|
||||
{d.totalBuyers}
|
||||
</p>
|
||||
<h3 className="text-5xl font-black font-headline text-secondary">42,502</h3>
|
||||
<h3 className="text-5xl font-black font-headline text-secondary">
|
||||
{loading ? "—" : (data?.metrics.soldProducts ?? 0).toLocaleString("en-US")}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-4 text-tertiary font-bold text-sm">
|
||||
<span className="material-symbols-outlined text-sm">group</span>
|
||||
<span>{d.globalReach}</span>
|
||||
<span>{loading ? "Loading..." : `${data?.metrics.localProducts ?? 0} local`}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Refunds */}
|
||||
<div className="bg-surface-container-lowest p-8 rounded-xl magazine-shadow relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-tertiary"></div>
|
||||
<p className="text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">
|
||||
{d.refunds}
|
||||
</p>
|
||||
<h3 className="text-5xl font-black font-headline text-tertiary">142</h3>
|
||||
<h3 className="text-5xl font-black font-headline text-tertiary">
|
||||
{loading ? "—" : (data?.metrics.refundProducts ?? 0).toLocaleString("en-US")}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-4 text-error font-bold text-sm">
|
||||
<span className="material-symbols-outlined text-sm">history</span>
|
||||
<span>0.3% {d.returnRate}</span>
|
||||
<span>{loading ? "Loading..." : `${totalOrders} recent orders`}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Analytics Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-10">
|
||||
{/* Orders Analytics Chart */}
|
||||
<div className="lg:col-span-2 bg-surface-container-lowest p-8 rounded-xl magazine-shadow">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h4 className="text-xl font-black font-headline tracking-tight">{d.ordersAnalytics}</h4>
|
||||
<p className="text-sm text-on-surface-variant font-medium">{d.ordersSubtitle}</p>
|
||||
</div>
|
||||
<select className="bg-surface-container-low border border-surface-container-high rounded-xl text-sm font-bold text-on-surface px-3 py-2 outline-none focus:border-primary">
|
||||
<option>{d.last30Days}</option>
|
||||
<option>{d.lastQuarter}</option>
|
||||
</select>
|
||||
<div className="bg-surface-container-low border border-surface-container-high rounded-xl text-sm font-bold text-on-surface px-3 py-2">
|
||||
{d.last30Days}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bar Chart */}
|
||||
<div className="h-64 flex items-end justify-between gap-2 px-2">
|
||||
{barHeights.map((height, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-full rounded-t-lg transition-all ${
|
||||
i === 4 ? "bg-primary" : "bg-surface-container hover:bg-primary/20"
|
||||
}`}
|
||||
style={{ height: `${height}%` }}
|
||||
/>
|
||||
))}
|
||||
{loading ? (
|
||||
<div className="w-full flex items-center justify-center text-on-surface-variant text-sm font-semibold">
|
||||
Loading analytics...
|
||||
</div>
|
||||
) : analytics.length === 0 ? (
|
||||
<div className="w-full flex items-center justify-center text-on-surface-variant text-sm font-semibold">
|
||||
No analytics available
|
||||
</div>
|
||||
) : (
|
||||
analytics.map((item, i) => (
|
||||
<div
|
||||
key={`${item.label}-${i}`}
|
||||
className={`w-full rounded-t-lg transition-all ${
|
||||
i === analytics.length - 1 ? "bg-primary" : "bg-surface-container hover:bg-primary/20"
|
||||
}`}
|
||||
style={{ height: `${Math.max((item.value / maxAnalytics) * 100, 8)}%` }}
|
||||
title={`${item.label}: ${item.value}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between mt-4 px-2 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">
|
||||
<span>{d.wk} 1</span>
|
||||
<span>{d.wk} 2</span>
|
||||
<span>{d.wk} 3</span>
|
||||
<span>{d.wk} 4</span>
|
||||
{(analytics.length ? analytics : [{ label: `${d.wk} 1`, value: 0 }]).slice(0, 4).map((item) => (
|
||||
<span key={item.label}>{item.label}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Earnings */}
|
||||
<div className="bg-surface-container-lowest p-8 rounded-xl magazine-shadow flex flex-col">
|
||||
<h4 className="text-xl font-black font-headline tracking-tight mb-6">{d.earnings}</h4>
|
||||
|
||||
{/* Donut Chart Placeholder */}
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="w-44 h-44 rounded-full border-[16px] border-surface-container flex items-center justify-center relative">
|
||||
<div
|
||||
@ -143,7 +190,9 @@ export default function DashboardPage() {
|
||||
style={{ clipPath: "polygon(50% 50%, 50% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 50%)" }}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<span className="block text-3xl font-black font-headline">$84.2k</span>
|
||||
<span className="block text-3xl font-black font-headline">
|
||||
{loading ? "—" : formatCurrency(totalRevenue)}
|
||||
</span>
|
||||
<span className="text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">
|
||||
{d.grossRevenue}
|
||||
</span>
|
||||
@ -151,31 +200,29 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="space-y-4 mt-6">
|
||||
{[
|
||||
{ color: "bg-primary", label: d.directSales, pct: "65%" },
|
||||
{ color: "bg-secondary", label: d.retailPartners, pct: "25%" },
|
||||
{ color: "bg-tertiary", label: d.affiliates, pct: "10%" },
|
||||
{ color: "bg-primary", label: d.directSales, value: `${data?.metrics.totalProducts ?? 0}` },
|
||||
{ color: "bg-secondary", label: d.retailPartners, value: `${data?.metrics.soldProducts ?? 0}` },
|
||||
{ color: "bg-tertiary", label: d.affiliates, value: `${data?.metrics.refundProducts ?? 0}` },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-3 h-3 ${item.color} rounded-full`}></span>
|
||||
<span className="font-semibold">{item.label}</span>
|
||||
</div>
|
||||
<span className="font-bold text-on-surface-variant">{item.pct}</span>
|
||||
<span className="font-bold text-on-surface-variant">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Orders Table */}
|
||||
<div className="bg-surface-container-lowest rounded-xl magazine-shadow overflow-hidden">
|
||||
<div className="p-8 flex items-center justify-between border-b border-surface-container">
|
||||
<h4 className="text-xl font-black font-headline tracking-tight">{d.recentOrders}</h4>
|
||||
<button className="text-primary font-bold text-sm hover:underline flex items-center gap-1">
|
||||
{d.viewAll}{" "}
|
||||
{d.viewAll}
|
||||
<span className="material-symbols-outlined text-sm">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -192,7 +239,7 @@ export default function DashboardPage() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-surface-container">
|
||||
{recentOrders.map((order) => (
|
||||
{(data?.recentOrders || []).map((order) => (
|
||||
<tr key={order.id} className="hover:bg-surface-container-low/50 transition-colors">
|
||||
<td className="px-8 py-5">
|
||||
<div className="flex items-center gap-4">
|
||||
@ -201,7 +248,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-sm">{order.product}</p>
|
||||
<p className="text-xs text-on-surface-variant">SKU: {order.sku}</p>
|
||||
<p className="text-xs text-on-surface-variant">SKU: {order.sku || "—"}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -210,9 +257,9 @@ export default function DashboardPage() {
|
||||
<p className="text-xs text-on-surface-variant">{order.location}</p>
|
||||
</td>
|
||||
<td className="px-8 py-5 text-sm font-medium text-on-surface-variant">{order.date}</td>
|
||||
<td className="px-8 py-5 text-sm font-bold">{order.amount}</td>
|
||||
<td className="px-8 py-5 text-sm font-bold">{formatCurrency(order.amount)}</td>
|
||||
<td className="px-8 py-5">
|
||||
<span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-tighter ${order.statusColor}`}>
|
||||
<span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-tighter ${statusColor(order.status)}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
</td>
|
||||
@ -223,10 +270,23 @@ export default function DashboardPage() {
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!loading && !error && (data?.recentOrders || []).length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-8 py-10 text-center text-sm font-semibold text-on-surface-variant">
|
||||
No recent orders available.
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="mt-6 rounded-xl bg-error-container px-6 py-4 text-sm font-semibold text-on-error-container">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user