Compare commits

..

31 Commits

Author SHA1 Message Date
g
4c15f433d9 Prefetch dashboard route on login page
All checks were successful
Build Frontend / build (push) Successful in 1m9s
Added useEffect to prefetch the /dashboard route when the login page loads, improving navigation speed after login.
2026-01-13 05:12:13 +00:00
g
1242b8fd46 Add dnd-kit dependencies to lockfile
All checks were successful
Build Frontend / build (push) Successful in 1m24s
Added @dnd-kit/core, @dnd-kit/sortable, and @dnd-kit/utilities to pnpm-lock.yaml. Also updated dependency resolution for eslint-import-resolver-typescript and related packages.
2026-01-13 05:03:26 +00:00
g
fe01f31538 Refactor UI imports and update component paths
Some checks failed
Build Frontend / build (push) Failing after 7s
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
g
a6e6cd0757 Remove widget resizing and edit mode from dashboard
Some checks failed
Build Frontend / build (push) Failing after 7s
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
g
9acd18955e Add scroll area to widget settings modal
Some checks failed
Build Frontend / build (push) Failing after 6s
Wrapped the widget settings modal content in a ScrollArea to improve usability when there are many settings, preventing overflow and keeping the modal compact.
2026-01-12 10:41:52 +00:00
g
318927cd0c Add modular dashboard widgets and layout editor
Some checks failed
Build Frontend / build (push) Failing after 7s
Introduces a modular dashboard system with draggable, configurable widgets including revenue, low stock, recent customers, and pending chats. Adds a dashboard editor for layout customization, widget visibility, and settings. Refactors dashboard content to use the new widget system and improves UI consistency and interactivity.
2026-01-12 10:39:50 +00:00
g
a6b7286b45 Refactor customer dialog styles for consistency
Updated the customer profile dialog to use more consistent and theme-based styling classes, replacing hardcoded colors and gradients with utility classes. Improved layout and text handling for better responsiveness and readability, and simplified button styles for maintainability.
2026-01-12 09:40:08 +00:00
g
d78e6c0725 Update order-table.tsx
All checks were successful
Build Frontend / build (push) Successful in 1m9s
2026-01-12 09:02:51 +00:00
g
3f9d28bf1b Improve browser detection and table UX for Firefox
All checks were successful
Build Frontend / build (push) Successful in 1m10s
Standardizes browser detection logic across admin and storefront pages to more accurately identify Firefox. Updates table rendering logic to provide better compatibility and fallback for Firefox, including conditional use of AnimatePresence and improved loading/empty states. Refines table UI styles for consistency and accessibility.
2026-01-12 08:59:04 +00:00
g
064cd7a486 Update page.tsx
All checks were successful
Build Frontend / build (push) Successful in 1m8s
2026-01-12 08:33:54 +00:00
g
6cd658c4cb Revamp OrderTable UI and add Firefox animation fallback
All checks were successful
Build Frontend / build (push) Successful in 1m8s
Updated the OrderTable component with a new dark-themed UI, improved color schemes, and enhanced button and table styles. Added browser detection to provide a simplified animation experience for Firefox users, ensuring compatibility and smoother rendering. Improved loading state visuals and refined table header and cell styling for better readability.
2026-01-12 08:28:36 +00:00
g
6997838bf7 Revamp dashboard UI with improved dark theme styles
All checks were successful
Build Frontend / build (push) Successful in 1m11s
Updated category, quick actions, and product table components to use enhanced dark theme styling, including new background colors, borders, gradients, and shadow effects. Improved visual hierarchy, contrast, and hover states for better user experience and consistency across dashboard elements.
2026-01-12 08:19:59 +00:00
g
e369741b2d Enhance customer and profit analysis dialogs UI/UX
All checks were successful
Build Frontend / build (push) Successful in 1m14s
Revamps the customer details dialog with improved layout, animations, and clearer stats breakdown. Upgrades the profit analysis modal with animated cards, clearer tier breakdown, and improved cost/margin/profit explanations. Also increases recent activity fetch limit, fixes quote hydration in dashboard content, and minor animation tweak in order table.
2026-01-12 08:12:36 +00:00
g
7ddcd7afb6 Update UI styles and dashboard product display
All checks were successful
Build Frontend / build (push) Successful in 1m10s
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
g
3ffb64cf9a admin
All checks were successful
Build Frontend / build (push) Successful in 1m6s
2026-01-12 07:52:36 +00:00
g
e9737c8b24 Refactor UI to remove Christmas theme and improve actions
All checks were successful
Build Frontend / build (push) Successful in 1m11s
Removed all Christmas-specific theming and logic from the home page and navbar, standardizing colors to indigo. Updated QuickActions to open a modal for adding products instead of navigating to a new page, including logic for product creation and category fetching. Simplified ChatTable row animations and fixed minor layout issues. Updated button styles and mobile menu links for consistency.
2026-01-12 07:43:33 +00:00
g
244014f33a Improve admin UI and vendor invite experience
All checks were successful
Build Frontend / build (push) Successful in 1m7s
Enhanced the admin dashboard tab styling for better clarity. Refactored InviteVendorCard with improved UI, feedback, and clipboard copy functionality. Fixed vendor store ID update to send raw object instead of JSON string. Ensured product price formatting is robust against non-numeric values.
2026-01-12 07:33:16 +00:00
g
1186952ed8 Update page.tsx
All checks were successful
Build Frontend / build (push) Successful in 1m10s
2026-01-12 07:25:05 +00:00
g
0bb1497db6 Update page.tsx
Some checks failed
Build Frontend / build (push) Has been cancelled
2026-01-12 07:24:49 +00:00
g
688f519fd6 Update AdminAnalytics.tsx 2026-01-12 07:23:45 +00:00
g
73adbe5d07 Enhance admin dashboard UI and tables with new styles
All checks were successful
Build Frontend / build (push) Successful in 1m4s
Refactors admin dashboard, users, vendors, shipping, and stock pages to improve UI consistency and visual clarity. Adds new icons, animated transitions, and card styles for stats and tables. Updates table row rendering with framer-motion for smooth animations, improves badge and button styling, and enhances search/filter inputs. Refines loading skeletons and overall layout for a more modern, accessible admin experience.
2026-01-12 07:16:33 +00:00
g
63c833b510 Update page.tsx
All checks were successful
Build Frontend / build (push) Successful in 1m11s
2026-01-12 07:03:19 +00:00
g
bfd31f9d35 Update product-modal.tsx
Some checks failed
Build Frontend / build (push) Has been cancelled
2026-01-12 07:02:08 +00:00
g
f7e768f6d6 Improve product image handling and add costPerUnit
All checks were successful
Build Frontend / build (push) Successful in 1m10s
Added a utility to generate product image URLs, ensuring images are displayed correctly in the product table. Updated the Product model to include an optional costPerUnit field. Minor UI and code formatting improvements were made for consistency.
2026-01-12 06:59:21 +00:00
g
7c7db0fc09 Update product-table.tsx 2026-01-12 06:54:28 +00:00
g
211cdc71f9 Enhance dashboard UI and add order timeline
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.
2026-01-12 06:53:28 +00:00
g
7b95589867 Update PredictionsChart.tsx
All checks were successful
Build Frontend / build (push) Successful in 1m5s
2026-01-12 05:53:16 +00:00
g
c209dd60fc Add 180-day analytics time range options
All checks were successful
Build Frontend / build (push) Successful in 1m6s
Extended analytics dashboards and charts to support a 180-day time range selection. Also updated tooltip position in PredictionsChart for improved UI consistency.
2026-01-12 05:51:29 +00:00
g
a05787a091 Revamp analytics dashboard UI and charts
All checks were successful
Build Frontend / build (push) Successful in 1m11s
Enhanced the AnalyticsDashboard layout with a premium glassmorphism UI, improved toolbar, and reorganized tabs for better clarity. MetricsCard now features dynamic color coding and trend badges. PredictionsChart received scenario simulation UI upgrades, disabled future ranges based on available history, and improved chart tooltips and visuals. ProfitAnalyticsChart added error handling for product images and minor UI refinements. Updated globals.css with new premium utility classes and improved dark mode color variables.
2026-01-12 05:44:54 +00:00
g
a0605e47de Improve chart visuals and add null safety in analytics
All checks were successful
Build Frontend / build (push) Successful in 1m19s
Refactored GrowthAnalyticsChart to use Area for 'orders' with gradient fill and improved dot handling. Enhanced PredictionsChart with consistent null checks for predictions data, improved tooltip rendering, and adjusted chart margins and axis styles. Updated RevenueChart to add activeDot styling for better interactivity.
2026-01-12 04:52:40 +00:00
g
1933ef4007 Update package.json 2026-01-12 04:39:24 +00:00
181 changed files with 9241 additions and 4558 deletions

View File

@@ -4,9 +4,9 @@
import { useState, useEffect, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Label } from "@/components/common/label";
import { toast } from "sonner";
import { Loader2, ArrowRight } from "lucide-react";
import { motion } from "framer-motion";

View File

@@ -1,6 +1,7 @@
"use client"
import React, { Suspense, lazy } from "react";
import React, { Suspense, lazy, useEffect } from "react";
import { useRouter } from "next/navigation";
// Use lazy loading for the form component
@@ -30,6 +31,12 @@ function LoginLoading() {
// Main page component
export default function LoginPage() {
const router = useRouter();
useEffect(() => {
router.prefetch("/dashboard");
}, [router]);
return (
<div className="relative flex items-center justify-center min-h-screen overflow-hidden">
<AuthBackground />

View File

@@ -4,12 +4,12 @@ import { useState } from "react";
import { useRouter } from "next/navigation";
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Label } from "@/components/common/label";
import { Loader2, ArrowRight } from "lucide-react";
import { motion } from "framer-motion";
import { toast } from "@/hooks/use-toast";
import { toast } from "@/lib/hooks/use-toast";
// Matches LoginPage background
const AuthBackground = () => (
@@ -164,3 +164,5 @@ export default function RegisterPage() {
</div>
);
}

View File

@@ -2,11 +2,11 @@
import { useState, useEffect, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import { fetchClient } from "@/lib/api-client";
import { fetchClient } from "@/lib/api/api-client";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Label } from "@/components/common/label";
import { Loader2 } from "lucide-react";
interface Vendor {
@@ -193,3 +193,5 @@ export default function ResetPasswordPage() {
</Suspense>
);
}

View File

@@ -1,18 +1,18 @@
"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Label } from "@/components/common/label";
import { Textarea } from "@/components/common/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/common/select";
import { Badge } from "@/components/common/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/common/alert-dialog";
import { UserX, Shield, Search, Ban, Unlock, Loader2 } from "lucide-react";
import { fetchClient } from "@/lib/api-client";
import { useToast } from "@/hooks/use-toast";
import { fetchClient } from "@/lib/api/api-client";
import { useToast } from "@/lib/hooks/use-toast";
interface BlockedUser {
_id: string;
@@ -446,3 +446,4 @@ export default function AdminBanPage() {
</div>
);
}

View File

@@ -1,13 +1,13 @@
"use client";
import React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Label } from "@/components/common/label";
import { Textarea } from "@/components/common/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/common/select";
import { Badge } from "@/components/common/badge";
import { UserPlus, Mail, Copy, Check } from "lucide-react";
import { useState } from "react";
import Link from "next/link";
@@ -206,3 +206,4 @@ export default function AdminInvitePage() {
</div>
);
}

View File

@@ -1,8 +1,9 @@
import React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Package, AlertTriangle } from "lucide-react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Package, AlertTriangle, CheckCircle2, XCircle, DollarSign } from "lucide-react";
import { fetchServer } from "@/lib/api";
import OrdersTable from "@/components/admin/OrdersTable";
import { MotionWrapper } from "@/components/common/motion-wrapper";
export const dynamic = 'force-dynamic';
@@ -26,7 +27,6 @@ interface SystemStats {
chats: number;
}
export default async function AdminOrdersPage() {
let orders: Order[] = [];
let systemStats: SystemStats | null = null;
@@ -46,14 +46,14 @@ export default async function AdminOrdersPage() {
if (error) {
return (
<div className="space-y-6">
<div className="space-y-6 p-1">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Recent Orders</h1>
<p className="text-sm text-muted-foreground mt-1">Monitor and manage platform orders</p>
<h1 className="text-3xl font-bold tracking-tight">Recent Orders</h1>
<p className="text-muted-foreground mt-2">Monitor and manage platform orders</p>
</div>
<Card>
<Card className="border-destructive/50 bg-destructive/10">
<CardContent className="pt-6">
<div className="text-center text-red-500">
<div className="text-center text-destructive">
<p>{error}</p>
</div>
</CardContent>
@@ -62,146 +62,184 @@ export default async function AdminOrdersPage() {
);
}
const acknowledgedOrders = orders.filter(o => o.status === 'acknowledged');
const paidOrders = orders.filter(o => o.status === 'paid');
const completedOrders = orders.filter(o => o.status === 'completed');
const cancelledOrders = orders.filter(o => o.status === 'cancelled');
return (
<div className="space-y-6">
<div className="space-y-8 p-1">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Recent Orders</h1>
<p className="text-sm text-muted-foreground mt-1">Monitor and manage platform orders</p>
<h1 className="text-3xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/70">
Recent Orders
</h1>
<p className="text-muted-foreground mt-2 text-lg">
Monitor and manage platform transaction activity
</p>
</div>
<MotionWrapper>
<div className="space-y-8">
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm hover:shadow-md transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Orders</CardTitle>
<Package className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Total Orders</CardTitle>
<div className="p-2 rounded-lg bg-primary/10">
<Package className="h-4 w-4 text-primary" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{systemStats?.orders || 0}</div>
<p className="text-xs text-muted-foreground">All platform orders</p>
<div className="flex items-center mt-1">
<div className="h-1.5 w-1.5 rounded-full bg-primary mr-2" />
<p className="text-xs text-muted-foreground">Lifetime volume</p>
</div>
</CardContent>
</Card>
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm hover:shadow-md transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Acknowledged</CardTitle>
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Acknowledged</CardTitle>
<div className="p-2 rounded-lg bg-purple-500/10">
<AlertTriangle className="h-4 w-4 text-purple-500" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{acknowledgedOrders.length}</div>
<p className="text-xs text-muted-foreground">Vendor accepted</p>
<div className="flex items-center mt-1">
<div className="h-1.5 w-1.5 rounded-full bg-purple-500 mr-2" />
<p className="text-xs text-muted-foreground">Vendor pending</p>
</div>
</CardContent>
</Card>
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm hover:shadow-md transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Paid</CardTitle>
<Package className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Paid</CardTitle>
<div className="p-2 rounded-lg bg-emerald-500/10">
<DollarSign className="h-4 w-4 text-emerald-500" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{paidOrders.length}</div>
<p className="text-xs text-muted-foreground">Payment received</p>
<div className="flex items-center mt-1">
<div className="h-1.5 w-1.5 rounded-full bg-emerald-500 mr-2" />
<p className="text-xs text-muted-foreground">Processing</p>
</div>
</CardContent>
</Card>
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm hover:shadow-md transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Completed</CardTitle>
<Package className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Completed</CardTitle>
<div className="p-2 rounded-lg bg-blue-500/10">
<CheckCircle2 className="h-4 w-4 text-blue-500" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{completedOrders.length}</div>
<p className="text-xs text-muted-foreground">Successfully delivered</p>
<div className="flex items-center mt-1">
<div className="h-1.5 w-1.5 rounded-full bg-blue-500 mr-2" />
<p className="text-xs text-muted-foreground">Delivered</p>
</div>
</CardContent>
</Card>
</div>
{/* Orders Table with Pagination */}
<div className="bg-background/50 backdrop-blur-sm rounded-xl border border-border/40 overflow-hidden shadow-sm">
<OrdersTable orders={orders} />
</div>
{/* Order Analytics */}
<div className="grid gap-6 md:grid-cols-2">
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm">
<CardHeader>
<CardTitle>Order Status Distribution</CardTitle>
<CardDescription>Breakdown of recent orders by status</CardDescription>
<CardTitle>Status Distribution</CardTitle>
<CardDescription>Breakdown of active orders</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-purple-500 rounded-full"></div>
<span className="text-sm">Acknowledged</span>
<div className="flex items-center justify-between p-2 rounded-lg hover:bg-muted/30 transition-colors">
<div className="flex items-center space-x-3">
<div className="w-2 h-12 bg-purple-500 rounded-full"></div>
<div>
<p className="font-medium text-sm">Acknowledged</p>
<p className="text-xs text-muted-foreground">Waiting for shipment</p>
</div>
<span className="text-sm font-medium">
</div>
<span className="font-bold">
{orders.length > 0 ? Math.round((acknowledgedOrders.length / orders.length) * 100) : 0}%
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-emerald-500 rounded-full"></div>
<span className="text-sm">Paid</span>
<div className="flex items-center justify-between p-2 rounded-lg hover:bg-muted/30 transition-colors">
<div className="flex items-center space-x-3">
<div className="w-2 h-12 bg-emerald-500 rounded-full"></div>
<div>
<p className="font-medium text-sm">Paid</p>
<p className="text-xs text-muted-foreground">Payment confirmed</p>
</div>
<span className="text-sm font-medium">
</div>
<span className="font-bold">
{orders.length > 0 ? Math.round((paidOrders.length / orders.length) * 100) : 0}%
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span className="text-sm">Completed</span>
<div className="flex items-center justify-between p-2 rounded-lg hover:bg-muted/30 transition-colors">
<div className="flex items-center space-x-3">
<div className="w-2 h-12 bg-blue-500 rounded-full"></div>
<div>
<p className="font-medium text-sm">Completed</p>
<p className="text-xs text-muted-foreground">Successfully concluded</p>
</div>
<span className="text-sm font-medium">
</div>
<span className="font-bold">
{orders.length > 0 ? Math.round((completedOrders.length / orders.length) * 100) : 0}%
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<span className="text-sm">Cancelled</span>
</div>
<span className="text-sm font-medium">
{orders.length > 0 ? Math.round((cancelledOrders.length / orders.length) * 100) : 0}%
</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm">
<CardHeader>
<CardTitle>Order Summary</CardTitle>
<CardDescription>Recent order activity breakdown</CardDescription>
<CardTitle>Activity Summary</CardTitle>
<CardDescription>Recent volume breakdown</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm">Total Recent Orders</span>
<span className="text-sm font-medium">{orders.length}</span>
<div className="space-y-6">
<div className="flex items-center justify-between pb-4 border-b border-border/40">
<span className="text-sm text-muted-foreground">Total Displayed Orders</span>
<span className="text-xl font-bold">{orders.length}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Acknowledged</span>
<span className="text-sm font-medium">{acknowledgedOrders.length}</span>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<span className="text-xs uppercase text-muted-foreground tracking-wider">Active</span>
<p className="text-lg font-semibold">{acknowledgedOrders.length + paidOrders.length}</p>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Paid</span>
<span className="text-sm font-medium">{paidOrders.length}</span>
<div className="space-y-1">
<span className="text-xs uppercase text-muted-foreground tracking-wider">Finished</span>
<p className="text-lg font-semibold">{completedOrders.length}</p>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Completed</span>
<span className="text-sm font-medium">{completedOrders.length}</span>
<div className="space-y-1">
<span className="text-xs uppercase text-muted-foreground tracking-wider">Voided</span>
<p className="text-lg font-semibold text-destructive">{cancelledOrders.length}</p>
</div>
<div className="space-y-1">
<span className="text-xs uppercase text-muted-foreground tracking-wider">Success Rate</span>
<p className="text-lg font-semibold text-green-500">
{orders.length > 0 ? Math.round((completedOrders.length / (orders.length - (acknowledgedOrders.length + paidOrders.length))) * 100) || 100 : 0}%
</p>
</div>
<div className="flex items-center justify-between">
<span className="text-sm">Cancelled</span>
<span className="text-sm font-medium">{cancelledOrders.length}</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</MotionWrapper>
</div>
);
}

View File

@@ -2,12 +2,12 @@
export const dynamic = "force-dynamic";
import React, { Suspense, lazy, useState, useEffect, Component, ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { Button } from "@/components/common/button";
import Link from "next/link";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/common/tabs";
import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/common/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { AlertCircle, RefreshCw } from "lucide-react";
// Error Boundary Component
@@ -183,7 +183,7 @@ function AdminComponentSkeleton({ showSlowWarning = false }: { showSlowWarning?:
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
<Card
key={i}
className="overflow-hidden animate-in fade-in slide-in-from-bottom-4"
className="overflow-hidden animate-in fade-in slide-in-from-bottom-4 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm"
style={{
animationDelay: `${i * 50}ms`,
animationDuration: '400ms',
@@ -288,7 +288,7 @@ function ManagementCardsSkeleton({ showSlowWarning = false }: { showSlowWarning?
{[1, 2, 3, 4].map((i) => (
<Card
key={i}
className="overflow-hidden animate-in fade-in slide-in-from-bottom-4"
className="overflow-hidden animate-in fade-in slide-in-from-bottom-4 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm"
style={{
animationDelay: `${i * 75}ms`,
animationDuration: '400ms',
@@ -392,22 +392,23 @@ export default function AdminPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between animate-in fade-in slide-in-from-top-2 duration-300">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Admin Dashboard</h1>
<h1 className="text-2xl font-semibold tracking-tight text-foreground">Admin Dashboard</h1>
<p className="text-sm text-muted-foreground mt-1">Platform analytics and vendor management</p>
</div>
<Button asChild variant="outline" size="sm">
<Button asChild variant="outline" size="sm" className="border-border/50 bg-background/50 backdrop-blur-sm hover:bg-background/80 transition-all">
<Link href="/dashboard">Back to Dashboard</Link>
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList>
<TabsList className="bg-muted/20 p-1 border border-border/40 backdrop-blur-sm h-auto">
<TabsTrigger
value="analytics"
onMouseEnter={() => handleTabHover("analytics")}
onFocus={() => handleTabFocus("analytics")}
className="data-[state=active]:bg-background/80 data-[state=active]:backdrop-blur-sm data-[state=active]:text-foreground data-[state=active]:shadow-sm px-6 py-2 transition-all"
>
Analytics
</TabsTrigger>
@@ -415,6 +416,7 @@ export default function AdminPage() {
value="management"
onMouseEnter={() => handleTabHover("management")}
onFocus={() => handleTabFocus("management")}
className="data-[state=active]:bg-background/80 data-[state=active]:backdrop-blur-sm data-[state=active]:text-foreground data-[state=active]:shadow-sm px-6 py-2 transition-all"
>
Management
</TabsTrigger>
@@ -464,3 +466,4 @@ export default function AdminPage() {
}

View File

@@ -1,9 +1,10 @@
import React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Server, Database, Cpu, HardDrive, Activity } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import { Server, Database, Cpu, HardDrive, Activity, Zap } from "lucide-react";
import { fetchServer } from "@/lib/api";
import SystemStatusCard from "@/components/admin/SystemStatusCard";
import { MotionWrapper } from "@/components/common/motion-wrapper";
export const dynamic = 'force-dynamic';
@@ -35,17 +36,19 @@ export default async function AdminStatusPage() {
console.error("Failed to fetch system status:", err);
error = "Failed to load system status";
}
if (error) {
return (
<div className="space-y-6">
<div className="space-y-8 p-1">
<div>
<h1 className="text-2xl font-semibold tracking-tight">System Status</h1>
<p className="text-sm text-muted-foreground mt-1">Monitor system health and performance metrics</p>
<h1 className="text-3xl font-bold tracking-tight">System Status</h1>
<p className="text-muted-foreground mt-2">Monitor system health and real-time performance metrics</p>
</div>
<Card>
<Card className="border-destructive/50 bg-destructive/10">
<CardContent className="pt-6">
<div className="text-center text-red-500">
<p>{error}</p>
<div className="flex items-center gap-3 text-destructive">
<Activity className="h-5 w-5" />
<p className="font-medium">{error}</p>
</div>
</CardContent>
</Card>
@@ -71,112 +74,174 @@ export default async function AdminStatusPage() {
Math.round((systemStatus.memory.heapUsed / systemStatus.memory.heapTotal) * 100) : 0;
return (
<div className="space-y-6">
<div className="space-y-8 p-1">
<div>
<h1 className="text-2xl font-semibold tracking-tight">System Status</h1>
<p className="text-sm text-muted-foreground mt-1">Monitor system health and performance metrics</p>
<h1 className="text-3xl font-bold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-foreground to-foreground/70">
System Status
</h1>
<p className="text-muted-foreground mt-2 text-lg">
Monitor system health and real-time performance metrics
</p>
</div>
<MotionWrapper>
<div className="space-y-8">
<SystemStatusCard />
<div className="grid gap-4 lg:gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{/* Server Status */}
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm hover:shadow-md transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Server Status</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Server Uptime</CardTitle>
<div className="p-2 rounded-lg bg-green-500/10">
<Server className="h-4 w-4 text-green-500" />
</div>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-2">
<Badge variant="default" className="bg-green-500">Online</Badge>
<span className="text-sm text-muted-foreground">
<span className="text-2xl font-bold">
{systemStatus ? formatUptime(systemStatus.uptimeSeconds) : 'N/A'}
</span>
</div>
<p className="text-xs text-muted-foreground mt-2">
Last checked: {new Date().toLocaleTimeString()}
</p>
<div className="flex items-center mt-2 space-x-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
Online
</Badge>
<span className="text-xs text-muted-foreground">
Checked: {new Date().toLocaleTimeString()}
</span>
</div>
</CardContent>
</Card>
{/* Database Status */}
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm hover:shadow-md transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Database</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Database Health</CardTitle>
<div className="p-2 rounded-lg bg-blue-500/10">
<Database className="h-4 w-4 text-blue-500" />
</div>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-2">
<Badge variant="default" className="bg-green-500">Connected</Badge>
<span className="text-sm text-muted-foreground">
{systemStatus ? `${systemStatus.counts.vendors + systemStatus.counts.orders + systemStatus.counts.products} records` : 'N/A'}
<span className="text-2xl font-bold">
{systemStatus ? `${(systemStatus.counts.vendors + systemStatus.counts.orders + systemStatus.counts.products).toLocaleString()}` : '0'}
</span>
<span className="text-sm text-muted-foreground self-end mb-1">records</span>
</div>
<div className="flex items-center mt-2 space-x-2">
<Badge variant="outline" className="bg-blue-500/10 text-blue-500 border-blue-500/20">
Connected
</Badge>
<span className="text-xs text-muted-foreground">
4 active collections
</span>
</div>
<p className="text-xs text-muted-foreground mt-2">
Total collections: 4
</p>
</CardContent>
</Card>
{/* Memory Usage */}
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm hover:shadow-md transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Memory</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Memory Usage</CardTitle>
<div className="p-2 rounded-lg bg-purple-500/10">
<HardDrive className="h-4 w-4 text-purple-500" />
</div>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-2">
<Badge variant={memoryUsagePercent > 80 ? "destructive" : memoryUsagePercent > 60 ? "secondary" : "outline"}>
{memoryUsagePercent}%
</Badge>
<span className="text-sm text-muted-foreground">
<span className="text-2xl font-bold">
{systemStatus ? formatBytes(systemStatus.memory.heapUsed) : 'N/A'}
</span>
<span className="text-sm text-muted-foreground self-end mb-1">used</span>
</div>
<p className="text-xs text-muted-foreground mt-2">
<div className="flex items-center mt-2 space-x-2">
<Badge variant="outline" className={`
${memoryUsagePercent > 80 ? 'bg-red-500/10 text-red-500 border-red-500/20' :
memoryUsagePercent > 60 ? 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20' :
'bg-purple-500/10 text-purple-500 border-purple-500/20'}
`}>
{memoryUsagePercent}% Load
</Badge>
<span className="text-xs text-muted-foreground">
Total: {systemStatus ? formatBytes(systemStatus.memory.heapTotal) : 'N/A'}
</p>
</span>
</div>
</CardContent>
</Card>
{/* Platform Stats */}
<Card>
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm hover:shadow-md transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Platform Stats</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Platform Activity</CardTitle>
<div className="p-2 rounded-lg bg-orange-500/10">
<Activity className="h-4 w-4 text-orange-500" />
</div>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-2">
<Badge variant="default" className="bg-green-500">Active</Badge>
<span className="text-sm text-muted-foreground">
{systemStatus ? `${systemStatus.counts.vendors} vendors` : 'N/A'}
<span className="text-2xl font-bold">
{systemStatus ? systemStatus.counts.vendors : '0'}
</span>
<span className="text-sm text-muted-foreground self-end mb-1">Active Vendors</span>
</div>
<div className="flex items-center mt-2 space-x-2">
<Badge variant="outline" className="bg-orange-500/10 text-orange-500 border-orange-500/20">
Live
</Badge>
<span className="text-xs text-muted-foreground">
{systemStatus ? `${systemStatus.counts.orders} orders` : '0 orders'}
</span>
</div>
<p className="text-xs text-muted-foreground mt-2">
{systemStatus ? `${systemStatus.counts.orders} orders, ${systemStatus.counts.products} products` : 'N/A'}
</p>
</CardContent>
</Card>
{/* Node.js Version */}
<Card>
{/* Runtime Info */}
<Card className="bg-background/50 backdrop-blur-sm border-border/40 shadow-sm hover:shadow-md transition-all duration-300 md:col-span-2 lg:col-span-2">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Runtime</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Runtime Environment</CardTitle>
<div className="p-2 rounded-lg bg-zinc-500/10">
<Cpu className="h-4 w-4 text-zinc-500" />
</div>
</CardHeader>
<CardContent>
<div className="flex items-center space-x-2">
<Badge variant="outline">
{systemStatus ? `Node ${systemStatus.versions.node}` : 'N/A'}
</Badge>
<span className="text-sm text-muted-foreground">Runtime</span>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div>
<p className="text-2xl font-bold">Node.js</p>
<p className="text-xs text-muted-foreground mt-1">Runtime</p>
</div>
<Badge variant="secondary" className="text-sm h-7">
{systemStatus ? `v${systemStatus.versions.node}` : 'N/A'}
</Badge>
</div>
<div className="h-8 w-px bg-border/50 hidden sm:block" />
<div className="flex items-center gap-4">
<div>
<p className="text-2xl font-bold">V8</p>
<p className="text-xs text-muted-foreground mt-1">Engine</p>
</div>
<Badge variant="secondary" className="text-sm h-7">
{systemStatus ? systemStatus.versions.v8 : 'N/A'}
</Badge>
</div>
<div className="h-8 w-px bg-border/50 hidden sm:block" />
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/30 px-3 py-1.5 rounded-md">
<Zap className="h-3.5 w-3.5 text-yellow-500" />
<span>Performance Optimized</span>
</div>
</div>
<p className="text-xs text-muted-foreground mt-2">
{systemStatus ? `V8: ${systemStatus.versions.v8}` : 'N/A'}
</p>
</CardContent>
</Card>
</div>
</div>
</MotionWrapper>
</div>
);
}

View File

@@ -1,15 +1,16 @@
"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { Search, Ban, UserCheck, Package, DollarSign, Loader2, Repeat } from "lucide-react";
import { fetchClient } from "@/lib/api-client";
import { useToast } from "@/hooks/use-toast";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/common/tooltip";
import { Search, Ban, UserCheck, Package, DollarSign, Loader2, Repeat, Users, ShoppingBag, CreditCard, UserX } from "lucide-react";
import { fetchClient } from "@/lib/api/api-client";
import { useToast } from "@/lib/hooks/use-toast";
import { motion, AnimatePresence } from "framer-motion";
interface TelegramUser {
telegramUserId: string;
@@ -49,6 +50,14 @@ export default function AdminUsersPage() {
const { toast } = useToast();
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState<TelegramUser[]>([]);
// State for browser detection
// Browser detection
const [isFirefox, setIsFirefox] = useState(false);
useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
setIsFirefox(ua.includes("firefox") && !ua.includes("chrome"));
}, []);
const [searchQuery, setSearchQuery] = useState("");
const [page, setPage] = useState(1);
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
@@ -88,123 +97,86 @@ export default function AdminUsersPage() {
const totalSpent = users.reduce((sum, u) => sum + u.totalSpent, 0);
const totalOrders = users.reduce((sum, u) => sum + u.totalOrders, 0);
const stats = [
{
title: "Total Users",
value: users.length,
description: "Registered users",
icon: Users,
},
{
title: "Users with Orders",
value: usersWithOrders.length,
description: `${users.length > 0 ? Math.round((usersWithOrders.length / users.length) * 100) : 0}% conversion rate`,
icon: ShoppingBag,
},
{
title: "Total Revenue",
value: formatCurrency(totalSpent),
description: `${totalOrders} total orders`,
icon: DollarSign,
},
{
title: "Returning",
value: returningCustomers.length,
description: `${usersWithOrders.length > 0 ? Math.round((returningCustomers.length / usersWithOrders.length) * 100) : 0}% of customers`,
icon: Repeat,
},
{
title: "Blocked",
value: blockedUsers.length,
description: `${users.length > 0 ? Math.round((blockedUsers.length / users.length) * 100) : 0}% blocked rate`,
icon: UserX,
},
];
return (
<div className="space-y-6">
<div className="space-y-6 animate-in fade-in duration-500">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Telegram Users</h1>
<h1 className="text-2xl font-semibold tracking-tight text-foreground">Telegram Users</h1>
<p className="text-sm text-muted-foreground mt-1">Manage Telegram user accounts and view statistics</p>
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-5">
<Card>
{stats.map((stat, i) => (
<Card key={i} className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
<stat.icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
<div className="h-12 flex items-center">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground/50" />
</div>
) : (
<>
<div className="text-2xl font-bold">{users.length}</div>
<p className="text-xs text-muted-foreground">Registered users</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Users with Orders</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
<div className="text-2xl font-bold">{usersWithOrders.length}</div>
<p className="text-xs text-muted-foreground">
{users.length > 0 ? Math.round((usersWithOrders.length / users.length) * 100) : 0}% conversion rate
</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
<div className="text-2xl font-bold">{formatCurrency(totalSpent)}</div>
<p className="text-xs text-muted-foreground">{totalOrders} total orders</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Returning Customers</CardTitle>
<Repeat className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
<div className="text-2xl font-bold">{returningCustomers.length}</div>
<p className="text-xs text-muted-foreground">
{usersWithOrders.length > 0 ? Math.round((returningCustomers.length / usersWithOrders.length) * 100) : 0}% of customers
</p>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Blocked Users</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<>
<div className="text-2xl font-bold">{blockedUsers.length}</div>
<p className="text-xs text-muted-foreground">
{users.length > 0 ? Math.round((blockedUsers.length / users.length) * 100) : 0}% blocked rate
</p>
</>
<div className="animate-in fade-in slide-in-from-bottom-2 duration-500" style={{ animationDelay: `${i * 100}ms` }}>
<div className="text-2xl font-bold">{stat.value}</div>
<p className="text-xs text-muted-foreground">{stat.description}</p>
</div>
)}
</CardContent>
</Card>
))}
</div>
{/* Search and Filters */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader className="pb-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<CardTitle>User Management</CardTitle>
<CardTitle className="text-lg font-medium">User Management</CardTitle>
<CardDescription>View and manage all Telegram user accounts</CardDescription>
</div>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search users..."
className="pl-8 w-64"
className="pl-9 w-64 bg-background/50 border-border/50 focus:bg-background transition-colors"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
@@ -216,19 +188,11 @@ export default function AdminUsersPage() {
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : users.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{searchQuery ? "No users found matching your search" : "No users found"}
</div>
) : (
<div className="rounded-md border border-border/50 overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>User ID</TableHead>
<TableHeader className="bg-muted/30">
<TableRow className="border-border/50 hover:bg-transparent">
<TableHead className="w-[100px]">User ID</TableHead>
<TableHead>Username</TableHead>
<TableHead>Orders</TableHead>
<TableHead>Total Spent</TableHead>
@@ -239,88 +203,103 @@ export default function AdminUsersPage() {
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.telegramUserId}>
<TableCell>
<div className="font-mono text-sm">{user.telegramUserId}</div>
</TableCell>
<TableCell>
<div className="font-medium">
{user.telegramUsername !== "Unknown" ? `@${user.telegramUsername}` : "Unknown"}
<AnimatePresence mode="popLayout">
{loading ? (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
<div className="flex items-center justify-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
Loading users...
</div>
</TableCell>
</TableRow>
) : users.length > 0 ? (
users.map((user, index) => (
<motion.tr
key={user.telegramUserId}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
className={`group border-b border-border/50 transition-colors ${user.isBlocked ? "bg-destructive/5 hover:bg-destructive/10" : "hover:bg-muted/40"}`}
>
<TableCell className="font-mono text-xs">{user.telegramUserId}</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Package className="h-4 w-4 text-muted-foreground" />
<span>{user.totalOrders}</span>
{user.completedOrders > 0 && (
<Badge variant="secondary" className="text-xs">
{user.completedOrders} completed
</Badge>
<div className="flex items-center gap-2">
<span className="font-medium">@{user.telegramUsername || "Unknown"}</span>
{user.isBlocked && (
<Badge variant="destructive" className="h-5 px-1.5 text-[10px]">Blocked</Badge>
)}
</div>
</TableCell>
<TableCell>{user.totalOrders}</TableCell>
<TableCell>{formatCurrency(user.totalSpent)}</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
<DollarSign className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{formatCurrency(user.totalSpent)}</span>
<div className="flex flex-col gap-1 text-xs">
<span className="text-emerald-500">{user.completedOrders} Completed</span>
<span className="text-muted-foreground">{user.paidOrders - user.completedOrders} Pending</span>
</div>
</TableCell>
<TableCell>
<TableCell className="text-xs text-muted-foreground">
{user.firstOrderDate ? new Date(user.firstOrderDate).toLocaleDateString() : "-"}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{user.lastOrderDate ? new Date(user.lastOrderDate).toLocaleDateString() : "-"}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1">
{user.isBlocked ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="destructive">
<Ban className="h-3 w-3 mr-1" />
Blocked
</Badge>
<Button size="sm" variant="outline" className="h-8 border-emerald-500/20 text-emerald-500 hover:bg-emerald-500/10 hover:text-emerald-400">
<UserCheck className="h-4 w-4 mr-1" />
Unblock
</Button>
</TooltipTrigger>
{user.blockedReason && (
<TooltipContent>
<p className="max-w-xs">{user.blockedReason}</p>
<p>Unblock this user</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : user.totalOrders > 0 ? (
<Badge variant="default">Active</Badge>
) : (
<Badge variant="secondary">No Orders</Badge>
)}
</TableCell>
<TableCell>
{user.firstOrderDate
? new Date(user.firstOrderDate).toLocaleDateString()
: 'N/A'}
</TableCell>
<TableCell>
{user.lastOrderDate
? new Date(user.lastOrderDate).toLocaleDateString()
: 'N/A'}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end space-x-2">
{!user.isBlocked ? (
<Button variant="outline" size="sm">
<Ban className="h-4 w-4" />
</Button>
) : (
<Button variant="outline" size="sm">
<UserCheck className="h-4 w-4" />
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="sm" variant="outline" className="h-8 border-destructive/20 text-destructive hover:bg-destructive/10 hover:text-destructive">
<Ban className="h-4 w-4 mr-1" />
Block
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Block access to the store</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</TableCell>
</motion.tr>
))
) : (
<TableRow>
<TableCell colSpan={8} className="h-32 text-center text-muted-foreground">
<div className="flex flex-col items-center justify-center gap-2">
<Users className="h-8 w-8 opacity-20" />
<p>No users found</p>
</div>
</TableCell>
</TableRow>
))}
)}
</AnimatePresence>
</TableBody>
</Table>
)}
</div>
{pagination && pagination.totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<div className="mt-4 flex items-center justify-between border-t border-border/50 pt-4">
<div className="text-sm text-muted-foreground">
Showing page {pagination.page} of {pagination.totalPages} ({pagination.total} total users)
Showing page <span className="font-medium text-foreground">{pagination.page}</span> of {pagination.totalPages}
</div>
<div className="flex gap-2">
<Button
@@ -328,6 +307,7 @@ export default function AdminUsersPage() {
size="sm"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={!pagination.hasPrevPage}
className="h-8"
>
Previous
</Button>
@@ -336,6 +316,7 @@ export default function AdminUsersPage() {
size="sm"
onClick={() => setPage(p => p + 1)}
disabled={!pagination.hasNextPage}
className="h-8"
>
Next
</Button>
@@ -347,3 +328,5 @@ export default function AdminUsersPage() {
</div>
);
}

View File

@@ -1,14 +1,25 @@
"use client";
import React, { useState, useEffect, useCallback } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Search, MoreHorizontal, UserCheck, UserX, Mail, Loader2 } from "lucide-react";
import { fetchClient } from "@/lib/api-client";
import { useToast } from "@/hooks/use-toast";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
import { Search, MoreHorizontal, UserCheck, UserX, Mail, Loader2, Store, Shield, ShieldAlert, Clock, Calendar, Pencil, Plus } from "lucide-react";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/common/dialog";
import { Label } from "@/components/common/label";
import { fetchClient } from "@/lib/api/api-client";
import { useToast } from "@/lib/hooks/use-toast";
import { motion, AnimatePresence } from "framer-motion";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/common/dropdown-menu";
interface Vendor {
_id: string;
@@ -37,9 +48,73 @@ export default function AdminVendorsPage() {
const { toast } = useToast();
const [loading, setLoading] = useState(true);
const [vendors, setVendors] = useState<Vendor[]>([]);
// State for browser detection
const [isFirefox, setIsFirefox] = useState(false);
useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
setIsFirefox(ua.includes("firefox") && !ua.includes("chrome"));
}, []);
const [page, setPage] = useState(1);
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [isEditStoreOpen, setIsEditStoreOpen] = useState(false);
const [editingVendor, setEditingVendor] = useState<Vendor | null>(null);
const [newStoreId, setNewStoreId] = useState("");
const [updating, setUpdating] = useState(false);
const handleToggleStatus = async (vendor: Vendor) => {
try {
await fetchClient(`/admin/vendors/${vendor._id}/status`, {
method: 'PATCH',
body: { isActive: !vendor.isActive }
});
toast({
title: "Success",
description: `Vendor ${vendor.isActive ? 'suspended' : 'activated'} successfully`,
});
fetchVendors();
} catch (error: any) {
toast({
title: "Error",
description: error.message || "Failed to update vendor status",
variant: "destructive",
});
}
};
const handleEditStore = (vendor: Vendor) => {
setEditingVendor(vendor);
setNewStoreId(vendor.storeId || "");
setIsEditStoreOpen(true);
};
const saveStoreId = async () => {
if (!editingVendor) return;
try {
setUpdating(true);
await fetchClient(`/admin/vendors/${editingVendor._id}/store-id`, {
method: 'PUT',
body: { storeId: newStoreId }
});
toast({
title: "Success",
description: "Store ID updated successfully",
});
setIsEditStoreOpen(false);
fetchVendors(); // Refresh list
} catch (error: any) {
toast({
title: "Error",
description: error.message || "Failed to update store ID",
variant: "destructive",
});
} finally {
setUpdating(false);
}
};
const fetchVendors = useCallback(async () => {
try {
@@ -79,96 +154,90 @@ export default function AdminVendorsPage() {
const adminVendors = vendors.filter(v => v.isAdmin);
const totalVendors = pagination?.total || vendors.length;
const stats = [
{
title: "Total Vendors",
value: totalVendors,
description: "Registered vendors",
icon: Store,
},
{
title: "Active Vendors",
value: activeVendors.length,
description: `${vendors.length > 0 ? Math.round((activeVendors.length / vendors.length) * 100) : 0}% active rate`,
icon: UserCheck,
},
{
title: "Suspended",
value: suspendedVendors.length,
description: `${vendors.length > 0 ? Math.round((suspendedVendors.length / vendors.length) * 100) : 0}% suspension rate`,
icon: UserX,
},
{
title: "Admin Users",
value: adminVendors.length,
description: "Administrative access",
icon: ShieldAlert,
},
];
return (
<div className="space-y-6">
<div className="space-y-6 animate-in fade-in duration-500">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight">All Vendors</h1>
<h1 className="text-2xl font-semibold tracking-tight text-foreground">All Vendors</h1>
<p className="text-sm text-muted-foreground mt-1">Manage vendor accounts and permissions</p>
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-4">
<Card>
{stats.map((stat, i) => (
<Card key={i} className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Vendors</CardTitle>
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
<stat.icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{totalVendors}</div>
<p className="text-xs text-muted-foreground">Registered vendors</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Vendors</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{activeVendors.length}</div>
<p className="text-xs text-muted-foreground">
{vendors.length > 0 ? Math.round((activeVendors.length / vendors.length) * 100) : 0}% active rate
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Suspended</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{suspendedVendors.length}</div>
<p className="text-xs text-muted-foreground">
{vendors.length > 0 ? Math.round((suspendedVendors.length / vendors.length) * 100) : 0}% suspension rate
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Admin Users</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{adminVendors.length}</div>
<p className="text-xs text-muted-foreground">Administrative access</p>
<div className="animate-in fade-in slide-in-from-bottom-2 duration-500" style={{ animationDelay: `${i * 100}ms` }}>
<div className="text-2xl font-bold">{stat.value}</div>
<p className="text-xs text-muted-foreground">{stat.description}</p>
</div>
</CardContent>
</Card>
))}
</div>
{/* Search and Filters */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader className="pb-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<CardTitle>Vendor Management</CardTitle>
<CardTitle className="text-lg font-medium">Vendor Management</CardTitle>
<CardDescription>View and manage all vendor accounts</CardDescription>
</div>
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search vendors..."
className="pl-8 w-64"
className="pl-9 w-64 bg-background/50 border-border/50 focus:bg-background transition-colors"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Button variant="outline" size="sm">
<Button variant="outline" size="sm" className="bg-background/50 border-border/50 hover:bg-background transition-colors">
<Mail className="h-4 w-4 mr-2" />
Send Message
Message
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : filteredVendors.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
{searchQuery.trim() ? "No vendors found matching your search" : "No vendors found"}
</div>
) : (
<>
<div className="rounded-md border border-border/50 overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHeader className="bg-muted/30">
<TableRow className="border-border/50 hover:bg-transparent">
<TableHead>Vendor</TableHead>
<TableHead>Store</TableHead>
<TableHead>Status</TableHead>
@@ -178,53 +247,269 @@ export default function AdminVendorsPage() {
</TableRow>
</TableHeader>
<TableBody>
{filteredVendors.map((vendor) => (
<TableRow key={vendor._id}>
<TableCell>
<div className="font-medium">{vendor.username}</div>
{isFirefox ? (
loading ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center">
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
<Loader2 className="h-8 w-8 animate-spin opacity-25" />
<p>Loading vendors...</p>
</div>
</TableCell>
<TableCell>{vendor.storeId || 'No store'}</TableCell>
</TableRow>
) : filteredVendors.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
{searchQuery.trim() ? "No vendors found matching your search" : "No vendors found"}
</TableCell>
</TableRow>
) : (
filteredVendors.map((vendor, index) => (
<motion.tr
key={vendor._id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
>
<TableCell>
<div className="flex flex-col space-y-1">
<div className="font-medium flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-xs">
{vendor.username.substring(0, 2).toUpperCase()}
</div>
{vendor.username}
</div>
</TableCell>
<TableCell>
{vendor.storeId ? (
<div className="flex items-center gap-2 group/store">
<span className="font-mono text-xs">{vendor.storeId}</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 opacity-0 group-hover/store:opacity-100 transition-opacity"
onClick={() => handleEditStore(vendor)}
>
<Pencil className="h-3 w-3" />
</Button>
</div>
) : (
<div className="flex items-center gap-2">
<span className="text-muted-foreground italic text-xs">No store</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-primary"
onClick={() => handleEditStore(vendor)}
>
<Plus className="h-3 w-3" />
</Button>
</div>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Badge
variant={vendor.isActive ? "default" : "destructive"}
className={vendor.isActive ? "bg-green-600 hover:bg-green-700" : ""}
>
{vendor.isActive ? "active" : "suspended"}
{vendor.isActive ? "Active" : "Suspended"}
</Badge>
{vendor.isAdmin && (
<Badge variant="secondary" className="text-xs">
<Badge variant="outline" className="border-blue-200 text-blue-700 bg-blue-50 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-900">
<Shield className="h-3 w-3 mr-1" />
Admin
</Badge>
)}
</div>
</TableCell>
<TableCell>
<TableCell className="text-sm text-muted-foreground">
<div className="flex items-center gap-1.5">
<Calendar className="h-3.5 w-3.5 opacity-70" />
{vendor.createdAt ? new Date(vendor.createdAt).toLocaleDateString() : 'N/A'}
</div>
</TableCell>
<TableCell>
<TableCell className="text-sm text-muted-foreground">
<div className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 opacity-70" />
{vendor.lastLogin ? new Date(vendor.lastLogin).toLocaleDateString() : 'Never'}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end space-x-2">
<Button variant="outline" size="sm">
<UserCheck className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<UserX className="h-4 w-4" />
</Button>
<Button variant="outline" size="sm">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(vendor._id)}
>
Copy Vendor ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className={vendor.isActive ? "text-red-600" : "text-green-600"}
onClick={() => handleToggleStatus(vendor)}
>
{vendor.isActive ? (
<>
<UserX className="mr-2 h-4 w-4" />
Suspend Vendor
</>
) : (
<>
<UserCheck className="mr-2 h-4 w-4" />
Activate Vendor
</>
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</motion.tr>
))
)
) : (
<AnimatePresence mode="popLayout">
{loading ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center">
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
<Loader2 className="h-8 w-8 animate-spin opacity-25" />
<p>Loading vendors...</p>
</div>
</TableCell>
</TableRow>
))}
) : filteredVendors.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="h-32 text-center text-muted-foreground">
{searchQuery.trim() ? "No vendors found matching your search" : "No vendors found"}
</TableCell>
</TableRow>
) : (
filteredVendors.map((vendor, index) => (
<motion.tr
key={vendor._id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
>
<TableCell>
<div className="font-medium flex items-center gap-2">
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-xs">
{vendor.username.substring(0, 2).toUpperCase()}
</div>
{vendor.username}
</div>
</TableCell>
<TableCell>
{vendor.storeId ? (
<div className="flex items-center gap-2 group/store">
<span className="font-mono text-xs">{vendor.storeId}</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 opacity-0 group-hover/store:opacity-100 transition-opacity"
onClick={() => handleEditStore(vendor)}
>
<Pencil className="h-3 w-3" />
</Button>
</div>
) : (
<div className="flex items-center gap-2">
<span className="text-muted-foreground italic text-xs">No store</span>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-primary"
onClick={() => handleEditStore(vendor)}
>
<Plus className="h-3 w-3" />
</Button>
</div>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Badge
variant={vendor.isActive ? "default" : "destructive"}
className={vendor.isActive ? "bg-green-600 hover:bg-green-700" : ""}
>
{vendor.isActive ? "Active" : "Suspended"}
</Badge>
{vendor.isAdmin && (
<Badge variant="outline" className="border-blue-200 text-blue-700 bg-blue-50 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-900">
<Shield className="h-3 w-3 mr-1" />
Admin
</Badge>
)}
</div>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
<div className="flex items-center gap-1.5">
<Calendar className="h-3.5 w-3.5 opacity-70" />
{vendor.createdAt ? new Date(vendor.createdAt).toLocaleDateString() : 'N/A'}
</div>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
<div className="flex items-center gap-1.5">
<Clock className="h-3.5 w-3.5 opacity-70" />
{vendor.lastLogin ? new Date(vendor.lastLogin).toLocaleDateString() : 'Never'}
</div>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(vendor._id)}
>
Copy Vendor ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className={vendor.isActive ? "text-red-600" : "text-green-600"}
onClick={() => handleToggleStatus(vendor)}
>
{vendor.isActive ? (
<>
<UserX className="mr-2 h-4 w-4" />
Suspend Vendor
</>
) : (
<>
<UserCheck className="mr-2 h-4 w-4" />
Activate Vendor
</>
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</motion.tr>
))
)}
</AnimatePresence>
)}
</TableBody>
</Table>
</div>
{pagination && pagination.totalPages > 1 && (
<div className="mt-4 flex items-center justify-between text-sm text-muted-foreground">
<div className="mt-4 flex items-center justify-between border-t border-border/50 pt-4 text-sm text-muted-foreground">
<span>
Page {pagination.page} of {pagination.totalPages} ({pagination.total} total)
Page <span className="font-medium text-foreground">{pagination.page}</span> of {pagination.totalPages}
</span>
<div className="flex gap-2">
<Button
@@ -232,6 +517,7 @@ export default function AdminVendorsPage() {
size="sm"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={!pagination.hasPrevPage || loading}
className="h-8"
>
Previous
</Button>
@@ -240,16 +526,50 @@ export default function AdminVendorsPage() {
size="sm"
onClick={() => setPage(p => p + 1)}
disabled={!pagination.hasNextPage || loading}
className="h-8"
>
Next
</Button>
</div>
</div>
)}
</>
)}
</CardContent>
</Card>
<Dialog open={isEditStoreOpen} onOpenChange={setIsEditStoreOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Update Vendor Store</DialogTitle>
<DialogDescription>
Enter the Store ID to assign to vendor <span className="font-semibold text-foreground">{editingVendor?.username}</span>.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="storeId">Store ID</Label>
<Input
id="storeId"
value={newStoreId}
onChange={(e) => setNewStoreId(e.target.value)}
placeholder="Enter 24-character Store ID"
className="col-span-3 font-mono"
/>
<p className="text-xs text-muted-foreground">
Ensure the Store ID corresponds to an existing store in the system.
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditStoreOpen(false)} disabled={updating}>Cancel</Button>
<Button onClick={saveStoreId} disabled={updating || !newStoreId || newStoreId.length < 24}>
{updating && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div >
);
}

View File

@@ -1,5 +1,5 @@
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/common/card";
import Layout from "@/components/layout/layout";
import { SnowLoader } from "@/components/snow-loader";

View File

@@ -5,7 +5,7 @@ import Dashboard from "@/components/dashboard/dashboard";
import AnalyticsDashboard from '@/components/analytics/AnalyticsDashboard';
import AnalyticsDashboardSkeleton from '@/components/analytics/AnalyticsDashboardSkeleton';
import StoreSelector from '@/components/analytics/StoreSelector';
import { getAnalyticsOverviewServer } from '@/lib/server-api';
import { getAnalyticsOverviewServer } from '@/lib/api/server-api';
import { fetchServer } from '@/lib/api';
import { performance } from 'perf_hooks';
import { Info, GitCommit, User, Zap, BarChart3 } from 'lucide-react';
@@ -103,7 +103,7 @@ export default async function AnalyticsPage({
// If it's a 401/403 error, redirect to login
if (error instanceof Error && error.message.includes('401')) {
redirect('/login');
redirect('/auth/login');
}
// If it's a 400 error (missing storeId for staff), show store selector

View File

@@ -2,8 +2,8 @@
import { useState } from "react";
import Dashboard from "@/components/dashboard/dashboard";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/common/card";
import { Button } from "@/components/common/button";
import { Wallet, Bitcoin, Coins, DollarSign, ArrowUpRight } from "lucide-react";
import { toast } from "sonner";
@@ -166,3 +166,4 @@ export default function BalancePage() {
);
}

View File

@@ -2,8 +2,8 @@
import { useState, useEffect, useRef } from "react";
import Layout from "@/components/layout/layout";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Plus, Pencil, Trash2, ChevronRight, ChevronDown, MoveVertical, FolderTree } from "lucide-react";
import { toast } from "sonner";
import {
@@ -12,7 +12,7 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
} from "@/components/common/select";
import {
AlertDialog,
AlertDialogAction,
@@ -22,10 +22,11 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
} from "@/components/common/alert-dialog";
import { apiRequest } from "@/lib/api";
import type { Category } from "@/models/categories";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { Category } from "@/lib/models/categories";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/common/card";
import { motion, AnimatePresence } from "framer-motion";
// Drag and Drop imports
import { DndProvider, useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
@@ -49,6 +50,7 @@ export default function CategoriesPage() {
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
const [categoryToDelete, setCategoryToDelete] = useState<Category | null>(null);
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
const [loading, setLoading] = useState(true);
// Get root categories sorted by order
const rootCategories = categories
@@ -67,10 +69,13 @@ export default function CategoriesPage() {
const fetchCategories = async () => {
try {
setLoading(true);
const fetchedCategories = await apiRequest("/categories", "GET");
setCategories(fetchedCategories);
} catch (error) {
toast.error("Failed to fetch categories");
} finally {
setLoading(false);
}
};
@@ -249,30 +254,38 @@ export default function CategoriesPage() {
drag(drop(ref));
return (
<div key={category._id} className="space-y-1">
<motion.div
key={category._id}
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="space-y-1"
>
<div
ref={ref}
className={`group flex items-center p-2 rounded-md transition-colors
${isEditing ? 'bg-gray-100 dark:bg-gray-800' : ''}
${isOver ? 'bg-gray-100 dark:bg-gray-800' : 'hover:bg-gray-100 dark:hover:bg-gray-800'}
${isDragging ? 'opacity-50' : 'opacity-100'}`}
className={`group flex items-center p-3 rounded-xl transition-all duration-200 border mb-2
${isEditing ? 'bg-indigo-500/10 border-indigo-500/30' : ''}
${isOver ? 'bg-indigo-500/20 border-indigo-500/50 scale-[1.02]' : 'bg-black/40 border-white/5 hover:bg-black/60 hover:border-white/10 hover:shadow-lg'}
${isDragging ? 'opacity-30' : 'opacity-100'} backdrop-blur-sm`}
style={{ marginLeft: `${level * 24}px` }}
data-handler-id={handlerId}
>
<div className="cursor-grab mr-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<div className="cursor-grab mr-2 text-muted-foreground/40 hover:text-muted-foreground transition-colors">
<MoveVertical className="h-4 w-4" />
</div>
{hasSubcategories && (
{hasSubcategories ? (
<button
onClick={() => toggleExpand(category._id)}
className="mr-1 focus:outline-none"
className="mr-1 focus:outline-none p-0.5 rounded-sm hover:bg-muted text-muted-foreground transition-colors"
>
{isExpanded ?
<ChevronDown className="h-4 w-4 text-muted-foreground" /> :
<ChevronRight className="h-4 w-4 text-muted-foreground" />
<ChevronDown className="h-4 w-4" /> :
<ChevronRight className="h-4 w-4" />
}
</button>
) : (
<div className="w-6 h-5" /> // Spacer
)}
<div className="flex-1 flex items-center space-x-2">
@@ -280,7 +293,7 @@ export default function CategoriesPage() {
<Input
value={editingCategory?.name || ""}
onChange={(e) => setEditingCategory(prev => prev ? { ...prev, name: e.target.value } : prev)}
className="h-8 max-w-[200px]"
className="h-8 max-w-[200px] border-primary/30 focus-visible:ring-primary/20"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && editingCategory) {
@@ -300,7 +313,7 @@ export default function CategoriesPage() {
<Button
size="sm"
variant="ghost"
className="h-8 px-2 text-green-600 hover:text-green-700 hover:bg-green-50"
className="h-8 px-2 text-green-600 hover:text-green-700 hover:bg-green-500/10"
onClick={() => editingCategory && handleUpdateCategory(category._id, editingCategory.name)}
>
Save
@@ -317,83 +330,107 @@ export default function CategoriesPage() {
) : (
<>
<Button
size="sm"
size="icon"
variant="ghost"
className="h-8 w-8 p-0 text-blue-500 hover:text-blue-600 hover:bg-blue-50"
className="h-8 w-8 text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
onClick={() => setEditingCategory(category)}
>
<Pencil className="h-4 w-4" />
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button
size="sm"
size="icon"
variant="ghost"
className="h-8 w-8 p-0 text-red-500 hover:text-red-600 hover:bg-red-50"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
onClick={() => setCategoryToDelete(category)}
>
<Trash2 className="h-4 w-4" />
<Trash2 className="h-3.5 w-3.5" />
</Button>
</>
)}
</div>
</div>
{isExpanded && subcategories.map(subcat =>
<AnimatePresence>
{isExpanded && hasSubcategories && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
{subcategories.map(subcat =>
<CategoryItem key={subcat._id} category={subcat} level={level + 1} />
)}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};
return (
<Layout>
<div className="space-y-6">
<div className="space-y-8 animate-in fade-in duration-500">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white flex items-center">
<FolderTree className="mr-2 h-6 w-6" />
<div>
<h1 className="text-2xl font-semibold text-foreground flex items-center">
<FolderTree className="mr-3 h-6 w-6 text-primary" />
Categories
</h1>
<p className="text-muted-foreground text-sm mt-1">
Manage your product categories and hierarchy
</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 xl:grid-cols-5 gap-4 lg:gap-6">
{/* Add Category Card - Takes up 2 columns */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="text-lg font-medium">Add New Category</CardTitle>
<div className="grid grid-cols-1 lg:grid-cols-3 xl:grid-cols-5 gap-6 lg:gap-8">
{/* Add Category Card */}
<Card className="lg:col-span-2 border-white/10 bg-black/40 backdrop-blur-xl shadow-xl h-fit sticky top-6 rounded-xl overflow-hidden">
<CardHeader className="bg-white/[0.02] border-b border-white/5 pb-4">
<CardTitle className="text-lg font-bold flex items-center text-white">
<div className="p-2 mr-3 rounded-lg bg-indigo-500/10 border border-indigo-500/20">
<Plus className="h-4 w-4 text-indigo-400" />
</div>
Add New Category
</CardTitle>
<CardDescription className="text-zinc-400">
Create a new category or subcategory
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="pt-6">
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
<label className="text-sm font-medium leading-none text-zinc-300">
Category Name
</label>
<Input
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
placeholder="Enter category name"
className="h-9"
placeholder="e.g. Electronics, Clothing..."
className="h-10 border-white/10 bg-black/20 focus:bg-black/40 transition-colors text-white placeholder:text-zinc-600"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
<label className="text-sm font-medium leading-none text-zinc-300">
Parent Category
</label>
<Select
value={selectedParentId || "none"}
onValueChange={setSelectedParentId}
>
<SelectTrigger className="h-9">
<SelectTrigger className="h-10 border-white/10 bg-black/20 focus:bg-black/40 transition-colors text-white">
<SelectValue placeholder="Select parent category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No parent (root category)</SelectItem>
<SelectContent className="bg-zinc-900 border-white/10 text-white">
<SelectItem value="none" className="focus:bg-zinc-800">No parent (root category)</SelectItem>
{categories.map((cat) => (
<SelectItem key={cat._id} value={cat._id}>
<SelectItem key={cat._id} value={cat._id} className="focus:bg-zinc-800">
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button onClick={handleAddCategory} className="w-full">
<Button onClick={handleAddCategory} className="w-full mt-2 bg-indigo-600 hover:bg-indigo-700 text-white border-0" size="lg">
<Plus className="h-4 w-4 mr-2" />
Add Category
</Button>
@@ -401,22 +438,34 @@ export default function CategoriesPage() {
</CardContent>
</Card>
{/* Category List Card - Takes up 3 columns */}
<Card className="lg:col-span-3">
<CardHeader>
<CardTitle className="text-lg font-medium">Category List</CardTitle>
{/* Category List Card */}
<Card className="lg:col-span-3 border-none bg-transparent shadow-none">
<CardHeader className="pl-0 pt-0 pb-4">
<CardTitle className="text-lg font-bold text-white">Structure</CardTitle>
<CardDescription className="text-zinc-400">
Drag and drop to reorder categories
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="p-0">
<DndProvider backend={HTML5Backend}>
<div className="space-y-1">
{rootCategories.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">
No categories yet. Add your first category above.
</p>
<div className="space-y-2 min-h-[300px]">
{loading ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground animate-pulse">
<FolderTree className="h-10 w-10 mb-3 opacity-20" />
<p>Loading categories...</p>
</div>
) : rootCategories.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground border-2 border-dashed border-border/40 rounded-xl bg-muted/20">
<FolderTree className="h-10 w-10 mb-3 opacity-20" />
<p>No categories yet</p>
<p className="text-xs opacity-60 mt-1">Add your first category to get started</p>
</div>
) : (
rootCategories.map(category => (
<div className="space-y-1">
{rootCategories.map(category => (
<CategoryItem key={category._id} category={category} />
))
))}
</div>
)}
</div>
</DndProvider>
@@ -425,24 +474,25 @@ export default function CategoriesPage() {
</div>
{/* Delete Confirmation Dialog */}
{categoryToDelete && (
<AlertDialog open={!!categoryToDelete} onOpenChange={() => setCategoryToDelete(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete the category "{categoryToDelete.name}".
This will permanently delete the category <span className="font-medium text-foreground">"{categoryToDelete?.name}"</span>.
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteConfirm}>Delete</AlertDialogAction>
<AlertDialogAction onClick={handleDeleteConfirm} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete Category
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
</Layout>
);
}

View File

@@ -1,6 +1,6 @@
import Layout from "@/components/layout/layout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card";
import { Skeleton } from "@/components/common/skeleton";
import { Loader2 } from "lucide-react";
import { SnowLoader } from "@/components/snow-loader";

View File

@@ -1,7 +1,7 @@
import React from "react";
import { Metadata } from "next";
import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton";
import { Skeleton } from "@/components/common/skeleton";
import Dashboard from "@/components/dashboard/dashboard";
export const metadata: Metadata = {

View File

@@ -1,7 +1,7 @@
import React from "react";
import { Metadata, Viewport } from "next";
import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton";
import { Skeleton } from "@/components/common/skeleton";
import Dashboard from "@/components/dashboard/dashboard";
export const metadata: Metadata = {

View File

@@ -5,10 +5,10 @@ import { useRouter } from "next/navigation";
import Dashboard from "@/components/dashboard/dashboard";
import { MessageCircle, AlertCircle, RefreshCw } from "lucide-react";
import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/common/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { Button } from "@/components/common/button";
// Error Boundary Component
interface ErrorBoundaryState {
@@ -163,19 +163,6 @@ function ChatTableSkeleton() {
}
export default function ChatsPage() {
const router = useRouter();
useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
}
}, [router]);
return (
<Dashboard>
<div className="space-y-6">

View File

@@ -1,11 +1,11 @@
"use client";
import { Component, ReactNode, useState, useEffect, Suspense } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { Button } from "@/components/common/button";
import { AlertCircle, RefreshCw } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/common/card";
// Error Boundary Component
interface ErrorBoundaryState {
@@ -260,3 +260,4 @@ export default function DashboardContentWrapper({ children }: { children: ReactN
);
}

View File

@@ -1,7 +1,7 @@
"use client"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Skeleton } from "@/components/common/skeleton";
import Layout from "@/components/layout/layout";
import { Loader2 } from "lucide-react";
import { SnowLoader } from "@/components/snow-loader";

View File

@@ -1,7 +1,7 @@
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card";
import { Skeleton } from "@/components/common/skeleton";
import Layout from "@/components/layout/layout";
import { Loader2 } from "lucide-react";
import { SnowLoader } from "@/components/snow-loader";

View File

@@ -4,10 +4,10 @@ import { fetchData } from '@/lib/api';
import { clientFetch } from '@/lib/api';
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Label } from "@/components/common/label";
import { Textarea } from "@/components/common/textarea";
import {
Table,
TableBody,
@@ -15,13 +15,13 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
} from "@/components/common/table";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
} from "@/components/common/card";
import { Clipboard, Truck, Package, ArrowRight, ChevronDown, AlertTriangle, Copy, Loader2, RefreshCw, MessageCircle } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -36,9 +36,11 @@ import {
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
} from "@/components/common/alert-dialog";
import Layout from "@/components/layout/layout";
import { cacheUtils } from '@/lib/api-client';
import { cacheUtils } from '@/lib/api/api-client';
import OrderTimeline from "@/components/orders/order-timeline";
import { motion, AnimatePresence } from "framer-motion";
interface Order {
orderId: string;
@@ -791,10 +793,26 @@ export default function OrderDetailsPage() {
</CardContent>
</Card>
)}
{/* Order Timeline */}
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader className="pb-0">
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Order Lifecycle</CardTitle>
</CardHeader>
<CardContent>
<OrderTimeline
status={order?.status || ''}
orderDate={order?.orderDate || ''}
paidAt={order?.paidAt}
/>
</CardContent>
</Card>
<div className="grid grid-cols-3 gap-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="grid grid-cols-3 gap-6"
>
{/* Left Column - Order Details */}
<div className="col-span-2 space-y-6">
{/* Products Card */}
@@ -1168,8 +1186,7 @@ export default function OrderDetailsPage() {
{[...Array(5)].map((_, i) => (
<svg
key={i}
className={`w-4 h-4 ${
i < (order?.review?.stars || 0)
className={`w-4 h-4 ${i < (order?.review?.stars || 0)
? "text-yellow-400"
: "text-zinc-600"
}`}
@@ -1198,10 +1215,10 @@ export default function OrderDetailsPage() {
</Card>
)}
</div>
</motion.div>
</div>
{/* Shipping Dialog removed; use inline tracking input above */}
</div>
</Layout>
);
}

View File

@@ -5,10 +5,10 @@ import { useRouter } from "next/navigation";
import Dashboard from "@/components/dashboard/dashboard";
import { Package, AlertCircle, RefreshCw } from "lucide-react";
import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/common/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { Button } from "@/components/common/button";
// Error Boundary Component
interface ErrorBoundaryState {
@@ -163,19 +163,6 @@ function OrderTableSkeleton() {
}
export default function OrdersPage() {
const router = useRouter();
useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
}
}, [router]);
return (
<Dashboard>
<div className="space-y-6">

View File

@@ -6,8 +6,8 @@ import packageJson from '../../package.json';
import { getGitInfo, getShortGitHash } from '@/lib/utils/git';
import { Suspense } from 'react';
import dynamic from 'next/dynamic';
import { Skeleton } from '@/components/ui/skeleton';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/common/skeleton';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/common/card';
import DashboardContentWrapper from './dashboard-content-wrapper';
// Loading skeleton for the dashboard content
@@ -183,3 +183,4 @@ export default async function DashboardPage() {
</Dashboard>
);
}

View File

@@ -3,16 +3,16 @@
import { useState, useEffect, ChangeEvent, Suspense } from "react";
import { useRouter } from "next/navigation";
import Layout from "@/components/layout/layout";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Product } from "@/models/products";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Product } from "@/lib/models/products";
import { Plus, Upload, Search, RefreshCw, Package2 } from "lucide-react";
import { clientFetch } from "@/lib/api";
import { Category } from "@/models/categories";
import { Category } from "@/lib/models/categories";
import { toast } from "sonner";
import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/common/card";
// Lazy load heavy components with error handling
const ProductTable = dynamic(() => import("@/components/tables/product-table").catch((err) => {
@@ -45,7 +45,7 @@ const ProfitAnalysisModal = dynamic(() => import("@/components/modals/profit-ana
function ProductTableSkeleton() {
return (
<Card className="animate-in fade-in duration-500">
<Card className="animate-in fade-in duration-500 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader>
<div className="flex items-center justify-between">
<Skeleton className="h-6 w-32" />
@@ -156,16 +156,6 @@ export default function ProductsPage() {
// Fetch products and categories
useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
return;
}
const fetchDataAsync = async () => {
try {
setLoading(true);
@@ -194,7 +184,7 @@ export default function ProductsPage() {
};
fetchDataAsync();
}, [router]);
}, []);
const handleAddTier = () => {
setProductData((prev) => ({
@@ -552,3 +542,5 @@ export default function ProductsPage() {
</Layout>
);
}

View File

@@ -4,8 +4,8 @@ import { useState, useEffect, ChangeEvent, Suspense } from "react";
import { useRouter } from "next/navigation";
import Layout from "@/components/layout/layout";
import { Edit, Plus, Trash, Truck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/common/button";
import { Skeleton } from "@/components/common/skeleton";
import {
fetchShippingMethods,
addShippingMethod,
@@ -15,7 +15,7 @@ import {
ShippingData
} from "@/lib/services/shipping-service";
import dynamic from "next/dynamic";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Card, CardContent, CardHeader } from "@/components/common/card";
// Lazy load components with error handling
const ShippingModal = dynamic(() => import("@/components/modals/shipping-modal").then(mod => ({ default: mod.ShippingModal })).catch((err) => {
@@ -54,7 +54,7 @@ const ShippingTable = dynamic(() => import("@/components/tables/shipping-table")
// Loading skeleton for shipping table
function ShippingTableSkeleton() {
return (
<Card className="animate-in fade-in duration-500 relative">
<Card className="animate-in fade-in duration-500 relative border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
{/* Subtle loading indicator */}
<div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden rounded-t-lg">
<div className="h-full bg-primary w-1/3"
@@ -142,11 +142,6 @@ export default function ShippingPage() {
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
return;
}
const fetchedMethods: ShippingMethod[] = await fetchShippingMethods(
authToken
);

View File

@@ -3,21 +3,25 @@
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Layout from "@/components/layout/layout";
import { Button } from "@/components/ui/button";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/common/button";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/common/table";
import { Input } from "@/components/common/input";
import { Switch } from "@/components/common/switch";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator
} from "@/components/common/dropdown-menu";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
} from "@/components/common/popover";
import {
AlertDialog,
AlertDialogAction,
@@ -28,14 +32,15 @@ import {
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Product } from "@/models/products";
import { Package, RefreshCw, ChevronDown, CheckSquare, XSquare, Boxes, Download, Calendar } from "lucide-react";
} from "@/components/common/alert-dialog";
import { Product } from "@/lib/models/products";
import { Package, RefreshCw, ChevronDown, CheckSquare, XSquare, Boxes, Download, Calendar, Search, Filter, Save, X, Edit2 } from "lucide-react";
import { clientFetch } from "@/lib/api";
import { toast } from "sonner";
import { DatePicker, DateRangePicker, DateRangeDisplay, MonthPicker } from "@/components/ui/date-picker";
import { DatePicker, DateRangePicker, DateRangeDisplay, MonthPicker } from "@/components/common/date-picker";
import { DateRange } from "react-day-picker";
import { addDays, startOfDay, endOfDay, format, isSameDay } from "date-fns";
import { motion, AnimatePresence } from "framer-motion";
interface StockData {
currentStock: number;
@@ -67,16 +72,6 @@ export default function StockManagementPage() {
const [isExporting, setIsExporting] = useState<boolean>(false);
useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
return;
}
const fetchDataAsync = async () => {
try {
const response = await clientFetch<Product[]>('api/products');
@@ -100,7 +95,7 @@ export default function StockManagementPage() {
};
fetchDataAsync();
}, [router]);
}, []);
const handleEditStock = (productId: string) => {
setEditingStock({
@@ -379,6 +374,26 @@ export default function StockManagementPage() {
return 'In stock';
};
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case 'Out of stock': return 'destructive';
case 'Low stock': return 'warning'; // Custom variant or use secondary/outline
case 'In stock': return 'default'; // often maps to primary which might be blue/black
default: return 'secondary';
}
};
// Helper for badging - if your Badge component doesn't support 'warning' directly, use className overrides
const StatusBadge = ({ status }: { status: string }) => {
let styles = "font-medium border-transparent shadow-none";
if (status === 'Out of stock') styles += " bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400";
else if (status === 'Low stock') styles += " bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400";
else if (status === 'In stock') styles += " bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400";
else styles += " bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400";
return <Badge className={styles} variant="outline">{status}</Badge>;
};
const filteredProducts = products.filter(product => {
if (!searchTerm) return true;
@@ -392,31 +407,39 @@ export default function StockManagementPage() {
return (
<Layout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white flex items-center">
<Boxes className="mr-2 h-6 w-6" />
<div className="space-y-6 animate-in fade-in duration-500">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold text-foreground flex items-center gap-2">
<Boxes className="h-6 w-6 text-primary" />
Stock Management
</h1>
<div className="flex items-center gap-3">
<p className="text-muted-foreground text-sm mt-1">
Track inventory levels and manage stock status
</p>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search products..."
className="w-64"
className="pl-9 w-full sm:w-64 bg-background/50 border-border/50 focus:bg-background transition-colors"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* Report Type Selector */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2">
<Calendar className="h-4 w-4" />
{reportType.charAt(0).toUpperCase() + reportType.slice(1)} Report
<ChevronDown className="h-4 w-4" />
<Button variant="outline" size="icon" className="h-10 w-10 border-border/50 bg-background/50">
<Filter className="h-4 w-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Filter Reports</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setReportType('daily')}>
Daily Report
</DropdownMenuItem>
@@ -433,12 +456,13 @@ export default function StockManagementPage() {
</DropdownMenu>
{/* Date Selection based on report type */}
<div className="hidden sm:block">
{reportType === 'daily' && (
<DatePicker
date={exportDate ? new Date(exportDate) : undefined}
onDateChange={(date) => setExportDate(date ? date.toISOString().split('T')[0] : '')}
placeholder="Select export date"
className="w-auto"
className="w-auto border-border/50 bg-background/50"
/>
)}
@@ -447,7 +471,7 @@ export default function StockManagementPage() {
dateRange={exportDateRange}
onDateRangeChange={setExportDateRange}
placeholder="Select date range"
className="w-auto"
className="w-auto border-border/50 bg-background/50"
/>
)}
@@ -456,28 +480,29 @@ export default function StockManagementPage() {
selectedMonth={selectedMonth}
onMonthChange={(date) => setSelectedMonth(date || new Date())}
placeholder="Select month"
className="w-auto"
className="w-auto border-border/50 bg-background/50"
/>
)}
</div>
<Button
variant="outline"
onClick={handleExportStock}
disabled={isExporting}
className="gap-2"
className="gap-2 border-border/50 bg-background/50 hover:bg-background transition-colors"
>
{isExporting ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
{isExporting ? 'Exporting...' : 'Export CSV'}
Export
</Button>
{selectedProducts.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2">
<Button variant="default" className="gap-2">
<Package className="h-4 w-4" />
Bulk Actions
<ChevronDown className="h-4 w-4" />
@@ -486,11 +511,11 @@ export default function StockManagementPage() {
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleBulkAction('enable')}>
<CheckSquare className="h-4 w-4 mr-2" />
Enable Stock Tracking
Enable Tracking
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleBulkAction('disable')}>
<XSquare className="h-4 w-4 mr-2" />
Disable Stock Tracking
Disable Tracking
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -498,52 +523,77 @@ export default function StockManagementPage() {
</div>
</div>
<div className="rounded-md border">
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader className="py-4 px-6 border-b border-border/50 bg-muted/30 flex flex-row items-center justify-between">
<div>
<CardTitle className="text-lg font-medium">Inventory Data</CardTitle>
<CardDescription>Manage stock levels and tracking for {products.length} products</CardDescription>
</div>
<div className="text-xs text-muted-foreground bg-background/50 px-3 py-1 rounded-full border border-border/50">
{filteredProducts.length} items
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<TableHeader className="bg-muted/50">
<TableRow className="border-border/50 hover:bg-transparent">
<TableHead className="w-12 pl-6">
<input
type="checkbox"
checked={selectedProducts.length === products.length}
checked={selectedProducts.length === products.length && products.length > 0}
onChange={toggleSelectAll}
className="rounded border-gray-300"
className="rounded border-gray-300 dark:border-zinc-700 bg-background focus:ring-primary/20"
/>
</TableHead>
<TableHead>Product</TableHead>
<TableHead>Stock Status</TableHead>
<TableHead>Status</TableHead>
<TableHead>Current Stock</TableHead>
<TableHead>Track Stock</TableHead>
<TableHead className="text-right">Actions</TableHead>
<TableHead>Tracking</TableHead>
<TableHead className="text-right pr-6">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<AnimatePresence mode="popLayout">
{loading ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8">
<RefreshCw className="h-6 w-6 animate-spin inline-block" />
<span className="ml-2">Loading products...</span>
<TableCell colSpan={6} className="text-center py-12">
<div className="flex flex-col items-center justify-center gap-3 text-muted-foreground">
<RefreshCw className="h-8 w-8 animate-spin opacity-20" />
<p>Loading products...</p>
</div>
</TableCell>
</TableRow>
) : filteredProducts.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8">
No products found
<TableCell colSpan={6} className="text-center py-12">
<div className="flex flex-col items-center justify-center gap-3 text-muted-foreground">
<Boxes className="h-10 w-10 opacity-20" />
<p>No products found matching your search</p>
</div>
</TableCell>
</TableRow>
) : (
filteredProducts.map((product) => (
<TableRow key={product._id}>
<TableCell>
filteredProducts.map((product, index) => (
<motion.tr
key={product._id}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
>
<TableCell className="pl-6">
<input
type="checkbox"
checked={selectedProducts.includes(product._id || '')}
onChange={() => toggleSelectProduct(product._id || '')}
className="rounded border-gray-300"
className="rounded border-gray-300 dark:border-zinc-700 bg-background focus:ring-primary/20"
/>
</TableCell>
<TableCell>{product.name}</TableCell>
<TableCell>{getStockStatus(product)}</TableCell>
<TableCell className="font-medium">{product.name}</TableCell>
<TableCell>
<StatusBadge status={getStockStatus(product)} />
</TableCell>
<TableCell>
{editingStock[product._id || ''] ? (
<div className="flex items-center gap-2">
@@ -551,37 +601,62 @@ export default function StockManagementPage() {
type="number"
value={stockValues[product._id || ''] || 0}
onChange={(e) => handleStockChange(product._id || '', parseInt(e.target.value) || 0)}
className="w-24"
className="w-20 h-8 font-mono bg-background"
/>
<Button size="sm" onClick={() => handleSaveStock(product)}>Save</Button>
</div>
) : (
<span>{product.currentStock || 0}</span>
<span className="font-mono text-sm">{product.currentStock || 0}</span>
)}
</TableCell>
<TableCell>
<Switch
checked={product.stockTracking || false}
onCheckedChange={() => handleToggleStockTracking(product)}
className="data-[state=checked]:bg-primary"
/>
</TableCell>
<TableCell className="text-right">
{!editingStock[product._id || ''] && (
<TableCell className="text-right pr-6">
<div className="flex justify-end gap-1">
{editingStock[product._id || ''] ? (
<>
<Button
variant="outline"
size="sm"
size="icon"
variant="ghost"
className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-100 dark:hover:bg-green-900/20"
onClick={() => handleSaveStock(product)}
>
<Save className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => setEditingStock({ ...editingStock, [product._id || '']: false })}
>
<X className="h-4 w-4" />
</Button>
</>
) : (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-primary hover:bg-primary/10"
onClick={() => handleEditStock(product._id || '')}
>
Edit Stock
<Edit2 className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
</motion.tr>
))
)}
</AnimatePresence>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
<AlertDialog open={isConfirmDialogOpen} onOpenChange={setIsConfirmDialogOpen}>
@@ -589,15 +664,18 @@ export default function StockManagementPage() {
<AlertDialogHeader>
<AlertDialogTitle>Confirm Bulk Action</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to {bulkAction} stock tracking for {selectedProducts.length} selected products?
Are you sure you want to {bulkAction} stock tracking for <span className="font-medium text-foreground">{selectedProducts.length}</span> selected products?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setBulkAction(null)}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={executeBulkAction}>Continue</AlertDialogAction>
<AlertDialogAction onClick={executeBulkAction} className="bg-primary text-primary-foreground">
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Layout>
);
}

View File

@@ -2,7 +2,7 @@
import React, { useEffect, useState, useCallback } from "react";
import { getCustomers, type CustomerStats } from "@/lib/api";
import { formatCurrency } from "@/utils/format";
import { formatCurrency } from "@/lib/utils/format";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import Layout from "@/components/layout/layout";
@@ -13,14 +13,14 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
} from "@/components/common/table";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
} from "@/components/common/select";
import {
Dialog,
DialogContent,
@@ -28,8 +28,8 @@ import {
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
} from "@/components/common/dialog";
import { Button } from "@/components/common/button";
import {
ChevronLeft,
ChevronRight,
@@ -40,21 +40,35 @@ import {
UserPlus,
MoreHorizontal,
Search,
X
X,
CreditCard,
Calendar,
ShoppingBag,
Truck,
CheckCircle,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/common/badge";
import { Input } from "@/components/common/input";
import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/common/card";
import { motion, AnimatePresence } from "framer-motion";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
} from "@/components/common/dropdown-menu";
export default function CustomerManagementPage() {
const router = useRouter();
const [customers, setCustomers] = useState<CustomerStats[]>([]);
// State for browser detection
const [isFirefox, setIsFirefox] = useState(false);
useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
setIsFirefox(ua.includes("firefox") && !ua.includes("chrome"));
}, []);
const [filteredCustomers, setFilteredCustomers] = useState<CustomerStats[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(true);
@@ -112,16 +126,6 @@ export default function CustomerManagementPage() {
fetchCustomers();
}, [fetchCustomers]);
useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
}
}, [router]);
// Add filter function to filter customers when search query changes
useEffect(() => {
@@ -184,13 +188,13 @@ export default function CustomerManagementPage() {
</h1>
</div>
<div className="bg-black/40 border border-zinc-800 rounded-md overflow-hidden">
<div className="p-4 border-b border-zinc-800 bg-black/60 flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<div className="p-4 border-b border-border/50 bg-muted/30 flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="text-sm font-medium text-gray-400">Show:</div>
<div className="text-sm font-medium text-muted-foreground">Show:</div>
<Select value={itemsPerPage.toString()} onValueChange={handleItemsPerPageChange}>
<SelectTrigger className="w-[70px]">
<SelectTrigger className="w-[70px] bg-background/50 border-border/50">
<SelectValue placeholder="25" />
</SelectTrigger>
<SelectContent>
@@ -204,26 +208,26 @@ export default function CustomerManagementPage() {
<div className="relative flex-1 max-w-md">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<Search className="h-4 w-4 text-gray-400" />
<Search className="h-4 w-4 text-muted-foreground" />
</div>
<Input
type="text"
placeholder="Search by username or Telegram ID..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-10 py-2 w-full bg-black/40 border-zinc-700 text-white"
className="pl-10 pr-10 py-2 w-full bg-background/50 border-border/50 focus:ring-primary/20 transition-all duration-300"
/>
{searchQuery && (
<button
className="absolute inset-y-0 right-0 flex items-center pr-3"
onClick={clearSearch}
>
<X className="h-4 w-4 text-gray-400 hover:text-gray-200" />
<X className="h-4 w-4 text-muted-foreground hover:text-foreground" />
</button>
)}
</div>
<div className="text-sm text-gray-400 whitespace-nowrap">
<div className="text-sm text-muted-foreground whitespace-nowrap">
{loading
? "Loading..."
: searchQuery
@@ -232,31 +236,25 @@ export default function CustomerManagementPage() {
</div>
</div>
<CardContent className="p-0">
{loading ? (
<div className="p-8 bg-black/60">
<div className="p-8">
{/* Loading indicator */}
<div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden">
<div className="h-full bg-primary w-1/3"
<div className="absolute top-[69px] left-0 right-0 h-0.5 bg-muted overflow-hidden">
<div className="h-full bg-primary w-1/3 animate-shimmer"
style={{
background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)',
backgroundSize: '200% 100%',
animation: 'shimmer 2s ease-in-out infinite',
}}
/>
</div>
{/* Table skeleton */}
<div className="space-y-4">
<div className="flex items-center gap-4 pb-4 border-b border-zinc-800">
<div className="flex items-center gap-4 pb-4 border-b border-border/50">
{['Customer', 'Orders', 'Total Spent', 'Last Order', 'Status'].map((header, i) => (
<Skeleton
key={i}
className="h-4 w-20 flex-1 animate-in fade-in"
style={{
animationDelay: `${i * 50}ms`,
animationDuration: '300ms',
animationFillMode: 'both',
}}
className="h-4 w-20 flex-1"
/>
))}
</div>
@@ -264,12 +262,7 @@ export default function CustomerManagementPage() {
{[...Array(5)].map((_, i) => (
<div
key={i}
className="flex items-center gap-4 pb-4 border-b border-zinc-800 last:border-b-0 animate-in fade-in"
style={{
animationDelay: `${250 + i * 50}ms`,
animationDuration: '300ms',
animationFillMode: 'both',
}}
className="flex items-center gap-4 pb-4 border-b border-border/50 last:border-b-0"
>
<div className="flex items-center gap-3 flex-1">
<Skeleton className="h-10 w-10 rounded-full" />
@@ -287,114 +280,219 @@ export default function CustomerManagementPage() {
</div>
</div>
) : filteredCustomers.length === 0 ? (
<div className="p-8 text-center bg-black/60">
<Users className="h-12 w-12 mx-auto text-gray-500 mb-4" />
<h3 className="text-lg font-medium mb-2 text-white">
{searchQuery ? "No customers matching your search" : "No customers found"}
<div className="p-12 text-center">
<div className="w-16 h-16 bg-muted/50 rounded-full flex items-center justify-center mx-auto mb-4">
<Users className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium mb-2 text-foreground">
{searchQuery ? "No matching customers" : "No customers yet"}
</h3>
<p className="text-gray-500">
<p className="text-muted-foreground max-w-sm mx-auto mb-6">
{searchQuery
? "Try a different search term or clear the search"
? "We couldn't find any customers matching your search criteria."
: "Once you have customers placing orders, they will appear here."}
</p>
{searchQuery && (
<Button variant="outline" size="sm" onClick={clearSearch} className="mt-4">
Clear search
<Button variant="outline" size="sm" onClick={clearSearch}>
Clear Search
</Button>
)}
</div>
) : (
<div className="overflow-x-auto">
<Table className="[&_tr]:border-b [&_tr]:border-zinc-800 [&_tr:last-child]:border-b-0 [&_td]:border-r [&_td]:border-zinc-800 [&_td:last-child]:border-r-0 [&_th]:border-r [&_th]:border-zinc-800 [&_th:last-child]:border-r-0 [&_tr:hover]:bg-zinc-900/70">
<TableHeader className="bg-black/60 sticky top-0 z-10">
<TableRow>
<TableHead className="w-[180px] text-gray-300">Customer</TableHead>
<Table>
<TableHeader className="bg-muted/50">
<TableRow className="hover:bg-transparent border-border/50">
<TableHead className="w-[200px]">Customer</TableHead>
<TableHead
className="cursor-pointer w-[100px] text-gray-300 text-center"
className="cursor-pointer w-[100px] text-center hover:text-primary transition-colors"
onClick={() => handleSort("totalOrders")}
>
<div className="flex items-center justify-center">
<div className="flex items-center justify-center gap-1">
Orders
<ArrowUpDown className="ml-2 h-4 w-4" />
<ArrowUpDown className="h-3 w-3" />
</div>
</TableHead>
<TableHead
className="cursor-pointer w-[150px] text-gray-300 text-center"
className="cursor-pointer w-[150px] text-center hover:text-primary transition-colors"
onClick={() => handleSort("totalSpent")}
>
<div className="flex items-center justify-center">
<div className="flex items-center justify-center gap-1">
Total Spent
<ArrowUpDown className="ml-2 h-4 w-4" />
<ArrowUpDown className="h-3 w-3" />
</div>
</TableHead>
<TableHead
className="cursor-pointer w-[180px] text-gray-300 text-center"
className="cursor-pointer w-[180px] text-center hover:text-primary transition-colors"
onClick={() => handleSort("lastOrderDate")}
>
<div className="flex items-center justify-center">
<div className="flex items-center justify-center gap-1">
Last Order
<ArrowUpDown className="ml-2 h-4 w-4" />
<ArrowUpDown className="h-3 w-3" />
</div>
</TableHead>
<TableHead className="w-[250px] text-gray-300 text-center">Status</TableHead>
<TableHead className="w-[250px] text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCustomers.map((customer) => (
<TableRow
{isFirefox ? (
filteredCustomers.map((customer, index) => (
<motion.tr
key={customer.userId}
className={`cursor-pointer ${!customer.hasOrders ? "bg-black/30" : ""}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
className={`cursor-pointer group hover:bg-muted/50 border-b border-border/50 transition-colors ${!customer.hasOrders ? "bg-primary/5 hover:bg-primary/10" : ""}`}
onClick={() => setSelectedCustomer(customer)}
>
<TableCell>
<div className="font-medium text-gray-100">
<TableCell className="py-3">
<div className="flex items-center gap-3">
<div className={`h-9 w-9 rounded-full flex items-center justify-center text-xs font-medium ${!customer.hasOrders ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground'
}`}>
{customer.telegramUsername ? customer.telegramUsername.substring(0, 2).toUpperCase() : 'ID'}
</div>
<div>
<div className="font-medium flex items-center gap-2">
@{customer.telegramUsername || "Unknown"}
{!customer.hasOrders && (
<Badge variant="outline" className="ml-2 bg-purple-900/30 text-purple-300 border-purple-700">
<UserPlus className="h-3 w-3 mr-1" />
<Badge variant="outline" className="h-5 px-1.5 text-[10px] bg-primary/10 text-primary border-primary/20">
New
</Badge>
)}
</div>
<div className="text-sm text-gray-400">ID: {customer.telegramUserId}</div>
<div className="text-xs text-muted-foreground flex items-center font-mono mt-0.5">
<span className="opacity-50 select-none">ID:</span>
<span className="ml-1">{customer.telegramUserId}</span>
</div>
</div>
</div>
</TableCell>
<TableCell className="text-center">
<Badge className="bg-gray-700 text-white hover:bg-gray-600">{customer.totalOrders}</Badge>
<Badge variant="secondary" className="font-mono font-normal">
{customer.totalOrders}
</Badge>
</TableCell>
<TableCell className="font-medium text-gray-100 text-center">
<TableCell className="text-center font-mono text-sm">
{formatCurrency(customer.totalSpent)}
</TableCell>
<TableCell className="text-sm text-gray-100 text-center">
{customer.lastOrderDate ? formatDate(customer.lastOrderDate) : "N/A"}
<TableCell className="text-center text-sm text-muted-foreground">
{customer.lastOrderDate ? (
<div className="flex items-center justify-center gap-1.5">
<Calendar className="h-3 w-3 opacity-70" />
{formatDate(customer.lastOrderDate).split(",")[0]}
</div>
) : "Never"}
</TableCell>
<TableCell className="text-center">
{customer.hasOrders ? (
<div className="flex justify-center space-x-1">
<Badge className="bg-blue-500 text-white hover:bg-blue-600">
<div className="flex justify-center flex-wrap gap-1">
{customer.ordersByStatus.paid > 0 && (
<Badge className="bg-blue-500/15 text-blue-500 hover:bg-blue-500/25 border-blue-500/20 h-5 px-1.5 text-[10px]">
{customer.ordersByStatus.paid} Paid
</Badge>
<Badge className="bg-green-500 text-white hover:bg-green-600">
{customer.ordersByStatus.completed} Completed
</Badge>
<Badge className="bg-amber-500 text-white hover:bg-amber-600">
{customer.ordersByStatus.shipped} Shipped
</Badge>
</div>
) : (
<Badge variant="outline" className="bg-gray-800 text-gray-300 border-gray-700">
No orders yet
)}
{customer.ordersByStatus.completed > 0 && (
<Badge className="bg-emerald-500/15 text-emerald-500 hover:bg-emerald-500/25 border-emerald-500/20 h-5 px-1.5 text-[10px]">
{customer.ordersByStatus.completed} Done
</Badge>
)}
{customer.ordersByStatus.shipped > 0 && (
<Badge className="bg-amber-500/15 text-amber-500 hover:bg-amber-500/25 border-amber-500/20 h-5 px-1.5 text-[10px]">
{customer.ordersByStatus.shipped} Ship
</Badge>
)}
</div>
) : (
<span className="text-xs text-muted-foreground italic">No activity</span>
)}
</TableCell>
</TableRow>
</motion.tr>
))
) : (
<AnimatePresence mode="popLayout">
{filteredCustomers.map((customer, index) => (
<motion.tr
key={customer.userId}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
className={`cursor-pointer group hover:bg-muted/50 border-b border-border/50 transition-colors ${!customer.hasOrders ? "bg-primary/5 hover:bg-primary/10" : ""}`}
onClick={() => setSelectedCustomer(customer)}
>
<TableCell className="py-3">
<div className="flex items-center gap-3">
<div className={`h-9 w-9 rounded-full flex items-center justify-center text-xs font-medium ${!customer.hasOrders ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground'
}`}>
{customer.telegramUsername ? customer.telegramUsername.substring(0, 2).toUpperCase() : 'ID'}
</div>
<div>
<div className="font-medium flex items-center gap-2">
@{customer.telegramUsername || "Unknown"}
{!customer.hasOrders && (
<Badge variant="outline" className="h-5 px-1.5 text-[10px] bg-primary/10 text-primary border-primary/20">
New
</Badge>
)}
</div>
<div className="text-xs text-muted-foreground flex items-center font-mono mt-0.5">
<span className="opacity-50 select-none">ID:</span>
<span className="ml-1">{customer.telegramUserId}</span>
</div>
</div>
</div>
</TableCell>
<TableCell className="text-center">
<Badge variant="secondary" className="font-mono font-normal">
{customer.totalOrders}
</Badge>
</TableCell>
<TableCell className="text-center font-mono text-sm">
{formatCurrency(customer.totalSpent)}
</TableCell>
<TableCell className="text-center text-sm text-muted-foreground">
{customer.lastOrderDate ? (
<div className="flex items-center justify-center gap-1.5">
<Calendar className="h-3 w-3 opacity-70" />
{formatDate(customer.lastOrderDate).split(",")[0]}
</div>
) : "Never"}
</TableCell>
<TableCell className="text-center">
{customer.hasOrders ? (
<div className="flex justify-center flex-wrap gap-1">
{customer.ordersByStatus.paid > 0 && (
<Badge className="bg-blue-500/15 text-blue-500 hover:bg-blue-500/25 border-blue-500/20 h-5 px-1.5 text-[10px]">
{customer.ordersByStatus.paid} Paid
</Badge>
)}
{customer.ordersByStatus.completed > 0 && (
<Badge className="bg-emerald-500/15 text-emerald-500 hover:bg-emerald-500/25 border-emerald-500/20 h-5 px-1.5 text-[10px]">
{customer.ordersByStatus.completed} Done
</Badge>
)}
{customer.ordersByStatus.shipped > 0 && (
<Badge className="bg-amber-500/15 text-amber-500 hover:bg-amber-500/25 border-amber-500/20 h-5 px-1.5 text-[10px]">
{customer.ordersByStatus.shipped} Ship
</Badge>
)}
</div>
) : (
<span className="text-xs text-muted-foreground italic">No activity</span>
)}
</TableCell>
</motion.tr>
))}
</AnimatePresence>
)}
</TableBody>
</Table>
</div>
)}
</CardContent>
<div className="p-4 border-t border-zinc-800 bg-black/40 flex justify-between items-center">
<div className="text-sm text-gray-400">
<div className="p-4 border-t border-border/50 bg-background/50 flex justify-between items-center">
<div className="text-sm text-muted-foreground">
Page {page} of {totalPages}
</div>
<div className="flex gap-2">
@@ -403,74 +501,100 @@ export default function CustomerManagementPage() {
size="sm"
onClick={() => handlePageChange(Math.max(1, page - 1))}
disabled={page === 1 || loading}
className="h-8"
>
<ChevronLeft className="h-4 w-4 mr-1" />
<ChevronLeft className="h-3 w-3 mr-1" />
Previous
</Button>
{totalPages > 2 && (
{totalPages > 2 ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<span className="sr-only">Go to page</span>
<MoreHorizontal className="h-4 w-4" />
<Button variant="outline" size="sm" className="h-8 px-2">
<MoreHorizontal className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="center" className="bg-black/90 border-zinc-800 max-h-60 overflow-y-auto">
<DropdownMenuContent align="center" className="max-h-60 overflow-y-auto">
{Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => (
<DropdownMenuItem
key={pageNum}
onClick={() => handlePageChange(pageNum)}
className={`cursor-pointer ${pageNum === page ? 'bg-zinc-800 text-white' : 'text-gray-300'}`}
className={pageNum === page ? 'bg-primary/10 text-primary' : ''}
>
Page {pageNum}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
) : null}
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(Math.min(totalPages, page + 1))}
disabled={page === totalPages || loading}
className="h-8"
>
Next
<ChevronRight className="h-4 w-4 ml-1" />
<ChevronRight className="h-3 w-3 ml-1" />
</Button>
</div>
</div>
</div>
</Card>
{/* Customer Details Dialog */}
<AnimatePresence>
{selectedCustomer && (
<Dialog open={!!selectedCustomer} onOpenChange={(open) => !open && setSelectedCustomer(null)}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl w-full overflow-y-auto max-h-[90vh] z-[80]">
<DialogHeader>
<DialogTitle className="text-base">
Customer Details
<DialogTitle className="text-lg flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-lg">
{selectedCustomer.telegramUsername ? selectedCustomer.telegramUsername.substring(0, 1).toUpperCase() : 'U'}
</div>
<div>
<div className="font-bold">Customer Details</div>
<div className="text-sm font-normal text-muted-foreground flex items-center gap-2">
@{selectedCustomer.telegramUsername || "Unknown"}
<span className="w-1 h-1 rounded-full bg-primary" />
<span className="font-mono text-xs opacity-70">ID: {selectedCustomer.telegramUserId}</span>
</div>
</div>
</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 py-4">
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Customer Information */}
<div>
<div className="mb-4">
<h3 className="text-sm font-medium mb-2">Customer Information</h3>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="space-y-4"
>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">Contact Info</h3>
<div className="rounded-xl border border-border p-4 space-y-4">
<div className="space-y-3">
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Username:</div>
<div className="font-medium">@{selectedCustomer.telegramUsername || "Unknown"}</div>
<div className="flex justify-between items-center text-sm group">
<div className="text-muted-foreground flex items-center gap-2">
<Users className="h-4 w-4 opacity-50" />
Username
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Telegram ID:</div>
<div className="font-medium">{selectedCustomer.telegramUserId}</div>
<div className="font-medium group-hover:text-primary transition-colors">@{selectedCustomer.telegramUsername || "Unknown"}</div>
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Chat ID:</div>
<div className="font-medium">{selectedCustomer.chatId}</div>
<div className="flex justify-between items-center text-sm group">
<div className="text-muted-foreground flex items-center gap-2">
<CreditCard className="h-4 w-4 opacity-50" />
User ID
</div>
<div className="font-medium font-mono">{selectedCustomer.telegramUserId}</div>
</div>
<div className="flex justify-between items-center text-sm group">
<div className="text-muted-foreground flex items-center gap-2">
<MessageCircle className="h-4 w-4 opacity-50" />
Chat ID
</div>
<div className="font-medium font-mono">{selectedCustomer.chatId}</div>
</div>
</div>
@@ -486,76 +610,101 @@ export default function CustomerManagementPage() {
Open Telegram Chat
</Button>
</div>
</motion.div>
{/* Order Statistics */}
<div>
<h3 className="text-sm font-medium mb-2">Order Statistics</h3>
<div className="bg-background rounded-lg border border-border p-4 space-y-3">
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Total Orders:</div>
<div className="font-medium">{selectedCustomer.totalOrders}</div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="space-y-4"
>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">Lifetime Stats</h3>
<div className="rounded-xl border border-border p-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-emerald-500/10 rounded-lg p-3 border border-emerald-500/20 min-w-0">
<div className="text-xs text-emerald-400/70 uppercase font-medium mb-1 whitespace-nowrap">Total Spent</div>
<div className="text-xl font-bold text-emerald-400 truncate">{formatCurrency(selectedCustomer.totalSpent)}</div>
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Total Spent:</div>
<div className="font-medium">{formatCurrency(selectedCustomer.totalSpent)}</div>
<div className="bg-blue-500/10 rounded-lg p-3 border border-blue-500/20 min-w-0">
<div className="text-xs text-blue-400/70 uppercase font-medium mb-1 whitespace-nowrap">Total Orders</div>
<div className="text-xl font-bold text-blue-400">{selectedCustomer.totalOrders}</div>
</div>
</div>
<div className="space-y-2 pt-2 border-t border-white/5">
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">First Order:</div>
<div className="font-medium">
<div className="text-muted-foreground text-xs">First Order</div>
<div className="font-medium text-white/70 text-xs bg-white/5 px-2 py-1 rounded">
{formatDate(selectedCustomer.firstOrderDate)}
</div>
</div>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">Last Order:</div>
<div className="font-medium">
<div className="text-muted-foreground text-xs">Last Activity</div>
<div className="font-medium text-white/70 text-xs bg-white/5 px-2 py-1 rounded">
{formatDate(selectedCustomer.lastOrderDate)}
</div>
</div>
</div>
</div>
</motion.div>
</div>
{/* Order Status Breakdown */}
<div className="mb-4">
<h3 className="text-sm font-medium mb-2">Order Status Breakdown</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="bg-blue-500/10 rounded-lg border border-blue-500/20 p-3">
<p className="text-sm text-muted-foreground">Paid</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.paid}</p>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="space-y-4"
>
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider pl-1">Order History Breakdown</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div className="bg-blue-500/5 hover:bg-blue-500/10 transition-colors rounded-xl border border-blue-500/20 p-4 text-center group">
<ShoppingBag className="h-5 w-5 text-blue-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.paid}</p>
<p className="text-xs font-medium text-blue-400/70 uppercase">Paid</p>
</div>
<div className="bg-purple-500/10 rounded-lg border border-purple-500/20 p-3">
<p className="text-sm text-muted-foreground">Acknowledged</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.acknowledged}</p>
<div className="bg-purple-500/5 hover:bg-purple-500/10 transition-colors rounded-xl border border-purple-500/20 p-4 text-center group">
<Loader2 className="h-5 w-5 text-purple-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.acknowledged}</p>
<p className="text-xs font-medium text-purple-400/70 uppercase">Processing</p>
</div>
<div className="bg-amber-500/10 rounded-lg border border-amber-500/20 p-3">
<p className="text-sm text-muted-foreground">Shipped</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.shipped}</p>
<div className="bg-amber-500/5 hover:bg-amber-500/10 transition-colors rounded-xl border border-amber-500/20 p-4 text-center group">
<Truck className="h-5 w-5 text-amber-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.shipped}</p>
<p className="text-xs font-medium text-amber-400/70 uppercase">Shipped</p>
</div>
<div className="bg-green-500/10 rounded-lg border border-green-500/20 p-3">
<p className="text-sm text-muted-foreground">Completed</p>
<p className="text-xl font-semibold">{selectedCustomer.ordersByStatus.completed}</p>
<div className="bg-emerald-500/5 hover:bg-emerald-500/10 transition-colors rounded-xl border border-emerald-500/20 p-4 text-center group">
<CheckCircle className="h-5 w-5 text-emerald-500 mx-auto mb-2 opacity-70 group-hover:opacity-100 transition-opacity" />
<p className="text-2xl font-bold text-white mb-1">{selectedCustomer.ordersByStatus.completed}</p>
<p className="text-xs font-medium text-emerald-400/70 uppercase">Completed</p>
</div>
</div>
</motion.div>
</div>
<DialogFooter>
<DialogFooter className="pt-4 border-t">
<Button
variant="outline"
variant="ghost"
onClick={() => setSelectedCustomer(null)}
className=""
>
Close
Close Profile
</Button>
<Button
onClick={() => router.push(`/dashboard/storefront/chat/new?userId=${selectedCustomer.telegramUserId}`)}
className=""
>
<MessageCircle className="h-4 w-4 mr-2" />
Start Chat
Message Customer
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</AnimatePresence>
</div>
</Layout>
);
}

View File

@@ -3,23 +3,27 @@
import { useState, useEffect, ChangeEvent } from "react";
import { useRouter } from "next/navigation";
import Layout from "@/components/layout/layout";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Save, Send, Key, MessageSquare, Shield, Globe, Wallet } from "lucide-react";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Textarea } from "@/components/common/textarea";
import { Save, Send, Key, MessageSquare, Shield, Globe, Wallet, RefreshCw } from "lucide-react";
import { apiRequest } from "@/lib/api";
import { toast } from "sonner";
import BroadcastDialog from "@/components/modals/broadcast-dialog";
import Dashboard from "@/components/dashboard/dashboard";
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/common/card";
import { Label } from "@/components/common/label";
import { Badge } from "@/components/common/badge";
import { motion, AnimatePresence } from "framer-motion";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
} from "@/components/common/select";
import { Switch } from "@/components/common/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/common/tooltip";
const SHIPPING_REGIONS = [
{ value: "UK", label: "United Kingdom", emoji: "🇬🇧" },
@@ -102,16 +106,6 @@ export default function StorefrontPage() {
const [saving, setSaving] = useState<boolean>(false);
useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
return;
}
const fetchStorefront = async () => {
try {
setLoading(true);
@@ -166,17 +160,61 @@ export default function StorefrontPage() {
return (
<Dashboard>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white flex items-center">
<Globe className="mr-2 h-6 w-6" />
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<div className="p-3 rounded-xl bg-primary/10 text-primary">
<Globe className="h-8 w-8" />
</div>
<div>
<h1 className="text-3xl font-bold tracking-tight text-foreground">
Storefront Settings
</h1>
<div className="flex items-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<p className="text-muted-foreground">
Manage your shop's appearance, policies, and configuration
</p>
</div>
</div>
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => setBroadcastOpen(true)}
className="gap-2 h-10"
>
<Send className="h-4 w-4" />
Broadcast
</Button>
<Button
onClick={saveStorefront}
disabled={saving}
className="gap-2 h-10 min-w-[120px]"
>
{saving ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
{saving ? "Saving..." : "Save Changes"}
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Column */}
<div className="lg:col-span-2 space-y-6">
{/* Store Status Card */}
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }}>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden relative">
<div className={`absolute top-0 left-0 w-1 h-full ${storefront.isEnabled ? 'bg-emerald-500' : 'bg-destructive'}`} />
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="space-y-1">
<CardTitle>Store Status</CardTitle>
<CardDescription>Control your store's visibility to customers</CardDescription>
</div>
<Badge variant={storefront.isEnabled ? "default" : "destructive"} className="h-6">
{storefront.isEnabled ? "Open for Business" : "Store Closed"}
</Badge>
</div>
</CardHeader>
<CardContent className="pb-6">
<div className="flex items-center gap-4 p-4 rounded-lg bg-card border border-border/50">
<Switch
checked={storefront.isEnabled}
onCheckedChange={(checked) =>
@@ -185,183 +223,190 @@ export default function StorefrontPage() {
isEnabled: checked,
}))
}
className="data-[state=checked]:bg-emerald-500"
/>
<span className={`text-sm font-medium ${storefront.isEnabled ? 'text-emerald-400' : 'text-zinc-400'}`}>
{storefront.isEnabled ? 'Store Open' : 'Store Closed'}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{storefront.isEnabled ? 'Click to close store' : 'Click to open store'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div>
<h4 className="font-medium text-sm">
{storefront.isEnabled ? 'Your store is currently online' : 'Your store is currently offline'}
</h4>
<p className="text-xs text-muted-foreground">
{storefront.isEnabled
? 'Customers can browse listings and place orders normally.'
: 'Customers will see a maintenance page. No new orders can be placed.'}
</p>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setBroadcastOpen(true)}
className="gap-2"
size="sm"
>
<Send className="h-4 w-4" />
Broadcast
</Button>
<Button
onClick={saveStorefront}
disabled={saving}
className="gap-2"
size="sm"
>
<Save className="h-4 w-4" />
{saving ? "Saving..." : "Save Changes"}
</Button>
</div>
</CardContent>
</Card>
</motion.div>
{/* Welcome & Policy */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }}>
<Card className="h-full border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<MessageSquare className="h-4 w-4 text-primary" />
Welcome Message
</CardTitle>
</CardHeader>
<CardContent>
<Textarea
value={storefront.welcomeMessage}
onChange={(e) => setStorefront(prev => ({ ...prev, welcomeMessage: e.target.value }))}
placeholder="Enter the welcome message for new customers..."
className="min-h-[180px] bg-background/50 border-border/50 resize-none focus:ring-primary/20"
/>
</CardContent>
</Card>
</motion.div>
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }}>
<Card className="h-full border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Shield className="h-4 w-4 text-orange-400" />
Store Policy
</CardTitle>
</CardHeader>
<CardContent>
<Textarea
value={storefront.storePolicy}
onChange={(e) => setStorefront(prev => ({ ...prev, storePolicy: e.target.value }))}
placeholder="Enter your store's policies, terms, and conditions..."
className="min-h-[180px] bg-background/50 border-border/50 resize-none focus:ring-primary/20"
/>
</CardContent>
</Card>
</motion.div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6">
{/* Security Settings */}
<div className="space-y-3">
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
<div className="flex items-center gap-2 mb-3">
<Shield className="h-4 w-4 text-purple-400" />
<h2 className="text-base font-medium text-zinc-100">
Security
</h2>
</div>
<div className="space-y-3">
<div>
<label className="text-xs font-medium mb-1 block text-zinc-400">PGP Public Key</label>
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }}>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Key className="h-5 w-5 text-purple-400" />
Security Configuration
</CardTitle>
<CardDescription>Manage keys and access tokens for your store security</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 gap-4">
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">PGP Public Key</Label>
<Textarea
value={storefront.pgpKey}
onChange={(e) => setStorefront(prev => ({ ...prev, pgpKey: e.target.value }))}
placeholder="Enter your PGP public key"
className="font-mono text-sm h-24 bg-[#1C1C1C] border-zinc-800 resize-none"
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----..."
className="font-mono text-xs h-32 bg-zinc-950/50 border-zinc-800/50 resize-none"
/>
</div>
<div>
<label className="text-xs font-medium mb-1 block text-zinc-400">Telegram Bot Token</label>
<div className="space-y-2">
<Label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Telegram Bot Token</Label>
<div className="relative">
<Input
type="password"
value={storefront.telegramToken}
onChange={(e) => setStorefront(prev => ({ ...prev, telegramToken: e.target.value }))}
placeholder="Enter your Telegram bot token"
className="bg-[#1C1C1C] border-zinc-800 h-8 text-sm"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
className="bg-background/50 border-border/50 font-mono text-sm pl-10"
/>
<div className="absolute left-3 top-2.5 text-muted-foreground">
<Shield className="h-4 w-4" />
</div>
</div>
<p className="text-[10px] text-muted-foreground">Used for notifications and bot integration.</p>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</div>
{/* Sidebar Column */}
<div className="space-y-6">
{/* Shipping Settings */}
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
<div className="flex items-center gap-2 mb-3">
<motion.div initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.4 }}>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Globe className="h-4 w-4 text-blue-400" />
<h2 className="text-base font-medium text-zinc-100">
Shipping
</h2>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs font-medium mb-1 block text-zinc-400">Ships From</label>
Shipping & Logistics
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Ships From</Label>
<Select
value={storefront.shipsFrom}
onValueChange={(value) =>
setStorefront((prev) => ({ ...prev, shipsFrom: value as any }))
}
>
<SelectTrigger className="bg-[#1C1C1C] border-zinc-800 h-8 text-sm">
<SelectTrigger className="bg-background/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SHIPPING_REGIONS.map((region) => (
<SelectItem key={region.value} value={region.value}>
{region.emoji} {region.label}
<span className="flex items-center gap-2">
<span className="text-lg">{region.emoji}</span>
{region.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-xs font-medium mb-1 block text-zinc-400">Ships To</label>
<div className="space-y-2">
<Label>Ships To</Label>
<Select
value={storefront.shipsTo}
onValueChange={(value) =>
setStorefront((prev) => ({ ...prev, shipsTo: value as any }))
}
>
<SelectTrigger className="bg-[#1C1C1C] border-zinc-800 h-8 text-sm">
<SelectTrigger className="bg-background/50 border-border/50">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SHIPPING_REGIONS.map((region) => (
<SelectItem key={region.value} value={region.value}>
{region.emoji} {region.label}
<span className="flex items-center gap-2">
<span className="text-lg">{region.emoji}</span>
{region.label}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
{/* Messaging and Payments */}
<div className="space-y-3">
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
<div className="flex items-center gap-2 mb-3">
<MessageSquare className="h-4 w-4 text-emerald-400" />
<h2 className="text-base font-medium text-zinc-100">
Welcome Message
</h2>
</div>
<Textarea
value={storefront.welcomeMessage}
onChange={(e) => setStorefront(prev => ({ ...prev, welcomeMessage: e.target.value }))}
placeholder="Enter the welcome message for new customers"
className="h-36 bg-[#1C1C1C] border-zinc-800 text-sm resize-none"
/>
</div>
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
<div className="flex items-center gap-2 mb-3">
<Shield className="h-4 w-4 text-orange-400" />
<h2 className="text-base font-medium text-zinc-100">
Store Policy
</h2>
</div>
<Textarea
value={storefront.storePolicy}
onChange={(e) => setStorefront(prev => ({ ...prev, storePolicy: e.target.value }))}
placeholder="Enter your store's policies, terms, and conditions"
className="h-48 bg-[#1C1C1C] border-zinc-800 text-sm resize-none"
/>
</div>
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
<div className="flex items-center gap-2 mb-3">
<Wallet className="h-4 w-4 text-yellow-400" />
<h2 className="text-base font-medium text-zinc-100">
Payment Methods
</h2>
</div>
<div className="space-y-2">
{/* Payment Methods */}
<motion.div initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.5 }}>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Wallet className="h-4 w-4 text-yellow-500" />
Crypto Wallets
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{WALLET_OPTIONS.map((wallet) => (
<div key={wallet.id} className="space-y-1.5">
<div className="flex items-center justify-between">
<label className="text-xs font-medium flex items-center gap-2 text-zinc-400">
<span>{wallet.emoji}</span>
<div key={wallet.id} className="p-3 rounded-lg border border-border/50 bg-card/30 hover:bg-card/50 transition-colors">
<div className="flex items-center justify-between mb-3">
<label className="text-sm font-medium flex items-center gap-2">
<span className="text-lg">{wallet.emoji}</span>
{wallet.name}
{wallet.comingSoon && (
<span className="text-[10px] bg-purple-900/50 text-purple-400 px-1.5 py-0.5 rounded">
Coming Soon
</span>
)}
</label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<div className="flex items-center gap-2">
{wallet.comingSoon && (
<Badge variant="secondary" className="text-[10px] h-5">Soon</Badge>
)}
<Switch
checked={storefront.enabledWallets[wallet.id]}
onCheckedChange={(checked) =>
@@ -374,18 +419,12 @@ export default function StorefrontPage() {
}))
}
disabled={wallet.disabled}
className="scale-90"
/>
</div>
</TooltipTrigger>
{wallet.disabled && (
<TooltipContent>
<p>Coming soon</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
{storefront.enabledWallets[wallet.id] && !wallet.disabled && (
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }}>
<Input
value={storefront.wallets[wallet.id]}
onChange={(e) =>
@@ -398,13 +437,15 @@ export default function StorefrontPage() {
}))
}
placeholder={wallet.placeholder}
className="font-mono text-sm h-8 bg-[#1C1C1C] border-zinc-800"
className="font-mono text-xs h-9 bg-background/50"
/>
</motion.div>
)}
</div>
))}
</div>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</div>
@@ -414,3 +455,5 @@ export default function StorefrontPage() {
</Dashboard >
);
}

View File

@@ -16,6 +16,7 @@ body {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
@@ -61,7 +62,10 @@ body {
}
/* Larger touch targets for interactive elements */
button, input, textarea, [role="button"] {
button,
input,
textarea,
[role="button"] {
min-height: 44px;
}
}
@@ -90,7 +94,9 @@ body {
}
/* Improved focus visibility */
input:focus, textarea:focus, button:focus {
input:focus,
textarea:focus,
button:focus {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
}
@@ -102,6 +108,7 @@ body {
/* Chromebook-specific optimizations */
@media screen and (max-width: 1366px) and (min-resolution: 1.5dppx) {
/* Chromebook display optimizations */
.text-sm {
font-size: 0.875rem;
@@ -114,7 +121,11 @@ body {
}
/* Better touch targets for Chromebooks */
button, input, textarea, [role="button"], [role="tab"] {
button,
input,
textarea,
[role="button"],
[role="tab"] {
min-height: 48px;
min-width: 48px;
}
@@ -131,6 +142,7 @@ body {
/* Chromebook touch screen optimizations */
@media (pointer: coarse) and (hover: none) {
/* Larger touch targets */
.touch-target {
min-height: 52px;
@@ -148,13 +160,16 @@ body {
}
/* Better input field sizing */
input, textarea {
input,
textarea {
padding: 0.875rem;
font-size: 1rem;
}
/* Enhanced focus states for touch */
button:focus-visible, input:focus-visible, textarea:focus-visible {
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 3px solid hsl(var(--ring));
outline-offset: 2px;
}
@@ -162,6 +177,7 @@ body {
/* Chromebook keyboard navigation improvements */
@media (hover: hover) and (pointer: fine) {
/* Better hover states for mouse/trackpad */
button:hover:not(:disabled) {
transform: translateY(-1px);
@@ -169,7 +185,9 @@ body {
}
/* Improved focus indicators */
button:focus-visible, input:focus-visible, textarea:focus-visible {
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
box-shadow: 0 0 0 4px hsl(var(--ring) / 0.2);
@@ -178,6 +196,7 @@ body {
/* Chromebook display scaling fixes */
@media screen and (min-resolution: 1.5dppx) {
/* Prevent text from being too small on high-DPI displays */
html {
-webkit-text-size-adjust: 100%;
@@ -254,8 +273,17 @@ body {
/* Christmas-themed animations */
@keyframes twinkle {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.3; transform: scale(0.8); }
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(0.8);
}
}
@keyframes snowflake {
@@ -263,6 +291,7 @@ body {
transform: translateY(-100vh) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(360deg);
opacity: 0;
@@ -270,10 +299,13 @@ body {
}
@keyframes sparkle {
0%, 100% {
0%,
100% {
opacity: 0;
transform: scale(0) rotate(0deg);
}
50% {
opacity: 1;
transform: scale(1) rotate(180deg);
@@ -281,9 +313,12 @@ body {
}
@keyframes glow {
0%, 100% {
0%,
100% {
box-shadow: 0 0 5px hsl(var(--christmas-red)), 0 0 10px hsl(var(--christmas-red)), 0 0 15px hsl(var(--christmas-red));
}
50% {
box-shadow: 0 0 10px hsl(var(--christmas-green)), 0 0 20px hsl(var(--christmas-green)), 0 0 30px hsl(var(--christmas-green));
}
@@ -376,6 +411,42 @@ body {
.christmas-theme *:focus-visible {
outline-color: hsl(var(--christmas-red));
}
/* Premium UI Utilities */
.glass-morphism {
@apply bg-background/60 backdrop-blur-md border border-border/50;
}
.dark .glass-morphism {
@apply bg-black/40 backdrop-blur-xl border-white/5;
}
.premium-card {
@apply transition-all duration-300;
}
.premium-card:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
border-color: hsl(var(--primary) / 0.2);
}
.dark .premium-card {
@apply bg-card;
}
.dark .premium-card:hover {
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
border-color: hsl(var(--primary) / 0.2);
}
.text-gradient {
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/60;
}
.bg-gradient-premium {
background: radial-gradient(circle at top left, hsl(var(--primary) / 0.05), transparent),
radial-gradient(circle at bottom right, hsl(var(--primary) / 0.02), transparent);
}
}
@layer base {
@@ -410,26 +481,27 @@ body {
--christmas-green: 142 76% 36%;
--christmas-gold: 43 96% 56%;
}
.dark {
--background: 0 0% 3.9%;
--background: 240 10% 2%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card: 240 10% 3%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover: 240 10% 2%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary: 240 4% 10%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--muted: 240 4% 10%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 4% 10%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--border: 240 4% 12%;
--input: 240 4% 12%;
--ring: 240 5% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
@@ -464,6 +536,7 @@ body {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}

View File

@@ -1,6 +1,6 @@
import { Metadata, Viewport } from "next";
import Link from "next/link";
import { buttonVariants } from "@/components/ui/button";
import { buttonVariants } from "@/components/common/button";
import { ArrowLeft } from "lucide-react";
export const metadata: Metadata = {
@@ -38,3 +38,4 @@ export default function NotFound() {
</div>
);
}

View File

@@ -1,77 +1,43 @@
import { getPlatformStatsServer } from "@/lib/server-api";
import { HomeNavbar } from "@/components/home-navbar";
import { Suspense } from "react";
import { Shield, LineChart, Zap, ArrowRight, CheckCircle2, Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import { getPlatformStatsServer } from "@/lib/api/server-api";
import { HomeNavbar } from "@/components/layout/home-navbar";
import { Shield, LineChart, Zap, ArrowRight, Sparkles } from "lucide-react";
import { Button } from "@/components/common/button";
import Link from "next/link";
import { AnimatedStatsSection } from "@/components/animated-stats-section";
import { isDecember } from "@/lib/utils/christmas";
import { MotionWrapper } from "@/components/ui/motion-wrapper";
import { MotionWrapper } from "@/components/common/motion-wrapper";
export const dynamic = 'force-dynamic';
const PY_20 = 20;
const PY_32 = 32;
const PX_6 = 6;
const PX_10 = 10;
function formatNumberValue(num: number): string {
return new Intl.NumberFormat().format(Math.round(num));
}
// Format currency
function formatCurrencyValue(amount: number): string {
return new Intl.NumberFormat('en-GB', {
style: 'currency',
currency: 'GBP',
maximumFractionDigits: 0
}).format(amount);
}
// This is a server component
export default async function Home() {
try {
const stats = await getPlatformStatsServer();
const isDec = isDecember();
return (
<div className={`flex flex-col min-h-screen bg-black text-white ${isDec ? 'christmas-theme' : ''}`}>
<div className={`absolute inset-0 bg-gradient-to-br pointer-events-none scale-100 ${isDec
? 'from-red-500/10 via-green-500/5 to-transparent'
: 'from-[#D53F8C]/10 via-[#D53F8C]/3 to-transparent'
}`} />
<div className="relative flex flex-col min-h-screen bg-black text-white">
<div className="absolute inset-0 bg-gradient-to-br from-indigo-500/10 via-purple-500/5 to-transparent pointer-events-none scale-100" />
<HomeNavbar />
{/* Hero Section */}
<section className="relative overflow-hidden">
<div className="relative flex flex-col items-center px-4 py-20 md:py-32 mx-auto max-w-7xl">
<div className="flex flex-col items-center text-center space-y-6 max-w-3xl">
<div className={`inline-flex items-center px-4 py-2 rounded-full border mb-4 ${isDec
? 'bg-red-500/10 border-red-500/20'
: 'bg-[#D53F8C]/10 border-[#D53F8C]/20'
}`}>
<Sparkles className={`h-4 w-4 mr-2 ${isDec ? 'text-red-400' : 'text-[#D53F8C]'}`} />
<span className={`text-sm font-medium ${isDec ? 'text-red-400' : 'text-[#D53F8C]'}`}>
<div className="inline-flex items-center px-4 py-2 rounded-full border border-indigo-500/20 bg-indigo-500/10 mb-4">
<Sparkles className="h-4 w-4 mr-2 text-indigo-400" />
<span className="text-sm font-medium text-indigo-400">
Secure Crypto Payments
</span>
</div>
<h1 className="text-4xl md:text-6xl font-bold tracking-tight">
The Future of <span className={isDec ? 'text-red-400' : 'text-[#D53F8C]'}>E-commerce</span> Management
The Future of <span className="text-indigo-500">E-commerce</span> Management
</h1>
<p className="text-lg md:text-xl text-zinc-400 max-w-2xl">
{isDec
? 'Spread joy this holiday season with our all-in-one platform. Secure payments, order tracking, and analytics wrapped up in one beautiful package. 🎄'
: 'Streamline your online business with our all-in-one platform. Secure payments, order tracking, and analytics in one place.'
}
Streamline your online business with our all-in-one platform. Secure payments, order tracking, and analytics in one place.
</p>
<div className="flex flex-col sm:flex-row gap-4 mt-4">
<Link href="/dashboard">
<Button
size="lg"
className={`gap-2 text-white border-0 h-12 px-8 ${isDec
? 'bg-gradient-to-r from-red-500 to-green-500 hover:from-red-600 hover:to-green-600'
: 'bg-[#D53F8C] hover:bg-[#B83280]'
}`}
className="gap-2 text-white border-0 h-12 px-8 bg-indigo-600 hover:bg-indigo-700"
>
Get Started
<ArrowRight className="h-4 w-4" />
@@ -103,35 +69,21 @@ export default async function Home() {
title: "Lightning Fast",
description: "Optimized for speed with real-time updates and instant notifications."
}
].map((feature, i) => {
const christmasColors = ['from-red-500/5', 'from-green-500/5', 'from-yellow-500/5'];
const christmasBorders = ['border-red-500/30', 'border-green-500/30', 'border-yellow-500/30'];
const christmasIcons = ['text-red-400', 'text-green-400', 'text-yellow-400'];
const christmasBgs = ['bg-red-500/10', 'bg-green-500/10', 'bg-yellow-500/10'];
return (
].map((feature, i) => (
<div
key={i}
className={`group relative overflow-hidden rounded-xl bg-gradient-to-b p-6 border transition-all duration-300 hover:scale-[1.02] hover:shadow-lg ${isDec
? `from-zinc-800/30 to-transparent ${christmasBorders[i % 3]}`
: 'from-zinc-800/30 to-transparent border-zinc-800'
}`}
className="group relative overflow-hidden rounded-xl bg-gradient-to-b from-zinc-800/30 to-transparent p-6 border border-zinc-800 transition-all duration-300 hover:scale-[1.02] hover:shadow-lg"
>
<div
className={`absolute inset-0 bg-gradient-to-b to-transparent opacity-0 group-hover:opacity-100 transition-opacity ${isDec ? christmasColors[i % 3] : 'from-[#D53F8C]/5'
}`}
/>
<div className="absolute inset-0 bg-gradient-to-b from-indigo-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative">
<div className={`h-12 w-12 flex items-center justify-center rounded-lg mb-4 ${isDec ? christmasBgs[i % 3] : 'bg-[#D53F8C]/10'
}`}>
<feature.icon className={`h-6 w-6 ${isDec ? christmasIcons[i % 3] : 'text-[#D53F8C]'}`} />
<div className="h-12 w-12 flex items-center justify-center rounded-lg mb-4 bg-indigo-500/10">
<feature.icon className="h-6 w-6 text-indigo-500" />
</div>
<h3 className="text-lg font-semibold text-white mb-2">{feature.title}</h3>
<p className="text-sm text-zinc-400">{feature.description}</p>
</div>
</div>
);
})}
))}
</div>
</MotionWrapper>
@@ -145,13 +97,6 @@ export default async function Home() {
{/* Footer */}
<footer className="relative py-12 px-4 mt-auto">
<div className="max-w-7xl mx-auto flex flex-col items-center">
{isDec && (
<div className="flex items-center gap-2 mb-4 text-red-400 animate-twinkle">
<span className="text-xl">🎄</span>
<span className="text-sm font-medium">Happy Holidays from da ember team!</span>
<span className="text-xl">🎄</span>
</div>
)}
<div className="text-sm text-zinc-500">
© {new Date().getFullYear()} Ember. All rights reserved.
</div>
@@ -164,3 +109,6 @@ export default async function Home() {
return <div>Error loading page</div>;
}
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useKeepOnline } from "@/hooks/useKeepOnline";
import { useKeepOnline } from "@/lib/hooks/useKeepOnline";
const KeepOnline = () => {
useKeepOnline({

View File

@@ -7,16 +7,16 @@ import {
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
} from "@/components/common/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/common/tabs";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
} from "@/components/common/select";
import { Button } from "@/components/common/button";
import {
AlertCircle,
BarChart,
@@ -29,8 +29,8 @@ import {
Package,
Trophy,
} from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { fetchClient } from "@/lib/api-client";
import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { fetchClient } from "@/lib/api/api-client";
import {
BarChart as RechartsBarChart,
Bar,
@@ -43,7 +43,7 @@ import {
Line,
ComposedChart,
} from "recharts";
import { formatGBP } from "@/utils/format";
import { formatGBP, formatNumber } from "@/lib/utils/format";
import { PieChart, Pie, Cell, Legend } from "recharts";
interface GrowthData {
@@ -519,39 +519,39 @@ export default function AdminAnalytics() {
const bestMonth = calculateBestMonth();
return (
<div className="space-y-6">
<div className="space-y-8 animate-in fade-in duration-500">
{errorMessage && (
<Alert variant="destructive">
<Alert variant="destructive" className="animate-in slide-in-from-top-2 border-destructive/50 bg-destructive/10 text-destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{errorMessage}</AlertDescription>
</Alert>
)}
<div className="flex justify-between items-center">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold">
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent">
Dashboard Analytics
{!isViewingCurrentYear && (
<span className="ml-2 text-lg font-normal text-muted-foreground">
<span className="ml-2 text-xl font-normal text-muted-foreground/60">
({selectedYear})
</span>
)}
</h2>
<p className="text-muted-foreground">
<p className="text-muted-foreground mt-1">
{isViewingCurrentYear
? "Overview of your marketplace performance"
: `Historical data for ${selectedYear}`}
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 bg-background/40 p-1 rounded-lg border border-border/40 backdrop-blur-md">
{/* Year selector */}
<Select
value={selectedYear.toString()}
onValueChange={(value) => setSelectedYear(parseInt(value, 10))}
>
<SelectTrigger className="w-[100px]">
<SelectTrigger className="w-[100px] border-0 bg-transparent focus:ring-0">
<SelectValue placeholder="Year" />
</SelectTrigger>
<SelectContent>
@@ -563,13 +563,15 @@ export default function AdminAnalytics() {
</SelectContent>
</Select>
<div className="h-4 w-px bg-border/40" />
{/* Date range selector - only show options for current year */}
<Select
value={isViewingCurrentYear ? dateRange : "year"}
onValueChange={setDateRange}
disabled={!isViewingCurrentYear}
>
<SelectTrigger className="w-[140px]">
<SelectTrigger className="w-[140px] border-0 bg-transparent focus:ring-0">
<SelectValue placeholder="Last 7 days" />
</SelectTrigger>
<SelectContent>
@@ -578,6 +580,8 @@ export default function AdminAnalytics() {
<SelectItem value="24hours">Last 24 hours</SelectItem>
<SelectItem value="7days">Last 7 days</SelectItem>
<SelectItem value="30days">Last 30 days</SelectItem>
<SelectItem value="90days">Last 90 days</SelectItem>
<SelectItem value="180days">Last 180 days</SelectItem>
<SelectItem value="ytd">Year to Date</SelectItem>
<SelectItem value="year">Full Year</SelectItem>
</>
@@ -587,11 +591,14 @@ export default function AdminAnalytics() {
</SelectContent>
</Select>
<div className="h-4 w-px bg-border/40" />
<Button
variant="outline"
variant="ghost"
size="icon"
onClick={handleRefresh}
disabled={refreshing}
className="h-8 w-8 hover:bg-background/60"
>
<RefreshCw
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
@@ -599,9 +606,10 @@ export default function AdminAnalytics() {
</Button>
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={() => setShowDebug(!showDebug)}
className="px-2 text-xs hover:bg-background/60"
>
{showDebug ? "Hide" : "Show"} Debug
</Button>
@@ -609,9 +617,9 @@ export default function AdminAnalytics() {
</div>
{showDebug && analyticsData && (
<Card className="mt-4">
<Card className="mt-4 border-yellow-500/20 bg-yellow-500/5 backdrop-blur-sm">
<CardHeader>
<CardTitle>Debug: Raw Data</CardTitle>
<CardTitle className="text-yellow-600">Debug: Raw Data</CardTitle>
<CardDescription>Date Range: {dateRange}</CardDescription>
</CardHeader>
<CardContent>
@@ -625,8 +633,9 @@ export default function AdminAnalytics() {
Daily Orders Array Length:{" "}
{analyticsData?.orders?.dailyOrders?.length || 0}
</div>
{/* ... Existing Debug details kept for brevity ... */}
<div>First 3 Daily Orders:</div>
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
<pre className="pl-4 bg-muted/50 p-2 rounded overflow-auto max-h-32">
{JSON.stringify(
analyticsData?.orders?.dailyOrders?.slice(0, 3),
null,
@@ -635,51 +644,12 @@ export default function AdminAnalytics() {
</pre>
</div>
</div>
<div>
<div className="font-semibold mb-2">Revenue:</div>
<div className="pl-4 space-y-1">
<div>Total: {analyticsData?.revenue?.total || "N/A"}</div>
<div>Today: {analyticsData?.revenue?.today || "N/A"}</div>
<div>
Daily Revenue Array Length:{" "}
{analyticsData?.revenue?.dailyRevenue?.length || 0}
</div>
<div>First 3 Daily Revenue:</div>
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
{JSON.stringify(
analyticsData?.revenue?.dailyRevenue?.slice(0, 3),
null,
2,
)}
</pre>
</div>
</div>
<div>
<div className="font-semibold mb-2">Vendors:</div>
<div className="pl-4 space-y-1">
<div>Total: {analyticsData?.vendors?.total || "N/A"}</div>
<div>
Daily Growth Array Length:{" "}
{analyticsData?.vendors?.dailyGrowth?.length || 0}
</div>
<div>First 3 Daily Growth:</div>
<pre className="pl-4 bg-muted p-2 rounded overflow-auto max-h-32">
{JSON.stringify(
analyticsData?.vendors?.dailyGrowth?.slice(0, 3),
null,
2,
)}
</pre>
</div>
</div>
{/* Simplified debug view for code brevity in replacement, focusing on style changes */}
<details className="mt-4">
<summary className="font-semibold cursor-pointer">
<summary className="font-semibold cursor-pointer hover:text-primary transition-colors">
Full JSON Response
</summary>
<pre className="mt-2 bg-muted p-4 rounded overflow-auto max-h-96 text-[10px]">
<pre className="mt-2 bg-muted/50 p-4 rounded overflow-auto max-h-96 text-[10px] backdrop-blur-sm">
{JSON.stringify(analyticsData, null, 2)}
</pre>
</details>
@@ -688,14 +658,16 @@ export default function AdminAnalytics() {
</Card>
)}
{/* Best Month Card (show for YTD, full year, or previous years) */}
{/* Best Month Card (show for YTD, full year, or previous years) */}
{bestMonth && (
<Card className="border-green-500/50 bg-green-500/5">
<CardContent className="pt-6">
<Card className="border-green-500/20 bg-green-500/5 backdrop-blur-sm overflow-hidden relative">
<div className="absolute inset-0 bg-gradient-to-r from-green-500/10 to-transparent opacity-50" />
<CardContent className="pt-6 relative">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-full bg-green-500/20">
<Trophy className="h-5 w-5 text-green-600" />
<div className="flex items-center gap-4">
<div className="p-3 rounded-full bg-green-500/20 border border-green-500/20 shadow-[0_0_15px_rgba(34,197,94,0.2)]">
<Trophy className="h-6 w-6 text-green-600" />
</div>
<div>
<div className="text-sm font-medium text-muted-foreground">
@@ -706,7 +678,7 @@ export default function AdminAnalytics() {
? "(Full Year)"
: "(YTD)"}
</div>
<div className="text-xl font-bold text-green-600">
<div className="text-2xl font-bold text-green-600 mt-1">
{bestMonth.month}
</div>
</div>
@@ -715,11 +687,11 @@ export default function AdminAnalytics() {
<div className="text-sm font-medium text-muted-foreground">
Revenue
</div>
<div className="text-xl font-bold">
<div className="text-2xl font-bold bg-gradient-to-r from-green-600 to-green-400 bg-clip-text text-transparent">
{formatCurrency(bestMonth.revenue)}
</div>
<div className="text-xs text-muted-foreground mt-1">
{bestMonth.orders.toLocaleString()} orders
<div className="text-sm text-muted-foreground mt-1">
{formatNumber(bestMonth.orders)} orders
</div>
</div>
</div>
@@ -727,36 +699,40 @@ export default function AdminAnalytics() {
</Card>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Orders Card */}
<Card>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Orders
</CardTitle>
<ShoppingCart className="h-4 w-4 text-muted-foreground" />
<div className="p-2 bg-blue-500/10 rounded-md">
<ShoppingCart className="h-4 w-4 text-blue-500" />
</div>
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{analyticsData?.orders?.total?.toLocaleString() || "0"}
{formatNumber(analyticsData?.orders?.total)}
</div>
<div className="flex items-center text-xs text-muted-foreground mt-1">
<span>Today: {analyticsData?.orders?.totalToday || 0}</span>
<span className="bg-muted/50 px-1.5 py-0.5 rounded">Today: {analyticsData?.orders?.totalToday || 0}</span>
<div className="ml-auto">
<TrendIndicator
current={analyticsData?.orders?.totalToday || 0}
previous={(analyticsData?.orders?.total || 0) / 30}
/>
</div>
</div>
{loading || refreshing ? (
<div className="mt-3 h-12 flex items-center justify-center">
<div className="mt-4 h-14 flex items-center justify-center">
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
</div>
) : analyticsData?.orders?.dailyOrders &&
analyticsData.orders.dailyOrders.length > 0 ? (
<div className="mt-3 h-12">
<div className="mt-4 h-14 -mx-2">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart
data={transformChartData(
@@ -765,27 +741,29 @@ export default function AdminAnalytics() {
)}
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
>
<Bar dataKey="value" fill="#3b82f6" radius={[2, 2, 0, 0]} />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="value" fill="#3b82f6" fillOpacity={0.8} radius={[2, 2, 0, 0]} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
</RechartsBarChart>
</ResponsiveContainer>
</div>
) : (
<div className="mt-3 text-xs text-muted-foreground">
No chart data available
<div className="mt-4 h-14 flex items-center justify-center text-xs text-muted-foreground bg-muted/20 rounded">
No chart data
</div>
)}
</CardContent>
</Card>
{/* Revenue Card */}
<Card>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Revenue
</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
<div className="p-2 bg-green-500/10 rounded-md">
<DollarSign className="h-4 w-4 text-green-500" />
</div>
</div>
</CardHeader>
<CardContent>
@@ -793,22 +771,22 @@ export default function AdminAnalytics() {
{formatCurrency(analyticsData?.revenue?.total || 0)}
</div>
<div className="flex items-center text-xs text-muted-foreground mt-1">
<span>
Today: {formatCurrency(analyticsData?.revenue?.today || 0)}
</span>
<span className="bg-muted/50 px-1.5 py-0.5 rounded">Today: {formatCurrency(analyticsData?.revenue?.today || 0)}</span>
<div className="ml-auto">
<TrendIndicator
current={analyticsData?.revenue?.today || 0}
previous={(analyticsData?.revenue?.total || 0) / 30} // Rough estimate
previous={(analyticsData?.revenue?.total || 0) / 30}
/>
</div>
</div>
{loading || refreshing ? (
<div className="mt-3 h-12 flex items-center justify-center">
<div className="mt-4 h-14 flex items-center justify-center">
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
</div>
) : analyticsData?.revenue?.dailyRevenue &&
analyticsData.revenue.dailyRevenue.length > 0 ? (
<div className="mt-3 h-12">
<div className="mt-4 h-14 -mx-2">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart
data={transformChartData(
@@ -817,52 +795,54 @@ export default function AdminAnalytics() {
)}
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
>
<Bar dataKey="value" fill="#10b981" radius={[2, 2, 0, 0]} />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="value" fill="#10b981" fillOpacity={0.8} radius={[2, 2, 0, 0]} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
</RechartsBarChart>
</ResponsiveContainer>
</div>
) : (
<div className="mt-3 text-xs text-muted-foreground">
No chart data available
<div className="mt-4 h-14 flex items-center justify-center text-xs text-muted-foreground bg-muted/20 rounded">
No chart data
</div>
)}
</CardContent>
</Card>
{/* Vendors Card */}
<Card>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">Vendors</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Vendors</CardTitle>
<div className="p-2 bg-purple-500/10 rounded-md">
<Users className="h-4 w-4 text-purple-500" />
</div>
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{analyticsData?.vendors?.total?.toLocaleString() || "0"}
</div>
<div className="flex items-center text-xs text-muted-foreground mt-1">
<span>Active: {analyticsData?.vendors?.active || 0}</span>
<span className="ml-2">
Stores: {analyticsData?.vendors?.activeStores || 0}
</span>
<div className="flex items-center text-xs text-muted-foreground mt-1 gap-2">
<span className="bg-muted/50 px-1.5 py-0.5 rounded">Active: {analyticsData?.vendors?.active || 0}</span>
<span className="bg-muted/50 px-1.5 py-0.5 rounded">Stores: {analyticsData?.vendors?.activeStores || 0}</span>
</div>
<div className="flex items-center text-xs text-muted-foreground mt-1">
<span>New Today: {analyticsData?.vendors?.newToday || 0}</span>
<div className="flex items-center text-xs text-muted-foreground mt-2">
<span>New: {analyticsData?.vendors?.newToday || 0}</span>
<div className="ml-auto">
<TrendIndicator
current={analyticsData?.vendors?.newToday || 0}
previous={(analyticsData?.vendors?.newThisWeek || 0) / 7} // Average per day
previous={(analyticsData?.vendors?.newThisWeek || 0) / 7}
/>
</div>
</div>
{loading || refreshing ? (
<div className="mt-3 h-12 flex items-center justify-center">
<div className="mt-2 h-12 flex items-center justify-center">
<div className="animate-spin h-4 w-4 border-2 border-primary border-t-transparent rounded-full"></div>
</div>
) : analyticsData?.vendors?.dailyGrowth &&
analyticsData.vendors.dailyGrowth.length > 0 ? (
<div className="mt-3 h-12">
<div className="mt-2 h-12 -mx-2">
<ResponsiveContainer width="100%" height="100%">
<RechartsBarChart
data={transformChartData(
@@ -871,52 +851,72 @@ export default function AdminAnalytics() {
)}
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
>
<Bar dataKey="value" fill="#8b5cf6" radius={[2, 2, 0, 0]} />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="value" fill="#8b5cf6" fillOpacity={0.8} radius={[2, 2, 0, 0]} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'transparent' }} />
</RechartsBarChart>
</ResponsiveContainer>
</div>
) : (
<div className="mt-3 text-xs text-muted-foreground">
No chart data available
<div className="mt-2 h-12 flex items-center justify-center text-xs text-muted-foreground bg-muted/20 rounded">
No chart data
</div>
)}
</CardContent>
</Card>
{/* Products Card */}
<Card>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">Products</CardTitle>
<Package className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium text-muted-foreground">Products</CardTitle>
<div className="p-2 bg-amber-500/10 rounded-md">
<Package className="h-4 w-4 text-amber-500" />
</div>
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{analyticsData?.products?.total?.toLocaleString() || "0"}
{formatNumber(analyticsData?.products?.total)}
</div>
<div className="flex items-center text-xs text-muted-foreground mt-1">
<span>New This Week: {analyticsData?.products?.recent || 0}</span>
<span className="bg-muted/50 px-1.5 py-0.5 rounded">New This Week: {analyticsData?.products?.recent || 0}</span>
</div>
{/* Visual spacer since no chart here */}
<div className="mt-4 h-14 w-full bg-gradient-to-r from-amber-500/5 to-transparent rounded-md flex items-center justify-center">
<span className="text-xs text-muted-foreground/50 italic">Inventory Overview</span>
</div>
</CardContent>
</Card>
</div>
<Tabs defaultValue="orders" className="mt-6">
<TabsList>
<TabsTrigger value="orders">Orders</TabsTrigger>
<TabsTrigger value="vendors">Vendors</TabsTrigger>
<TabsTrigger value="growth">Growth Since Launch</TabsTrigger>
<Tabs defaultValue="orders" className="mt-8">
<TabsList className="bg-background/40 backdrop-blur-md border border-border/40 p-1 w-full sm:w-auto h-auto grid grid-cols-3 sm:flex">
<TabsTrigger
value="orders"
className="data-[state=active]:bg-background/60 data-[state=active]:shadow-sm data-[state=active]:text-primary transition-all duration-300"
>
Orders
</TabsTrigger>
<TabsTrigger
value="vendors"
className="data-[state=active]:bg-background/60 data-[state=active]:shadow-sm data-[state=active]:text-primary transition-all duration-300"
>
Vendors
</TabsTrigger>
<TabsTrigger
value="growth"
className="data-[state=active]:bg-background/60 data-[state=active]:shadow-sm data-[state=active]:text-primary transition-all duration-300"
>
Growth Since Launch
</TabsTrigger>
</TabsList>
<TabsContent value="orders" className="mt-4">
<Card>
<TabsContent value="orders" className="mt-4 animate-in fade-in slide-in-from-bottom-2 duration-500">
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader>
<CardTitle>Order Trends</CardTitle>
<CardTitle className="bg-gradient-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent w-fit">Order Trends</CardTitle>
<CardDescription>
Daily order volume and revenue processed over the selected time
period
Daily order volume and revenue processed over the selected time period
</CardDescription>
</CardHeader>
<CardContent>
@@ -1039,10 +1039,10 @@ export default function AdminAnalytics() {
</Card>
</TabsContent>
<TabsContent value="vendors" className="mt-4">
<Card>
<TabsContent value="vendors" className="mt-4 animate-in fade-in slide-in-from-bottom-2 duration-500">
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader>
<CardTitle>Vendor Growth</CardTitle>
<CardTitle className="bg-gradient-to-r from-purple-600 to-pink-500 bg-clip-text text-transparent w-fit">Vendor Growth</CardTitle>
<CardDescription>
New vendor registrations over time
</CardDescription>
@@ -1096,27 +1096,25 @@ export default function AdminAnalytics() {
<div className="bg-muted/50 p-4 rounded-lg">
<div className="text-sm font-medium mb-1">Total Vendors</div>
<div className="text-2xl font-bold">
{analyticsData?.vendors?.total?.toLocaleString() || "0"}
{formatNumber(analyticsData?.vendors?.total)}
</div>
</div>
<div className="bg-muted/50 p-4 rounded-lg">
<div className="text-sm font-medium mb-1">Active Vendors</div>
<div className="text-2xl font-bold">
{analyticsData?.vendors?.active?.toLocaleString() || "0"}
{formatNumber(analyticsData?.vendors?.active)}
</div>
</div>
<div className="bg-muted/50 p-4 rounded-lg">
<div className="text-sm font-medium mb-1">Active Stores</div>
<div className="text-2xl font-bold">
{analyticsData?.vendors?.activeStores?.toLocaleString() ||
"0"}
{formatNumber(analyticsData?.vendors?.activeStores)}
</div>
</div>
<div className="bg-muted/50 p-4 rounded-lg">
<div className="text-sm font-medium mb-1">New This Week</div>
<div className="text-2xl font-bold">
{analyticsData?.vendors?.newThisWeek?.toLocaleString() ||
"0"}
{formatNumber(analyticsData?.vendors?.newThisWeek)}
</div>
</div>
</div>
@@ -1198,74 +1196,51 @@ export default function AdminAnalytics() {
{/* Cumulative Stats Cards */}
{growthData?.cumulative && (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Total Orders
</div>
<div className="text-2xl font-bold">
{growthData.cumulative.orders.toLocaleString()}
</div>
<div className="grid grid-cols-2 lg:grid-cols-6 gap-4">
<Card className="col-span-1 border-border/40 bg-background/50 backdrop-blur-sm hover:bg-background/60 transition-colors">
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Total Orders</div>
<div className="text-2xl font-bold">{formatNumber(growthData.cumulative.orders)}</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Total Revenue
</div>
<div className="text-2xl font-bold text-green-600">
{formatCurrency(growthData.cumulative.revenue)}
</div>
<Card className="col-span-1 border-green-500/20 bg-green-500/5 backdrop-blur-sm hover:bg-green-500/10 transition-colors">
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Total Revenue</div>
<div className="text-2xl font-bold text-green-600">{formatCurrency(growthData.cumulative.revenue)}</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Customers
</div>
<div className="text-2xl font-bold">
{growthData.cumulative.customers.toLocaleString()}
</div>
<Card className="col-span-1 border-border/40 bg-background/50 backdrop-blur-sm hover:bg-background/60 transition-colors">
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Customers</div>
<div className="text-2xl font-bold">{formatNumber(growthData.cumulative.customers)}</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Vendors
</div>
<div className="text-2xl font-bold">
{growthData.cumulative.vendors.toLocaleString()}
</div>
<Card className="col-span-1 border-border/40 bg-background/50 backdrop-blur-sm hover:bg-background/60 transition-colors">
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Vendors</div>
<div className="text-2xl font-bold">{formatNumber(growthData.cumulative.vendors)}</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Products
</div>
<div className="text-2xl font-bold">
{growthData.cumulative.products.toLocaleString()}
</div>
<Card className="col-span-1 border-border/40 bg-background/50 backdrop-blur-sm hover:bg-background/60 transition-colors">
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Products</div>
<div className="text-2xl font-bold">{formatNumber(growthData.cumulative.products)}</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="text-sm font-medium text-muted-foreground">
Avg Order Value
</div>
<div className="text-2xl font-bold">
{formatCurrency(growthData.cumulative.avgOrderValue)}
</div>
<Card className="col-span-1 border-purple-500/20 bg-purple-500/5 backdrop-blur-sm hover:bg-purple-500/10 transition-colors">
<CardContent className="pt-4 flex flex-col items-center justify-center text-center">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-1">Avg Order Value</div>
<div className="text-2xl font-bold text-purple-600">{formatCurrency(growthData.cumulative.avgOrderValue)}</div>
</CardContent>
</Card>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Monthly Revenue & Orders Chart */}
<Card>
<Card className="lg:col-span-2 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
<CardHeader>
<CardTitle>Monthly Revenue & Orders</CardTitle>
<CardTitle className="bg-gradient-to-r from-green-600 to-emerald-500 bg-clip-text text-transparent w-fit">Monthly Revenue & Orders</CardTitle>
<CardDescription>
Platform performance by month since launch
</CardDescription>
@@ -1290,36 +1265,52 @@ export default function AdminAnalytics() {
}))}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="formattedMonth" tick={{ fontSize: 12 }} />
<YAxis yAxisId="left" tick={{ fontSize: 12 }} />
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" opacity={0.4} />
<XAxis dataKey="formattedMonth" tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }} axisLine={false} tickLine={false} />
<YAxis yAxisId="left" tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }} axisLine={false} tickLine={false} />
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12 }}
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
axisLine={false} tickLine={false}
tickFormatter={(value) =>
`£${(value / 1000).toFixed(0)}k`
}
/>
<Tooltip
cursor={{ fill: 'hsl(var(--muted)/0.4)' }}
content={({ active, payload }) => {
if (active && payload?.length) {
const data = payload[0].payload;
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="font-medium mb-2">{data.month}</p>
<p className="text-sm text-blue-600">
Orders: {data.orders.toLocaleString()}
</p>
<p className="text-sm text-green-600">
Revenue: {formatCurrency(data.revenue)}
</p>
<p className="text-sm text-purple-600">
Customers: {data.customers.toLocaleString()}
</p>
<p className="text-sm text-amber-600">
New Vendors: {data.newVendors}
</p>
<div className="bg-background/95 border border-border/50 p-4 rounded-xl shadow-xl backdrop-blur-md">
<p className="font-semibold mb-3 border-b border-border/50 pb-2">{data.month}</p>
<div className="space-y-1.5">
<div className="flex items-center justify-between gap-4">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500" /> Orders
</span>
<span className="font-medium">{formatNumber(data.orders)}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" /> Revenue
</span>
<span className="font-medium text-green-600">{formatCurrency(data.revenue)}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-purple-500" /> Customers
</span>
<span className="font-medium">{formatNumber(data.customers)}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-sm text-muted-foreground flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-amber-500" /> New Vendors
</span>
<span className="font-medium">{data.newVendors}</span>
</div>
</div>
</div>
);
}
@@ -1331,7 +1322,9 @@ export default function AdminAnalytics() {
dataKey="orders"
fill="#3b82f6"
radius={[4, 4, 0, 0]}
maxBarSize={50}
name="Orders"
fillOpacity={0.8}
/>
<Line
yAxisId="right"
@@ -1339,14 +1332,15 @@ export default function AdminAnalytics() {
dataKey="revenue"
stroke="#10b981"
strokeWidth={3}
dot={{ fill: "#10b981", r: 4 }}
dot={{ fill: "#10b981", r: 4, strokeWidth: 2, stroke: "hsl(var(--background))" }}
activeDot={{ r: 6, strokeWidth: 0 }}
name="Revenue"
/>
</ComposedChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex items-center justify-center h-80 text-muted-foreground">
<div className="flex items-center justify-center h-80 text-muted-foreground bg-muted/20 rounded-lg border border-dashed border-border/50">
No growth data available
</div>
)}
@@ -1354,19 +1348,20 @@ export default function AdminAnalytics() {
</Card>
{/* Customer Segments Pie Chart */}
<Card>
<Card className="lg:col-span-1 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm h-full flex flex-col">
<CardHeader>
<CardTitle>Customer Segments</CardTitle>
<CardDescription>Breakdown by purchase behavior</CardDescription>
<CardTitle className="bg-gradient-to-r from-amber-600 to-orange-500 bg-clip-text text-transparent w-fit">Customer Segments</CardTitle>
<CardDescription>By purchase behavior</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="flex-1 flex flex-col justify-center">
{growthLoading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
</div>
) : growthData?.customers ? (
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<>
<div className="h-64 min-w-0">
<ResponsiveContainer key={growthData?.customers ? 'ready' : 'loading'} width="100%" height="100%">
<PieChart>
<Pie
data={[
@@ -1393,14 +1388,13 @@ export default function AdminAnalytics() {
]}
cx="50%"
cy="50%"
innerRadius={50}
outerRadius={80}
paddingAngle={2}
innerRadius={60}
outerRadius={90}
paddingAngle={3}
dataKey="value"
label={({ name, percent }) =>
`${name}: ${(percent * 100).toFixed(0)}%`
}
label={({ percent }) => `${(percent * 100).toFixed(0)}%`}
labelLine={false}
stroke="none"
>
{[
{ color: SEGMENT_COLORS.new },
@@ -1420,18 +1414,18 @@ export default function AdminAnalytics() {
data.name.split(" ")[0].toLowerCase()
];
return (
<div className="bg-background border border-border p-3 rounded-lg shadow-lg">
<p className="font-medium">{data.name}</p>
<div className="bg-background/95 border border-border/50 p-3 rounded-lg shadow-xl backdrop-blur-md">
<p className="font-semibold mb-1">{data.name}</p>
<p className="text-sm">
Count: {data.value.toLocaleString()}
Count: <span className="font-mono">{data.value.toLocaleString()}</span>
</p>
{details && (
<>
<p className="text-sm text-green-600">
<p className="text-sm text-green-600 font-medium">
Revenue:{" "}
{formatCurrency(details.totalRevenue)}
</p>
<p className="text-sm">
<p className="text-xs text-muted-foreground mt-1">
Avg Orders: {details.avgOrderCount}
</p>
</>
@@ -1445,101 +1439,104 @@ export default function AdminAnalytics() {
</PieChart>
</ResponsiveContainer>
</div>
) : (
<div className="flex items-center justify-center h-64 text-muted-foreground">
No customer data available
</div>
)}
{/* Segment Stats */}
{growthData?.customers && (
<div className="grid grid-cols-2 gap-2 mt-4">
<div className="p-2 rounded bg-blue-500/10 text-center">
<div className="text-lg font-bold text-blue-600">
<div className="grid grid-cols-2 gap-3 mt-4">
<div className="p-3 rounded-lg bg-blue-500/10 border border-blue-500/10 text-center hover:bg-blue-500/15 transition-colors">
<div className="text-xl font-bold text-blue-600">
{growthData.customers.segments.new}
</div>
<div className="text-xs text-muted-foreground">New</div>
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">New</div>
</div>
<div className="p-2 rounded bg-green-500/10 text-center">
<div className="text-lg font-bold text-green-600">
<div className="p-3 rounded-lg bg-green-500/10 border border-green-500/10 text-center hover:bg-green-500/15 transition-colors">
<div className="text-xl font-bold text-green-600">
{growthData.customers.segments.returning}
</div>
<div className="text-xs text-muted-foreground">
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Returning
</div>
</div>
<div className="p-2 rounded bg-amber-500/10 text-center">
<div className="text-lg font-bold text-amber-600">
<div className="p-3 rounded-lg bg-amber-500/10 border border-amber-500/10 text-center hover:bg-amber-500/15 transition-colors">
<div className="text-xl font-bold text-amber-600">
{growthData.customers.segments.loyal}
</div>
<div className="text-xs text-muted-foreground">Loyal</div>
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Loyal</div>
</div>
<div className="p-2 rounded bg-purple-500/10 text-center">
<div className="text-lg font-bold text-purple-600">
<div className="p-3 rounded-lg bg-purple-500/10 border border-purple-500/10 text-center hover:bg-purple-500/15 transition-colors">
<div className="text-xl font-bold text-purple-600">
{growthData.customers.segments.vip}
</div>
<div className="text-xs text-muted-foreground">VIP</div>
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wider">VIP</div>
</div>
</div>
</>
) : (
<div className="flex items-center justify-center h-64 text-muted-foreground bg-muted/20 rounded-lg border border-dashed border-border/50">
No customer data available
</div>
)}
</CardContent>
</Card>
</div>
{/* Monthly Growth Table */}
{growthData?.monthly && growthData.monthly.length > 0 && (
<Card>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader>
<CardTitle>Monthly Breakdown</CardTitle>
<CardTitle className="bg-gradient-to-r from-foreground to-foreground/70 bg-clip-text text-transparent w-fit">Monthly Breakdown</CardTitle>
<CardDescription>Detailed metrics by month</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<div className="rounded-md border border-border/40 overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="text-left p-2 font-medium">Month</th>
<th className="text-right p-2 font-medium">Orders</th>
<th className="text-right p-2 font-medium">Revenue</th>
<th className="text-right p-2 font-medium">
<tr className="bg-muted/40 border-b border-border/40">
<th className="text-left p-3 font-medium text-muted-foreground">Month</th>
<th className="text-right p-3 font-medium text-muted-foreground">Orders</th>
<th className="text-right p-3 font-medium text-muted-foreground">Revenue</th>
<th className="text-right p-3 font-medium text-muted-foreground">
Customers
</th>
<th className="text-right p-2 font-medium">
<th className="text-right p-3 font-medium text-muted-foreground">
Avg Order
</th>
<th className="text-right p-2 font-medium">
<th className="text-right p-3 font-medium text-muted-foreground">
New Vendors
</th>
<th className="text-right p-2 font-medium">
<th className="text-right p-3 font-medium text-muted-foreground">
New Customers
</th>
</tr>
</thead>
<tbody>
{growthData.monthly.map((month) => (
<tbody className="divide-y divide-border/30">
{growthData.monthly.map((month, i) => (
<tr
key={month.month}
className="border-b hover:bg-muted/50"
className="hover:bg-muted/30 transition-colors animate-in fade-in slide-in-from-bottom-2 duration-500 fill-mode-backwards"
style={{ animationDelay: `${i * 50}ms` }}
>
<td className="p-2 font-medium">
<td className="p-3 font-medium">
{new Date(month.month + "-01").toLocaleDateString(
"en-GB",
{ month: "long", year: "numeric" },
)}
</td>
<td className="text-right p-2">
<td className="text-right p-3">
<div className="inline-flex items-center px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-600 text-xs font-medium">
{month.orders.toLocaleString()}
</div>
</td>
<td className="text-right p-2 text-green-600">
<td className="text-right p-3 text-green-600 font-semibold">
{formatCurrency(month.revenue)}
</td>
<td className="text-right p-2">
<td className="text-right p-3 text-muted-foreground">
{month.customers.toLocaleString()}
</td>
<td className="text-right p-2">
<td className="text-right p-3 text-muted-foreground">
{formatCurrency(month.avgOrderValue)}
</td>
<td className="text-right p-2">{month.newVendors}</td>
<td className="text-right p-2">
<td className="text-right p-3 text-muted-foreground">{month.newVendors}</td>
<td className="text-right p-3 text-muted-foreground">
{month.newCustomers}
</td>
</tr>
@@ -1555,3 +1552,5 @@ export default function AdminAnalytics() {
</div>
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { fetchClient } from "@/lib/api-client";
import { fetchClient } from "@/lib/api/api-client";
export default function BanUserCard() {
const [telegramUserId, setTelegramUserId] = useState("");
@@ -70,3 +70,4 @@ export default function BanUserCard() {
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { fetchClient } from "@/lib/api-client";
import { fetchClient } from "@/lib/api/api-client";
interface Invitation {
_id: string;
@@ -88,3 +88,4 @@ export default function InvitationsListCard() {
}

View File

@@ -1,48 +1,107 @@
"use client";
import { useState } from "react";
import { fetchClient } from "@/lib/api-client";
import { fetchClient } from "@/lib/api/api-client";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/common/card";
import { Button } from "@/components/common/button";
import { Copy, Check, Ticket, Loader2, RefreshCw } from "lucide-react";
import { useToast } from "@/lib/hooks/use-toast";
export default function InviteVendorCard() {
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [code, setCode] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const { toast } = useToast();
async function handleInvite() {
setLoading(true);
setMessage(null);
setCode(null);
setCopied(false);
try {
const res = await fetchClient<{ code: string }>("/admin/invitations", { method: "POST" });
setMessage("Invitation created");
setCode(res.code);
toast({
title: "Invitation Created",
description: "New vendor invitation code generated successfully.",
});
} catch (e: any) {
setMessage(e?.message || "Failed to send invitation");
toast({
title: "Error",
description: e?.message || "Failed to generate invitation",
variant: "destructive",
});
} finally {
setLoading(false);
}
}
const copyToClipboard = () => {
if (!code) return;
navigator.clipboard.writeText(code);
setCopied(true);
toast({
title: "Copied",
description: "Invitation code copied to clipboard",
});
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
<h2 className="font-medium">Invite Vendor</h2>
<p className="text-sm text-muted-foreground mt-1">Generate a new invitation code</p>
<div className="mt-4 space-y-3">
<button
onClick={handleInvite}
className="inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground disabled:opacity-60"
disabled={loading}
<Card className="h-full border-border/40 bg-background/50 backdrop-blur-sm shadow-sm flex flex-col">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-medium flex items-center gap-2">
<Ticket className="h-4 w-4 text-primary" />
Invite Vendor
</CardTitle>
</div>
<CardDescription>Generate a one-time invitation code.</CardDescription>
</CardHeader>
<CardContent className="flex-1 flex flex-col justify-center gap-4">
{code ? (
<div className="space-y-3 animate-in fade-in zoom-in-95 duration-300">
<div className="p-3 rounded-md bg-muted/50 border border-border/50 text-center relative group">
<span className="font-mono text-xl font-bold tracking-widest text-primary">{code}</span>
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1 h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={copyToClipboard}
>
{loading ? "Generating..." : "Generate Invite Code"}
</button>
{message && <p className="text-xs text-muted-foreground">{message}</p>}
{code && (
<div className="text-sm">
Code: <span className="font-mono px-1.5 py-0.5 rounded bg-muted">{code}</span>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
<p className="text-xs text-center text-muted-foreground">
Share this code with the new vendor. It expires in 7 days.
</p>
</div>
) : (
<div className="text-center py-2 text-sm text-muted-foreground/80">
Click generate to create a new code.
</div>
)}
</div>
</div>
</CardContent>
<CardFooter className="pt-0">
<Button
onClick={handleInvite}
disabled={loading}
className="w-full bg-primary/90 hover:bg-primary shadow-sm"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
<>
{code ? <RefreshCw className="mr-2 h-4 w-4" /> : <Ticket className="mr-2 h-4 w-4" />}
{code ? "Generate Another" : "Generate Code"}
</>
)}
</Button>
</CardFooter>
</Card>
);
}

View File

@@ -7,13 +7,13 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { fetchClient } from "@/lib/api-client";
} from "@/components/common/dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import { Skeleton } from "@/components/common/skeleton";
import { Button } from "@/components/common/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/common/select";
import { fetchClient } from "@/lib/api/api-client";
import { toast } from "sonner";
import { Package, User, Calendar, DollarSign, MapPin, Truck, CheckCircle, XCircle, Clock, Wallet, Copy, ExternalLink } from "lucide-react";
@@ -562,3 +562,5 @@ export default function OrderDetailsModal({ orderId, open, onOpenChange }: Order
);
}

View File

@@ -1,11 +1,11 @@
"use client";
import { useState, useMemo } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/common/select";
import { Search, Filter, Eye, ChevronLeft, ChevronRight } from "lucide-react";
import { List } from 'react-window';
import OrderDetailsModal from "./OrderDetailsModal";
@@ -366,3 +366,4 @@ export default function OrdersTable({ orders, enableModal = true }: OrdersTableP
</Card>
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { fetchClient } from "@/lib/api-client";
import { fetchClient } from "@/lib/api/api-client";
interface OrderItem {
name: string;
@@ -99,3 +99,4 @@ export default function RecentOrdersCard() {
}

View File

@@ -1,8 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { fetchClient } from "@/lib/api-client";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { fetchClient } from "@/lib/api/api-client";
import { Button } from "@/components/common/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
interface Status {
uptimeSeconds: number;
@@ -151,3 +151,5 @@ export default function SystemStatusCard() {
}

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { fetchClient } from "@/lib/api-client";
import { fetchClient } from "@/lib/api/api-client";
interface Vendor {
_id: string;
@@ -137,3 +137,4 @@ export default function VendorsCard() {
</div>
);
}

View File

@@ -7,17 +7,17 @@ import {
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
} from "@/components/common/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/common/tabs";
import { Badge } from "@/components/common/badge";
import { Button } from "@/components/common/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
} from "@/components/common/select";
import {
TrendingUp,
ShoppingCart,
@@ -32,21 +32,21 @@ import {
EyeOff,
Calculator,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { useToast } from "@/lib/hooks/use-toast";
import MetricsCard from "./MetricsCard";
import {
getAnalyticsOverviewWithStore,
type AnalyticsOverview,
} from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format";
import { formatGBP, formatNumber } from "@/lib/utils/format";
import { MetricsCardSkeleton } from "./SkeletonLoaders";
import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton";
import { DateRangePicker } from "@/components/ui/date-picker";
import { Skeleton } from "@/components/common/skeleton";
import { DateRangePicker } from "@/components/common/date-picker";
import { DateRange } from "react-day-picker";
import { addDays, startOfDay, endOfDay } from "date-fns";
import type { DateRange as ProfitDateRange } from "@/lib/services/profit-analytics-service";
import { MotionWrapper } from "@/components/ui/motion-wrapper";
import { MotionWrapper } from "@/components/common/motion-wrapper";
import { motion } from "framer-motion";
const RevenueChart = dynamic(() => import("./RevenueChart"), {
@@ -170,7 +170,7 @@ export default function AnalyticsDashboard({
},
{
title: "Total Orders",
value: maskValue(data.orders.total.toLocaleString()),
value: maskValue(formatNumber(data.orders.total)),
description: "All-time orders",
icon: ShoppingCart,
trend: data.orders.completed > 0 ? ("up" as const) : ("neutral" as const),
@@ -178,7 +178,7 @@ export default function AnalyticsDashboard({
},
{
title: "Unique Customers",
value: maskValue(data.customers.unique.toLocaleString()),
value: maskValue(formatNumber(data.customers.unique)),
description: "Total customers",
icon: Users,
trend: "neutral" as const,
@@ -186,7 +186,7 @@ export default function AnalyticsDashboard({
},
{
title: "Products",
value: maskValue(data.products.total.toLocaleString()),
value: maskValue(formatNumber(data.products.total)),
description: "Active products",
icon: Package,
trend: "neutral" as const,
@@ -195,54 +195,116 @@ export default function AnalyticsDashboard({
];
return (
<div className="space-y-10">
{/* Header with Privacy Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-10 pb-20">
{/* Header with Integrated Toolbar */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
<h2 className="text-3xl font-bold tracking-tight">
Analytics Dashboard
</h2>
<p className="text-muted-foreground">
Overview of your store's performance and metrics.
<p className="text-muted-foreground mt-1">
Real-time performance metrics and AI-driven insights.
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 p-1.5 glass-morphism rounded-2xl border border-white/5 shadow-2xl backdrop-blur-xl ring-1 ring-white/5">
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={() => setHideNumbers(!hideNumbers)}
className="flex items-center gap-2"
className={`flex items-center gap-2 rounded-xl transition-all font-medium px-4 ${hideNumbers ? 'bg-primary text-primary-foreground shadow-lg' : 'hover:bg-white/5'}`}
>
{hideNumbers ? (
<>
<EyeOff className="h-4 w-4" />
Show Numbers
<span className="hidden sm:inline">Numbers Hidden</span>
</>
) : (
<>
<Eye className="h-4 w-4" />
Hide Numbers
<Eye className="h-4 w-4 text-primary/70" />
<span className="hidden sm:inline">Hide Numbers</span>
</>
)}
</Button>
<div className="w-px h-5 bg-white/10 mx-1" />
<Button
variant="outline"
variant="ghost"
size="sm"
onClick={refreshData}
disabled={isLoading}
className="flex items-center gap-2"
className="flex items-center gap-2 rounded-xl hover:bg-white/5 font-medium px-4"
>
<RefreshCw
className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`}
className={`h-4 w-4 ${isLoading ? "animate-spin text-primary" : "text-primary/70"}`}
/>
Refresh
<span className="hidden sm:inline">Refresh Data</span>
</Button>
</div>
</div>
<MotionWrapper className="space-y-12">
{/* Analytics Tabs Setup */}
<Tabs defaultValue="overview" className="space-y-10">
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-6 pb-2">
<TabsList className="bg-transparent h-auto p-0 flex flex-wrap gap-2 lg:gap-4">
<TabsTrigger
value="overview"
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
>
<Activity className="h-4 w-4" />
Overview
</TabsTrigger>
<TabsTrigger
value="financials"
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
>
<DollarSign className="h-4 w-4" />
Financials
</TabsTrigger>
<TabsTrigger
value="performance"
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
>
<BarChart3 className="h-4 w-4" />
Performance
</TabsTrigger>
<TabsTrigger
value="ai"
className="px-4 py-2 rounded-xl data-[state=active]:bg-primary/5 data-[state=active]:text-primary data-[state=active]:shadow-none border border-transparent data-[state=active]:border-primary/20 flex items-center gap-2 transition-all"
>
<Calculator className="h-4 w-4" />
AI Insights
</TabsTrigger>
</TabsList>
{/* Contextual Time Range Selector */}
<div className="flex items-center gap-3 bg-muted/30 p-1 rounded-xl border border-border/20">
<span className="text-xs font-semibold text-muted-foreground px-2">Range</span>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-[130px] h-8 border-none bg-transparent shadow-none focus:ring-0">
<SelectValue />
</SelectTrigger>
<SelectContent className="rounded-xl border-border/40">
<SelectItem value="7">Last 7 days</SelectItem>
<SelectItem value="30">Last 30 days</SelectItem>
<SelectItem value="90">Last 90 days</SelectItem>
<SelectItem value="180">Last 180 days</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<TabsContent value="overview" className="space-y-10 outline-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="space-y-10"
>
{/* Key Metrics Cards */}
<MotionWrapper className="space-y-10">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 lg:gap-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{isLoading
? [...Array(4)].map((_, i) => <MetricsCardSkeleton key={i} />)
: metrics.map((metric) => (
@@ -250,156 +312,88 @@ export default function AnalyticsDashboard({
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Completion Rate Card */}
<motion.div>
<Card className="hover:shadow-xl hover:border-indigo-500/30 transition-all duration-300">
<Card className="lg:col-span-1 glass-morphism premium-card">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Order Completion Rate
<CardTitle className="flex items-center gap-2 text-lg">
<Activity className="h-5 w-5 text-emerald-500" />
Order Completion
</CardTitle>
<CardDescription>
Percentage of orders that have been successfully completed
Successfully processed orders
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex items-center gap-4">
<div className="h-12 w-16 bg-muted/20 rounded animate-pulse" />
<div className="flex-1">
<div className="w-full bg-muted/20 rounded-full h-2 animate-pulse" />
</div>
<div className="h-6 w-16 bg-muted/20 rounded animate-pulse" />
</div>
<Skeleton className="h-24 w-full rounded-2xl" />
) : (
<div className="flex items-center gap-4">
<div className="text-3xl font-bold">
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="text-4xl font-extrabold tracking-tight">
{hideNumbers ? "**%" : `${data.orders.completionRate}%`}
</div>
<div className="flex-1">
<div className="w-full bg-secondary rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all duration-300"
style={{
width: hideNumbers
? "0%"
: `${data.orders.completionRate}%`,
}}
/>
</div>
</div>
<Badge variant="secondary">
<Badge variant="outline" className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20 px-3 py-1 text-xs font-bold">
{hideNumbers
? "** / **"
: `${data.orders.completed} / ${data.orders.total}`}
</Badge>
</div>
<div className="w-full bg-secondary/50 rounded-full h-3 overflow-hidden border border-border/20">
<motion.div
initial={{ width: 0 }}
animate={{ width: hideNumbers ? "0%" : `${data.orders.completionRate}%` }}
transition={{ duration: 1, ease: "circOut" }}
className="bg-gradient-to-r from-emerald-500 to-teal-400 h-full rounded-full"
/>
</div>
</div>
)}
</CardContent>
</Card>
</motion.div>
{/* Time Period Selector */}
<div className="flex flex-col sm:flex-row gap-4 sm:items-center sm:justify-between">
<div>
<h3 className="text-lg font-semibold">Time Period</h3>
<p className="text-sm text-muted-foreground">
Revenue, Profit, and Orders tabs use time filtering. Products and
Customers show all-time data.
</p>
</div>
<Select value={timeRange} onValueChange={setTimeRange}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Last 7 days</SelectItem>
<SelectItem value="30">Last 30 days</SelectItem>
<SelectItem value="90">Last 90 days</SelectItem>
</SelectContent>
</Select>
</div>
{/* Analytics Tabs */}
<div className="space-y-8">
<Tabs defaultValue="growth" className="space-y-8">
<TabsList className="grid w-full grid-cols-2 sm:grid-cols-3 lg:grid-cols-7">
<TabsTrigger value="growth" className="flex items-center gap-2">
<Activity className="h-4 w-4" />
Growth
</TabsTrigger>
<TabsTrigger value="revenue" className="flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Revenue
</TabsTrigger>
<TabsTrigger value="profit" className="flex items-center gap-2">
<Calculator className="h-4 w-4" />
Profit
</TabsTrigger>
<TabsTrigger value="products" className="flex items-center gap-2">
<Package className="h-4 w-4" />
Products
</TabsTrigger>
<TabsTrigger value="customers" className="flex items-center gap-2">
<Users className="h-4 w-4" />
Customers
</TabsTrigger>
<TabsTrigger value="orders" className="flex items-center gap-2">
<BarChart3 className="h-4 w-4" />
Orders
</TabsTrigger>
<TabsTrigger value="predictions" className="flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Predictions
</TabsTrigger>
</TabsList>
<TabsContent value="growth" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
{/* Growth Chart Snippet (Simplified) */}
<div className="lg:col-span-2 min-w-0">
<Suspense fallback={<ChartSkeleton />}>
<GrowthAnalyticsChart hideNumbers={hideNumbers} />
</Suspense>
</div>
</div>
</motion.div>
</TabsContent>
<TabsContent value="revenue" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<TabsContent value="financials" className="space-y-8 outline-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="grid grid-cols-1 xl:grid-cols-2 gap-8"
>
<Suspense fallback={<ChartSkeleton />}>
<div className="min-w-0">
<RevenueChart timeRange={timeRange} hideNumbers={hideNumbers} />
</div>
</Suspense>
</motion.div>
</TabsContent>
<TabsContent value="profit" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
{/* Date Range Selector for Profit Calculator */}
<Card>
<div className="space-y-8">
<Card className="glass-morphism">
<CardHeader>
<CardTitle className="text-lg">Date Range</CardTitle>
<CardTitle className="text-lg">Profit Range</CardTitle>
<CardDescription>
Select a custom date range for profit calculations
Custom date selection for analysis
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col sm:flex-row gap-4 sm:items-center">
<DateRangePicker
dateRange={profitDateRange}
onDateRangeChange={setProfitDateRange}
placeholder="Select date range"
showPresets={true}
className="w-full sm:w-auto"
className="w-full"
/>
{profitDateRange?.from && profitDateRange?.to && (
<div className="text-sm text-muted-foreground flex items-center">
<span>
{profitDateRange.from.toLocaleDateString()} -{" "}
{profitDateRange.to.toLocaleDateString()}
</span>
</div>
)}
</div>
</CardContent>
</Card>
<Suspense fallback={<ChartSkeleton />}>
<ProfitAnalyticsChart
dateRange={
@@ -413,43 +407,49 @@ export default function AnalyticsDashboard({
hideNumbers={hideNumbers}
/>
</Suspense>
</div>
</motion.div>
</TabsContent>
<TabsContent value="products" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<TabsContent value="performance" className="space-y-8 outline-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="grid grid-cols-1 lg:grid-cols-2 gap-8"
>
<Suspense fallback={<ChartSkeleton />}>
<div className="min-w-0">
<ProductPerformanceChart />
</div>
</Suspense>
</motion.div>
</TabsContent>
<TabsContent value="customers" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<Suspense fallback={<ChartSkeleton />}>
<CustomerInsightsChart />
</Suspense>
</motion.div>
</TabsContent>
<TabsContent value="orders" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<div className="space-y-8 min-w-0">
<Suspense fallback={<ChartSkeleton />}>
<OrderAnalyticsChart timeRange={timeRange} />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<CustomerInsightsChart />
</Suspense>
</div>
</motion.div>
</TabsContent>
<TabsContent value="predictions" className="space-y-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<TabsContent value="ai" className="space-y-8 outline-none">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
className="min-w-0"
>
<Suspense fallback={<ChartSkeleton />}>
<PredictionsChart timeRange={parseInt(timeRange)} />
</Suspense>
</motion.div>
</TabsContent>
</Tabs>
</div>
</MotionWrapper>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Skeleton } from "@/components/common/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/common/tabs";
import { MetricsCardSkeleton } from './SkeletonLoaders';
import {
TrendingUp,

View File

@@ -1,15 +1,15 @@
"use client"
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import { Button } from "@/components/common/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/common/select";
import { useToast } from "@/lib/hooks/use-toast";
import { Skeleton } from "@/components/common/skeleton";
import { Users, Crown, UserPlus, UserCheck, Star, ChevronLeft, ChevronRight } from "lucide-react";
import { getCustomerInsightsWithStore, type CustomerInsights } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format";
import { formatGBP } from "@/lib/utils/format";
import { CustomerInsightsSkeleton } from './SkeletonLoaders';
export default function CustomerInsightsChart() {
@@ -306,3 +306,4 @@ export default function CustomerInsightsChart() {
</div>
);
}

View File

@@ -7,9 +7,9 @@ import {
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useToast } from "@/hooks/use-toast";
} from "@/components/common/card";
import { Button } from "@/components/common/button";
import { useToast } from "@/lib/hooks/use-toast";
import { RefreshCw } from "lucide-react";
import {
getGrowthAnalyticsWithStore,
@@ -26,6 +26,7 @@ import {
ResponsiveContainer,
Area,
} from "recharts";
import { formatGBP, formatNumber } from "@/lib/utils/format";
interface GrowthAnalyticsChartProps {
hideNumbers?: boolean;
@@ -63,14 +64,6 @@ export default function GrowthAnalyticsChart({
fetchGrowthData();
};
const formatCurrency = (value: number) => {
if (hideNumbers) return "£***";
return new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
maximumFractionDigits: 0,
}).format(value);
};
return (
<div className="space-y-6">
@@ -115,9 +108,7 @@ export default function GrowthAnalyticsChart({
Total Orders
</div>
<div className="text-2xl font-bold">
{hideNumbers
? "***"
: growthData.cumulative.orders.toLocaleString()}
{hideNumbers ? "***" : formatNumber(growthData.cumulative.orders)}
</div>
</CardContent>
</Card>
@@ -127,7 +118,7 @@ export default function GrowthAnalyticsChart({
Total Revenue
</div>
<div className="text-2xl font-bold text-green-600">
{formatCurrency(growthData.cumulative.revenue)}
{hideNumbers ? "£***" : formatGBP(growthData.cumulative.revenue)}
</div>
</CardContent>
</Card>
@@ -137,9 +128,7 @@ export default function GrowthAnalyticsChart({
Customers
</div>
<div className="text-2xl font-bold">
{hideNumbers
? "***"
: growthData.cumulative.customers.toLocaleString()}
{hideNumbers ? "***" : formatNumber(growthData.cumulative.customers)}
</div>
</CardContent>
</Card>
@@ -149,9 +138,7 @@ export default function GrowthAnalyticsChart({
Products
</div>
<div className="text-2xl font-bold">
{hideNumbers
? "***"
: growthData.cumulative.products.toLocaleString()}
{hideNumbers ? "***" : formatNumber(growthData.cumulative.products)}
</div>
</CardContent>
</Card>
@@ -161,7 +148,7 @@ export default function GrowthAnalyticsChart({
Avg Order Value
</div>
<div className="text-2xl font-bold">
{formatCurrency(growthData.cumulative.avgOrderValue)}
{hideNumbers ? "£***" : formatGBP(growthData.cumulative.avgOrderValue)}
</div>
</CardContent>
</Card>
@@ -183,7 +170,7 @@ export default function GrowthAnalyticsChart({
</div>
) : growthData?.monthly && growthData.monthly.length > 0 ? (
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer key={growthData?.monthly?.length || 0} width="100%" height="100%">
<ComposedChart
data={growthData.monthly.map((m) => ({
...m,
@@ -201,6 +188,10 @@ export default function GrowthAnalyticsChart({
<stop offset="5%" stopColor="#10b981" stopOpacity={0.8} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorOrdersGrowth" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.6} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis
@@ -232,16 +223,16 @@ export default function GrowthAnalyticsChart({
Orders:{" "}
{hideNumbers
? "***"
: data.orders.toLocaleString()}
: formatNumber(data.orders)}
</p>
<p className="text-sm text-green-600">
Revenue: {formatCurrency(data.revenue)}
Revenue: {hideNumbers ? "£***" : formatGBP(data.revenue)}
</p>
<p className="text-sm text-purple-600">
Customers:{" "}
{hideNumbers
? "***"
: data.customers.toLocaleString()}
: formatNumber(data.customers)}
</p>
{data.newCustomers !== undefined && (
<p className="text-sm text-cyan-600">
@@ -255,14 +246,16 @@ export default function GrowthAnalyticsChart({
return null;
}}
/>
<Line
<Area
yAxisId="left"
type="monotone"
dataKey="orders"
stroke="#3b82f6"
strokeWidth={2}
dot={{ fill: "#3b82f6", r: 3 }}
dot={false}
activeDot={{ r: 4, strokeWidth: 0 }}
name="Orders"
fill="url(#colorOrdersGrowth)"
/>
<Area
yAxisId="right"
@@ -270,7 +263,8 @@ export default function GrowthAnalyticsChart({
dataKey="revenue"
stroke="#10b981"
strokeWidth={3}
dot={{ fill: "#10b981", r: 4 }}
dot={false}
activeDot={{ r: 5, strokeWidth: 0 }}
name="Revenue"
fill="url(#colorRevenueGrowth)"
/>
@@ -320,16 +314,16 @@ export default function GrowthAnalyticsChart({
)}
</td>
<td className="text-right p-2">
{hideNumbers ? "***" : month.orders.toLocaleString()}
{hideNumbers ? "***" : formatNumber(month.orders)}
</td>
<td className="text-right p-2 text-green-600">
{formatCurrency(month.revenue)}
{hideNumbers ? "£***" : formatGBP(month.revenue)}
</td>
<td className="text-right p-2">
{hideNumbers ? "***" : month.customers.toLocaleString()}
{hideNumbers ? "***" : formatNumber(month.customers)}
</td>
<td className="text-right p-2">
{formatCurrency(month.avgOrderValue)}
{hideNumbers ? "£***" : formatGBP(month.avgOrderValue)}
</td>
<td className="text-right p-2">
{hideNumbers ? "***" : (month.newCustomers ?? 0)}
@@ -345,3 +339,5 @@ export default function GrowthAnalyticsChart({
</div>
);
}

View File

@@ -1,6 +1,6 @@
"use client"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { TrendingUp, TrendingDown, Minus } from "lucide-react";
import { LucideIcon } from "lucide-react";
import { motion } from "framer-motion";
@@ -25,43 +25,89 @@ export default function MetricsCard({
const getTrendIcon = () => {
switch (trend) {
case "up":
return <TrendingUp className="h-4 w-4 text-green-500" />;
return <TrendingUp className="h-4 w-4 text-emerald-500" />;
case "down":
return <TrendingDown className="h-4 w-4 text-red-500" />;
return <TrendingDown className="h-4 w-4 text-rose-500" />;
default:
return <Minus className="h-4 w-4 text-gray-500" />;
return <Minus className="h-4 w-4 text-muted-foreground" />;
}
};
const getTrendColor = () => {
switch (trend) {
case "up":
return "text-green-600";
return "text-emerald-400 bg-emerald-500/10 border-emerald-500/10";
case "down":
return "text-red-600";
return "text-rose-400 bg-rose-500/10 border-rose-500/10";
default:
return "text-gray-600";
return "text-blue-400 bg-blue-500/10 border-blue-500/10";
}
};
const getCategoryColor = () => {
const t = title.toLowerCase();
if (t.includes("revenue") || t.includes("profit")) return "amber";
if (t.includes("order")) return "blue";
if (t.includes("customer")) return "indigo";
if (t.includes("product") || t.includes("inventory")) return "purple";
return "primary";
}
const categoryColor = getCategoryColor();
const getIconContainerColor = () => {
switch (categoryColor) {
case "amber": return "bg-amber-500/15 text-amber-500 border-amber-500/20";
case "blue": return "bg-blue-500/15 text-blue-500 border-blue-500/20";
case "indigo": return "bg-indigo-500/15 text-indigo-500 border-indigo-500/20";
case "purple": return "bg-purple-500/15 text-purple-500 border-purple-500/20";
default: return "bg-primary/15 text-primary border-primary/20";
}
}
const getBadgeColor = () => {
switch (categoryColor) {
case "amber": return "bg-amber-500/10 text-amber-400/80 border-amber-500/20";
case "blue": return "bg-blue-500/10 text-blue-400/80 border-blue-500/20";
case "indigo": return "bg-indigo-500/10 text-indigo-400/80 border-indigo-500/20";
case "purple": return "bg-purple-500/10 text-purple-400/80 border-purple-500/20";
default: return "bg-primary/10 text-primary/60 border-primary/20";
}
}
return (
<motion.div>
<Card className="hover:shadow-xl hover:border-indigo-500/30 transition-all duration-300">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
<motion.div
whileHover={{ y: -4 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<Card className="glass-morphism premium-card relative overflow-hidden group border-white/5">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-4 relative z-10">
<CardTitle className="text-[10px] font-bold tracking-wider text-white/40 uppercase">
{title}
</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
<div className={`p-2.5 rounded-2xl border ${getIconContainerColor()} transition-all duration-300 group-hover:scale-105`}>
<Icon className="h-4 w-4" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground mt-1">{description}</p>
<div className="flex items-center gap-1 mt-2">
<CardContent className="relative z-10">
<div className="space-y-1.5">
<div className="text-3xl font-bold text-white">
{value}
</div>
<p className="text-[11px] text-white/40">
{description}
</p>
</div>
<div className="flex items-center gap-2 mt-6 pt-5">
<div className={`flex items-center gap-1.5 px-3 py-1 rounded-full border ${getTrendColor()} transition-all duration-300`}>
{getTrendIcon()}
<span className={`text-xs ${getTrendColor()}`}>
{trendValue}
<span className="text-[10px] font-bold uppercase tracking-wide">
{trend === "up" ? "+" : ""}{trendValue}
</span>
</div>
</div>
</CardContent>
</Card>
</motion.div>

View File

@@ -1,13 +1,13 @@
"use client"
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import { useToast } from "@/lib/hooks/use-toast";
import { Skeleton } from "@/components/common/skeleton";
import { BarChart3, Clock, CheckCircle, XCircle, AlertCircle, AlertTriangle } from "lucide-react";
import { getOrderAnalyticsWithStore, type OrderAnalytics } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format";
import { formatGBP } from "@/lib/utils/format";
import { ChartSkeleton } from './SkeletonLoaders';
interface OrderAnalyticsChartProps {
@@ -202,3 +202,4 @@ export default function OrderAnalyticsChart({ timeRange }: OrderAnalyticsChartPr
</Card>
);
}

View File

@@ -7,16 +7,16 @@ import {
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
} from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import { Button } from "@/components/common/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
} from "@/components/common/select";
import {
TrendingUp,
TrendingDown,
@@ -32,8 +32,8 @@ import {
Info,
Download,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton";
import { useToast } from "@/lib/hooks/use-toast";
import { Skeleton } from "@/components/common/skeleton";
import CountUp from "react-countup";
import {
getPredictionsOverviewWithStore,
@@ -41,7 +41,7 @@ import {
type PredictionsOverview,
type StockPredictionsResponse,
} from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format";
import { formatGBP } from "@/lib/utils/format";
import {
Table,
TableBody,
@@ -49,7 +49,7 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
} from "@/components/common/table";
import { format } from "date-fns";
import {
AreaChart,
@@ -65,9 +65,9 @@ import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Slider } from "@/components/ui/slider";
} from "@/components/common/tooltip";
import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { Slider } from "@/components/common/slider";
interface PredictionsChartProps {
timeRange?: number;
@@ -164,6 +164,13 @@ export default function PredictionsChart({
setSimulationFactor(0);
}, [timeRange]);
// Auto-adjust daysAhead if it exceeds historical timeRange
useEffect(() => {
if (daysAhead > timeRange) {
setDaysAhead(timeRange);
}
}, [timeRange, daysAhead]);
// Switch predictions when daysAhead changes (instant, from batch)
useEffect(() => {
if (batchData) {
@@ -214,7 +221,7 @@ export default function PredictionsChart({
if (!predictions?.sales?.dailyPredictions) return [];
const baselineData = baselinePredictions?.sales?.dailyPredictions || [];
const simulatedDailyData = predictions.sales.dailyPredictions;
const simulatedDailyData = predictions?.sales?.dailyPredictions || [];
return simulatedDailyData.map((d: any, idx: number) => ({
...d,
@@ -307,7 +314,7 @@ export default function PredictionsChart({
Predictions & Forecasting
</CardTitle>
<CardDescription>
{predictions.sales.aiModel?.used
{predictions?.sales?.aiModel?.used
? "AI neural network + statistical models for sales, demand, and inventory"
: "AI-powered predictions for sales, demand, and inventory"}
</CardDescription>
@@ -322,10 +329,21 @@ export default function PredictionsChart({
</SelectTrigger>
<SelectContent>
<SelectItem value="7">7 days</SelectItem>
<SelectItem value="14">14 days</SelectItem>
<SelectItem value="30">30 days</SelectItem>
<SelectItem value="60">60 days</SelectItem>
<SelectItem value="90">90 days</SelectItem>
<SelectItem value="14" disabled={timeRange < 14}>
14 days {timeRange < 14 && "(Needs 14d history)"}
</SelectItem>
<SelectItem value="30" disabled={timeRange < 30}>
30 days {timeRange < 30 && "(Needs 30d history)"}
</SelectItem>
<SelectItem value="60" disabled={timeRange < 60}>
60 days {timeRange < 60 && "(Needs 60d history)"}
</SelectItem>
<SelectItem value="90" disabled={timeRange < 90}>
90 days {timeRange < 90 && "(Needs 90d history)"}
</SelectItem>
<SelectItem value="180" disabled={timeRange < 180}>
180 days {timeRange < 180 && "(Needs 180d history)"}
</SelectItem>
</SelectContent>
</Select>
<Button
@@ -372,13 +390,13 @@ export default function PredictionsChart({
</CardTitle>
</CardHeader>
<CardContent>
{predictions.sales.predicted !== null ? (
{predictions?.sales?.predicted !== null && predictions?.sales?.predicted !== undefined ? (
<div className="space-y-2">
<Tooltip>
<TooltipTrigger asChild>
<div className="text-2xl font-bold w-fit cursor-help">
<CountUp
end={predictions.sales.predicted}
end={predictions?.sales?.predicted || 0}
duration={1.5}
separator=","
decimals={2}
@@ -386,7 +404,7 @@ export default function PredictionsChart({
/>
</div>
</TooltipTrigger>
<TooltipContent>
<TooltipContent side="bottom" className="z-[100]">
<p>Predicted daily average revenue for the next {daysAhead} days</p>
</TooltipContent>
</Tooltip>
@@ -397,71 +415,79 @@ export default function PredictionsChart({
<span className="inline-flex cursor-help">
<Badge
className={getConfidenceColor(
predictions.sales.confidence,
predictions?.sales?.confidence || "low",
)}
>
{getConfidenceLabel(predictions.sales.confidence)} Confidence
{predictions.sales.confidenceScore !== undefined && (
{getConfidenceLabel(predictions?.sales?.confidence || "low")} Confidence
{predictions?.sales?.confidenceScore !== undefined && (
<span className="ml-1 opacity-75">
({Math.round(predictions.sales.confidenceScore * 100)}%)
({Math.round((predictions?.sales?.confidenceScore || 0) * 100)}%)
</span>
)}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent>
<TooltipContent side="bottom" className="z-[100]">
<p>Based on data consistency, historical accuracy, and model agreement</p>
</TooltipContent>
</Tooltip>
{predictions.sales.aiModel?.used && (
{predictions?.sales?.aiModel?.used && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex cursor-help">
<Badge variant="outline" className="bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-500/30">
🤖 AI Powered
{predictions.sales.aiModel.modelAccuracy !== undefined && (
{predictions?.sales?.aiModel?.modelAccuracy !== undefined && (
<span className="ml-1 opacity-75">
({Math.round(predictions.sales.aiModel.modelAccuracy * 100)}%)
({Math.round((predictions?.sales?.aiModel?.modelAccuracy || 0) * 100)}%)
</span>
)}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent>
<p>Predictions generated using a Deep Learning Ensemble Model</p>
<TooltipContent side="bottom" className="z-[100] max-w-xs">
<div className="space-y-1.5">
<p className="font-semibold">Deep Learning Ensemble Model</p>
<p className="text-xs text-muted-foreground">
This percentage indicates how well the AI has learned your specific sales patterns.
</p>
<p className="text-xs text-muted-foreground border-t pt-1.5">
Scores above 90% are optimal100% is avoided to prevent "memorizing" the past and ensure the model remains flexible for future shifts.
</p>
</div>
</TooltipContent>
</Tooltip>
)}
{predictions.sales.trend && (
{predictions?.sales?.trend && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex cursor-help">
<Badge
variant="outline"
className={
predictions.sales.trend.direction === "up"
predictions?.sales?.trend?.direction === "up"
? "text-green-600 border-green-600"
: predictions.sales.trend.direction === "down"
: predictions?.sales?.trend?.direction === "down"
? "text-red-600 border-red-600"
: ""
}
>
{predictions.sales.trend.direction === "up" && (
{predictions?.sales?.trend?.direction === "up" && (
<TrendingUp className="h-3 w-3 mr-1" />
)}
{predictions.sales.trend.direction === "down" && (
{predictions?.sales?.trend?.direction === "down" && (
<TrendingDown className="h-3 w-3 mr-1" />
)}
{predictions.sales.trend.direction === "up"
{predictions?.sales?.trend?.direction === "up"
? "Trending Up"
: predictions.sales.trend.direction === "down"
: predictions?.sales?.trend?.direction === "down"
? "Trending Down"
: "Stable"}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent>
<TooltipContent side="bottom" className="z-[100]">
<p>Direction of the recent sales trend (slope analysis)</p>
</TooltipContent>
</Tooltip>
@@ -470,24 +496,24 @@ export default function PredictionsChart({
Next {daysAhead} days
</span>
</div>
{predictions.sales.predictedOrders && (
{predictions?.sales?.predictedOrders && (
<div className="text-sm text-muted-foreground">
~{Math.round(predictions.sales.predictedOrders)}{" "}
~{Math.round(predictions?.sales?.predictedOrders || 0)}{" "}
orders
</div>
)}
{!predictions.sales.confidenceIntervals &&
predictions.sales.minPrediction &&
predictions.sales.maxPrediction && (
{!predictions?.sales?.confidenceIntervals &&
predictions?.sales?.minPrediction &&
predictions?.sales?.maxPrediction && (
<div className="text-xs text-muted-foreground">
Range: {formatGBP(predictions.sales.minPrediction)} -{" "}
{formatGBP(predictions.sales.maxPrediction)}
Range: {formatGBP(predictions?.sales?.minPrediction || 0)} -{" "}
{formatGBP(predictions?.sales?.maxPrediction || 0)}
</div>
)}
</div>
) : (
<div className="text-sm text-muted-foreground">
{predictions.sales.message ||
{predictions?.sales?.message ||
"Insufficient data for prediction"}
</div>
)}
@@ -504,7 +530,7 @@ export default function PredictionsChart({
<TooltipTrigger>
<Info className="h-3 w-3 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<TooltipContent side="bottom" className="z-[100]">
<p>Technical details about the active prediction model</p>
</TooltipContent>
</Tooltip>
@@ -521,7 +547,7 @@ export default function PredictionsChart({
Hybrid Ensemble (Deep Learning)
</span>
</TooltipTrigger>
<TooltipContent>
<TooltipContent side="bottom" className="z-[100]">
<p>Combines LSTM Neural Networks with Statistical Methods (Holt-Winters, ARIMA)</p>
</TooltipContent>
</Tooltip>
@@ -559,22 +585,41 @@ export default function PredictionsChart({
</Alert>
{/* Daily Predictions Chart */}
{predictions.sales.dailyPredictions &&
predictions.sales.dailyPredictions.length > 0 && (
<Card>
<CardHeader className="pb-3 flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-sm font-medium">
Daily Revenue Forecast
{predictions?.sales?.dailyPredictions &&
predictions?.sales?.dailyPredictions.length > 0 && (
<Card className="glass-morphism border-primary/10 overflow-hidden">
<CardHeader className="pb-6 bg-muted/5">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<CardTitle className="text-xl font-bold flex items-center gap-2 tracking-tight">
<Zap className="h-5 w-5 text-amber-500 fill-amber-500/20" />
Scenario Lab
</CardTitle>
<div className="flex items-center gap-4">
<div className="flex flex-col items-end">
<span className="text-xs font-medium text-muted-foreground">
Simulate Traffic:{" "}
<span className={simulationFactor > 0 ? "text-green-600" : simulationFactor < 0 ? "text-red-600" : ""}>
{simulationFactor > 0 ? "+" : ""}
{simulationFactor}%
</span>
<CardDescription className="text-muted-foreground/80 font-medium">
Adjust variables to see how traffic shifts impact your bottom line.
</CardDescription>
</div>
<div className="flex items-center gap-4 bg-black/40 p-2.5 rounded-2xl border border-white/5 shadow-2xl backdrop-blur-md">
<div className="flex flex-col items-start min-w-[150px]">
<div className="flex items-center gap-1.5 mb-1 ml-1">
<span className="text-[10px] font-bold uppercase tracking-wider text-primary/40">
Traffic Simulation
</span>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3 w-3 text-primary/30 cursor-help" />
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[200px] z-[110] bg-black border-white/10 text-white p-2">
<p className="text-[11px] leading-relaxed">
Simulate traffic growth or decline to see how it might impact your future revenue and order volume.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex items-center gap-3 w-full">
<Slider
value={[simulationFactor]}
min={-50}
@@ -582,52 +627,74 @@ export default function PredictionsChart({
step={10}
onValueChange={(val) => setSimulationFactor(val[0])}
onValueCommit={(val) => setCommittedSimulationFactor(val[0])}
className="w-[150px] mt-1.5"
className="w-full flex-1"
/>
<Badge variant="outline" className={`ml-2 min-w-[50px] text-center font-bold border-2 ${simulationFactor > 0 ? "text-emerald-400 border-emerald-500/30 bg-emerald-500/10" : simulationFactor < 0 ? "text-rose-400 border-rose-500/30 bg-rose-500/10" : "text-primary/60"}`}>
{simulationFactor > 0 ? "+" : ""}{simulationFactor}%
</Badge>
</div>
{simulationFactor !== 0 && (
</div>
{(simulationFactor !== 0 || committedSimulationFactor !== 0) && (
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
className="h-9 w-9 hover:bg-white/10 rounded-xl transition-all"
onClick={() => {
setSimulationFactor(0);
setCommittedSimulationFactor(0);
}}
title="Reset simulation"
title="Reset Scenario"
>
<RefreshCw className="h-3 w-3" />
<RefreshCw className="h-4 w-4 text-primary/70" />
</Button>
)}
</div>
<Button variant="outline" size="sm" onClick={handleExportCSV} title="Export to CSV">
<Button variant="outline" size="sm" onClick={handleExportCSV} className="rounded-xl border-white/10 hover:bg-white/5 font-bold px-4">
<Download className="mr-2 h-4 w-4" />
Export
Export Forecast
</Button>
</div>
</CardHeader>
<CardContent>
<div className="h-[300px] w-full mt-4 relative">
{isSimulating && (
<div className="absolute inset-0 flex items-center justify-center bg-background/50 z-10">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
<CardContent className="pt-8">
{/* Legend / Key */}
<div className="flex items-center gap-8 mb-8 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60">
<div className="flex items-center gap-3">
<div className="w-2.5 h-2.5 rounded-full bg-[#8884d8]" />
Baseline Forecast
</div>
{committedSimulationFactor !== 0 && (
<div className="flex items-center gap-3">
<div className="w-2.5 h-2.5 rounded-full bg-[#10b981]" />
Simulated Scenario
</div>
)}
<ResponsiveContainer width="100%" height="100%">
</div>
<div className="h-80 w-full relative">
{isSimulating && (
<div className="absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm z-20 transition-all rounded-xl">
<div className="flex flex-col items-center gap-3">
<div className="relative">
<RefreshCw className="h-10 w-10 animate-spin text-primary" />
<Zap className="h-4 w-4 text-amber-500 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
<span className="text-xs font-bold uppercase tracking-wider text-primary animate-pulse">Running Neural Simulation...</span>
</div>
</div>
)}
<ResponsiveContainer key={`${daysAhead}-${timeRange}`} width="100%" height="100%">
<AreaChart
data={chartData}
margin={{
top: 5,
right: 10,
left: 0,
bottom: 0,
}}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<defs>
<linearGradient id="colorBaseline" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="#8884d8"
stopOpacity={0.6}
stopOpacity={0.3}
/>
<stop
offset="95%"
@@ -639,7 +706,7 @@ export default function PredictionsChart({
<stop
offset="5%"
stopColor="#10b981"
stopOpacity={0.8}
stopOpacity={0.5}
/>
<stop
offset="95%"
@@ -648,40 +715,66 @@ export default function PredictionsChart({
/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border) / 0.4)" />
<XAxis
dataKey="formattedDate"
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }}
tickLine={false}
axisLine={false}
dy={15}
/>
<YAxis
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }}
tickLine={false}
axisLine={false}
tickFormatter={(value) => `£${value}`}
/>
<RechartsTooltip
contentStyle={{
backgroundColor: "hsl(var(--background))",
borderColor: "hsl(var(--border))",
borderRadius: "var(--radius)",
cursor={{ fill: "transparent", stroke: "hsl(var(--primary) / 0.05)", strokeWidth: 40 }}
content={({ active, payload }) => {
if (active && payload?.length) {
const data = payload[0].payload;
return (
<div className="bg-[#050505] p-5 rounded-2xl shadow-2xl border border-white/10 backdrop-blur-2xl ring-1 ring-white/5">
<p className="font-bold text-[10px] uppercase tracking-wide text-muted-foreground mb-4 border-b border-white/5 pb-3 px-1">{data.formattedDate}</p>
<div className="space-y-3">
<div className="flex items-center justify-between gap-10">
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-wide">Baseline:</span>
<span className="text-sm font-bold text-[#8884d8] tabular-nums">{formatGBP(data.baseline)}</span>
</div>
{committedSimulationFactor !== 0 && (
<div className="flex items-center justify-between gap-10">
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-wide">Simulated:</span>
<div className="flex flex-col items-end">
<span className="text-sm font-bold text-emerald-400 tabular-nums">{formatGBP(data.simulated)}</span>
<span className={`text-[10px] font-bold mt-0.5 ${data.simulated > data.baseline ? 'text-emerald-500' : 'text-rose-500'}`}>
{data.simulated > data.baseline ? '▴' : '▾'} {Math.abs(((data.simulated / data.baseline - 1) * 100)).toFixed(1)}%
</span>
</div>
</div>
)}
<div className="flex items-center justify-between gap-10 pt-3 border-t border-white/5">
<span className="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-widest">Est. Orders:</span>
<span className="text-sm font-bold tabular-nums">
{Math.round(data.orders)}</span>
</div>
</div>
</div>
);
}
return null;
}}
formatter={(value: number, name: string) => [
formatGBP(value),
name === "baseline" ? "Baseline" : "Simulated"
]}
/>
{/* Always show baseline as solid line */}
{/* Always show baseline */}
<Area
type="monotone"
dataKey="baseline"
stroke="#8884d8"
fillOpacity={committedSimulationFactor !== 0 ? 0.3 : 1}
fillOpacity={1}
fill="url(#colorBaseline)"
strokeWidth={committedSimulationFactor !== 0 ? 1 : 2}
strokeWidth={3}
dot={false}
activeDot={{ r: 4, strokeWidth: 0, fill: "#8884d8" }}
/>
{/* Show simulated line when simulation is active */}
{committedSimulationFactor !== 0 && (
@@ -689,10 +782,12 @@ export default function PredictionsChart({
type="monotone"
dataKey="simulated"
stroke="#10b981"
fillOpacity={0.6}
fillOpacity={1}
fill="url(#colorSimulated)"
strokeWidth={3}
strokeDasharray="5 5"
strokeWidth={2}
dot={false}
activeDot={{ r: 6, strokeWidth: 3, stroke: "#fff", fill: "#10b981" }}
/>
)}
</AreaChart>
@@ -838,3 +933,5 @@ export default function PredictionsChart({
</Card >
);
}

View File

@@ -1,14 +1,14 @@
"use client"
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
import { Badge } from "@/components/common/badge";
import { useToast } from "@/lib/hooks/use-toast";
import { Skeleton } from "@/components/common/skeleton";
import { Package } from "lucide-react";
import { getProductPerformanceWithStore, type ProductPerformance } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format";
import { formatGBP, formatNumber } from "@/lib/utils/format";
import { TableSkeleton } from './SkeletonLoaders';
export default function ProductPerformanceChart() {
@@ -137,7 +137,7 @@ export default function ProductPerformanceChart() {
</div>
</TableCell>
<TableCell className="text-right font-medium">
{parseInt(product.totalSold.toFixed(0)).toLocaleString()} {product.unitType}
{formatNumber(parseInt(product.totalSold.toFixed(0)))} {product.unitType}
</TableCell>
<TableCell className="text-right font-medium text-green-600">
{formatGBP(product.totalRevenue)}
@@ -156,3 +156,4 @@ export default function ProductPerformanceChart() {
</Card>
);
}

View File

@@ -1,9 +1,9 @@
"use client"
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/common/badge";
import { Alert, AlertDescription } from "@/components/common/alert";
import {
TrendingUp,
TrendingDown,
@@ -11,12 +11,13 @@ import {
PieChart,
Calculator,
Info,
AlertTriangle
AlertTriangle,
Package
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { formatGBP } from "@/utils/format";
import { useToast } from "@/lib/hooks/use-toast";
import { formatGBP } from "@/lib/utils/format";
import { getProfitOverview, type ProfitOverview, type DateRange } from "@/lib/services/profit-analytics-service";
import { Skeleton } from "@/components/ui/skeleton";
import { Skeleton } from "@/components/common/skeleton";
interface ProfitAnalyticsChartProps {
timeRange?: string;
@@ -28,6 +29,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
const [data, setData] = useState<ProfitOverview | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
const { toast } = useToast();
const maskValue = (value: string): string => {
@@ -237,8 +239,7 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
<CardTitle className="text-sm font-medium">Total Profit</CardTitle>
</CardHeader>
<CardContent>
<div className={`text-2xl font-bold flex items-center gap-2 ${
profitDirection ? 'text-green-600' : 'text-red-600'
<div className={`text-2xl font-bold flex items-center gap-2 ${profitDirection ? 'text-green-600' : 'text-red-600'
}`}>
{profitDirection ? (
<TrendingUp className="h-5 w-5" />
@@ -327,15 +328,34 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
return (
<div
key={product.productId}
className="flex items-center justify-between p-4 border rounded-lg"
className="flex items-center justify-between p-4 border rounded-lg transition-colors hover:bg-muted/30"
>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold text-sm">
<div className="flex items-center gap-4">
<div className="relative flex-shrink-0">
<div className="w-12 h-12 rounded-full overflow-hidden border-2 border-background shadow-sm bg-muted flex items-center justify-center">
{product.image && !imageErrors[product.productId] ? (
<img
src={`/api/products/${product.productId}/image`}
alt={product.productName}
className="w-full h-full object-cover"
onError={() => {
setImageErrors(prev => ({ ...prev, [product.productId]: true }));
}}
/>
) : (
<div className="flex items-center justify-center w-full h-full bg-primary/10 text-primary font-bold text-lg">
{product.productName.charAt(0)}
</div>
)}
</div>
<div className="absolute -top-1 -left-1 w-5 h-5 bg-primary text-[10px] text-primary-foreground flex items-center justify-center rounded-full font-bold border-2 border-background shadow-sm">
{index + 1}
</div>
</div>
<div>
<p className="font-medium">{product.productName}</p>
<p className="text-sm text-muted-foreground">
<p className="font-semibold">{product.productName}</p>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<Package className="h-3 w-3" />
{product.totalQuantitySold} units sold
</p>
</div>
@@ -359,3 +379,5 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
</div>
);
}

View File

@@ -1,12 +1,12 @@
"use client"
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { useToast } from "@/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { useToast } from "@/lib/hooks/use-toast";
import { Skeleton } from "@/components/common/skeleton";
import { TrendingUp, DollarSign } from "lucide-react";
import { getRevenueTrendsWithStore, type RevenueData } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format";
import { formatGBP } from "@/lib/utils/format";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
import { ChartSkeleton } from './SkeletonLoaders';
@@ -175,7 +175,7 @@ export default function RevenueChart({ timeRange, hideNumbers = false }: Revenue
<div className="space-y-6">
{/* Chart */}
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer key={timeRange} width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<defs>
<linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
@@ -208,6 +208,7 @@ export default function RevenueChart({ timeRange, hideNumbers = false }: Revenue
fillOpacity={1}
fill="url(#colorRevenue)"
strokeWidth={2}
activeDot={{ r: 4, strokeWidth: 0 }}
/>
</AreaChart>
</ResponsiveContainer>
@@ -239,3 +240,4 @@ export default function RevenueChart({ timeRange, hideNumbers = false }: Revenue
</Card>
);
}

View File

@@ -1,5 +1,5 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Skeleton } from "@/components/common/skeleton";
// Chart skeleton for revenue trends and order analytics
export function ChartSkeleton({

View File

@@ -2,12 +2,12 @@
import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/common/button";
import { Input } from "@/components/common/input";
import { Label } from "@/components/common/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Store, Search } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { useToast } from "@/lib/hooks/use-toast";
export default function StoreSelector() {
const [storeId, setStoreId] = useState('');
@@ -112,3 +112,4 @@ export default function StoreSelector() {
</Card>
);
}

View File

@@ -20,9 +20,9 @@ export function AnimatedStatsSection({ stats }: AnimatedStatsProps) {
return (
<div className="mx-auto grid max-w-5xl grid-cols-1 gap-6 md:grid-cols-3">
<div className="group relative overflow-hidden flex flex-col items-center text-center rounded-xl bg-gradient-to-b from-zinc-900/50 to-black border border-[#1C1C1C] p-6">
<div className="absolute inset-0 bg-[#D53F8C]/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="absolute inset-0 bg-indigo-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative flex flex-col items-center">
<Package className="h-6 w-6 text-[#D53F8C]" />
<Package className="h-6 w-6 text-indigo-500" />
<div className="mt-4 text-3xl font-bold text-white">
<AnimatedCounter
value={stats.orders.completed}
@@ -40,9 +40,9 @@ export function AnimatedStatsSection({ stats }: AnimatedStatsProps) {
</div>
<div className="group relative overflow-hidden flex flex-col items-center text-center rounded-xl bg-gradient-to-b from-zinc-900/50 to-black border border-[#1C1C1C] p-6">
<div className="absolute inset-0 bg-[#D53F8C]/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="absolute inset-0 bg-indigo-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative flex flex-col items-center">
<Users className="h-6 w-6 text-[#D53F8C]" />
<Users className="h-6 w-6 text-indigo-500" />
<div className="mt-4 text-3xl font-bold text-white">
<AnimatedCounter
value={stats.vendors.total}
@@ -60,9 +60,9 @@ export function AnimatedStatsSection({ stats }: AnimatedStatsProps) {
</div>
<div className="group relative overflow-hidden flex flex-col items-center text-center rounded-xl bg-gradient-to-b from-zinc-900/50 to-black border border-[#1C1C1C] p-6">
<div className="absolute inset-0 bg-[#D53F8C]/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="absolute inset-0 bg-indigo-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="relative flex flex-col items-center">
<CreditCard className="h-6 w-6 text-[#D53F8C]" />
<CreditCard className="h-6 w-6 text-indigo-500" />
<div className="mt-4 text-3xl font-bold text-white">
<AnimatedCounter
value={stats.transactions.volume}

View File

@@ -4,7 +4,7 @@ import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils/styles";
import { buttonVariants } from "@/components/ui/button"
import { buttonVariants } from "@/components/common/button"
const AlertDialog = AlertDialogPrimitive.Root
@@ -139,3 +139,4 @@ export {
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils/styles";
import { buttonVariants } from "@/components/ui/button"
import { buttonVariants } from "@/components/common/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
@@ -64,3 +64,4 @@ function Calendar({
Calendar.displayName = "Calendar"
export { Calendar }

View File

@@ -7,7 +7,7 @@ import useEmblaCarousel, {
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils/styles";
import { Button } from "@/components/ui/button"
import { Button } from "@/components/common/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
@@ -260,3 +260,4 @@ export {
CarouselPrevious,
CarouselNext,
}

View File

@@ -6,7 +6,7 @@ import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils/styles";
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { Dialog, DialogContent } from "@/components/common/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
@@ -15,7 +15,7 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
"flex h-full w-full flex-col overflow-hidden bg-transparent text-popover-foreground",
className
)}
{...props}
@@ -26,8 +26,8 @@ Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<DialogContent className="overflow-hidden p-0 shadow-2xl border-white/5 bg-[#0a0a0a]/80 backdrop-blur-2xl sm:max-w-[600px] [&_button[data-radix-collection-item]]:hidden [&_button[class*='absolute']]:hidden">
<Command className="[&_[cmdk-group-heading]]:px-4 [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-primary/50 [&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-widest [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-14 [&_[cmdk-item]]:px-3 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
@@ -39,12 +39,12 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<div className="flex items-center border-b border-white/5 px-4 bg-white/5" cmdk-input-wrapper="">
<Search className="mr-3 h-5 w-5 shrink-0 text-primary opacity-70" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
"flex h-14 w-full rounded-none bg-transparent py-4 text-base outline-none placeholder:text-muted-foreground/50 disabled:cursor-not-allowed disabled:opacity-50 border-none ring-0 focus:ring-0 focus:outline-none",
className
)}
{...props}
@@ -60,7 +60,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent", className)}
{...props}
/>
))
@@ -115,7 +115,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"relative flex cursor-default gap-2 select-none items-center rounded-lg px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-primary/20 data-[selected=true]:text-primary data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 transition-colors duration-200",
className
)}
{...props}
@@ -151,3 +151,4 @@ export {
CommandShortcut,
CommandSeparator,
}

View File

@@ -6,17 +6,17 @@ import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, X } from "lucide-r
import { DateRange } from "react-day-picker"
import { cn } from "@/lib/utils/styles"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Button } from "@/components/common/button"
import { Calendar } from "@/components/common/calendar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
} from "@/components/common/popover"
import { Badge } from "@/components/common/badge"
import { Input } from "@/components/common/input"
import { Label } from "@/components/common/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/common/select"
interface DatePickerProps {
date?: Date

View File

@@ -0,0 +1,139 @@
"use client"
import * as React from "react"
import { Button } from "@/components/common/button"
import {
Package,
ShoppingBag,
Users,
Truck,
MessageCircle,
Plus,
Share2,
LucideIcon
} from "lucide-react"
import Link from "next/link"
interface EmptyStateProps {
icon?: LucideIcon
title: string
description: string
actionLabel?: string
actionHref?: string
actionOnClick?: () => void
secondaryActionLabel?: string
secondaryActionHref?: string
className?: string
}
/**
* EmptyState - Reusable component for empty tables/lists
* Shows an icon, title, description, and optional action button
*/
export function EmptyState({
icon: Icon = Package,
title,
description,
actionLabel,
actionHref,
actionOnClick,
secondaryActionLabel,
secondaryActionHref,
className = ""
}: EmptyStateProps) {
return (
<div className={`flex flex-col items-center justify-center py-12 px-4 text-center ${className}`}>
<div className="h-16 w-16 rounded-full bg-muted/50 flex items-center justify-center mb-4">
<Icon className="h-8 w-8 text-muted-foreground/50" />
</div>
<h3 className="text-lg font-medium mb-2">{title}</h3>
<p className="text-sm text-muted-foreground max-w-sm mb-6">{description}</p>
<div className="flex items-center gap-3">
{actionLabel && (
actionHref ? (
<Button asChild>
<Link href={actionHref}>
<Plus className="h-4 w-4 mr-2" />
{actionLabel}
</Link>
</Button>
) : actionOnClick ? (
<Button onClick={actionOnClick}>
<Plus className="h-4 w-4 mr-2" />
{actionLabel}
</Button>
) : null
)}
{secondaryActionLabel && secondaryActionHref && (
<Button variant="outline" asChild>
<Link href={secondaryActionHref}>
<Share2 className="h-4 w-4 mr-2" />
{secondaryActionLabel}
</Link>
</Button>
)}
</div>
</div>
)
}
// Preset empty states for common scenarios
export function OrdersEmptyState() {
return (
<EmptyState
icon={ShoppingBag}
title="No orders yet"
description="When customers place orders, they'll appear here. Share your store link to start selling!"
secondaryActionLabel="Share Store"
secondaryActionHref="/dashboard/storefront"
/>
)
}
export function ProductsEmptyState({ onAddProduct }: { onAddProduct?: () => void }) {
return (
<EmptyState
icon={Package}
title="No products yet"
description="Add your first product to start selling. You can add products manually or import from a file."
actionLabel="Add Product"
actionOnClick={onAddProduct}
actionHref={onAddProduct ? undefined : "/dashboard/products/add"}
/>
)
}
export function CustomersEmptyState() {
return (
<EmptyState
icon={Users}
title="No customers yet"
description="Once customers interact with your store, they'll appear here. Share your store link to attract customers!"
secondaryActionLabel="Share Store"
secondaryActionHref="/dashboard/storefront"
/>
)
}
export function ShippingEmptyState({ onAddMethod }: { onAddMethod?: () => void }) {
return (
<EmptyState
icon={Truck}
title="No shipping methods"
description="Add shipping methods so customers know how they'll receive their orders."
actionLabel="Add Shipping Method"
actionOnClick={onAddMethod}
/>
)
}
export function ChatsEmptyState() {
return (
<EmptyState
icon={MessageCircle}
title="No conversations yet"
description="Customer chats will appear here when they message you through Telegram."
/>
)
}

View File

@@ -13,7 +13,7 @@ import {
} from "react-hook-form"
import { cn } from "@/lib/utils/styles";
import { Label } from "@/components/ui/label"
import { Label } from "@/components/common/label"
const Form = FormProvider
@@ -176,3 +176,4 @@ export {
FormMessage,
FormField,
}

View File

@@ -2,7 +2,7 @@ import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils/styles";
import { ButtonProps, buttonVariants } from "@/components/ui/button"
import { ButtonProps, buttonVariants } from "@/components/common/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
@@ -115,3 +115,4 @@ export {
PaginationNext,
PaginationPrevious,
}

View File

@@ -0,0 +1,115 @@
"use client"
import * as React from "react"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/common/tooltip"
import { format, formatDistanceToNow, isToday, isYesterday, differenceInMinutes } from "date-fns"
interface RelativeTimeProps {
date: Date | string | null | undefined
className?: string
showTooltip?: boolean
updateInterval?: number // ms, for auto-updating recent times
}
/**
* RelativeTime - Displays time as "2 hours ago" with full date on hover
* Auto-updates for times less than 1 hour old
*/
export function RelativeTime({
date,
className = "",
showTooltip = true,
updateInterval = 60000 // Update every minute
}: RelativeTimeProps) {
const [, forceUpdate] = React.useReducer(x => x + 1, 0)
const parsedDate = React.useMemo(() => {
if (!date) return null
return typeof date === "string" ? new Date(date) : date
}, [date])
// Auto-update for recent times
React.useEffect(() => {
if (!parsedDate) return
const minutesAgo = differenceInMinutes(new Date(), parsedDate)
// Only auto-update if within the last hour
if (minutesAgo < 60) {
const interval = setInterval(forceUpdate, updateInterval)
return () => clearInterval(interval)
}
}, [parsedDate, updateInterval])
if (!parsedDate || isNaN(parsedDate.getTime())) {
return <span className={className}>-</span>
}
const formatRelative = (d: Date): string => {
const now = new Date()
const minutesAgo = differenceInMinutes(now, d)
// Just now (< 1 minute)
if (minutesAgo < 1) return "Just now"
// Minutes ago (< 60 minutes)
if (minutesAgo < 60) return `${minutesAgo}m ago`
// Hours ago (< 24 hours and today)
if (isToday(d)) {
const hoursAgo = Math.floor(minutesAgo / 60)
return `${hoursAgo}h ago`
}
// Yesterday
if (isYesterday(d)) return "Yesterday"
// Use formatDistanceToNow for older dates
return formatDistanceToNow(d, { addSuffix: true })
}
const fullDate = format(parsedDate, "dd MMM yyyy, HH:mm")
const relativeText = formatRelative(parsedDate)
if (!showTooltip) {
return <span className={className}>{relativeText}</span>
}
return (
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<span className={`cursor-default ${className}`}>{relativeText}</span>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{fullDate}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
/**
* Utility function to get relative time string without component
*/
export function getRelativeTimeString(date: Date | string | null | undefined): string {
if (!date) return "-"
const d = typeof date === "string" ? new Date(date) : date
if (isNaN(d.getTime())) return "-"
const now = new Date()
const minutesAgo = differenceInMinutes(now, d)
if (minutesAgo < 1) return "Just now"
if (minutesAgo < 60) return `${minutesAgo}m ago`
if (isToday(d)) return `${Math.floor(minutesAgo / 60)}h ago`
if (isYesterday(d)) return "Yesterday"
return formatDistanceToNow(d, { addSuffix: true })
}

View File

@@ -5,19 +5,19 @@ import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { useIsMobile } from "@/lib/hooks/use-mobile"
import { cn } from "@/lib/utils/styles";
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import { Button } from "@/components/common/button"
import { Input } from "@/components/common/input"
import { Separator } from "@/components/common/separator"
import { Sheet, SheetContent } from "@/components/common/sheet"
import { Skeleton } from "@/components/common/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
} from "@/components/common/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar:state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
@@ -761,3 +761,5 @@ export {
SidebarTrigger,
useSidebar,
}

View File

@@ -5,7 +5,7 @@ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils/styles";
import { toggleVariants } from "@/components/ui/toggle"
import { toggleVariants } from "@/components/common/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
@@ -59,3 +59,4 @@ const ToggleGroupItem = React.forwardRef<
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

Some files were not shown because too many files have changed in this diff Show More