Compare commits

..

29 Commits

Author SHA1 Message Date
g
5e8ba7bd0a Remove gradient text styles from analytics UI
All checks were successful
Build Frontend / build (push) Successful in 1m10s
Replaces gradient text styles with solid color classes in AdminAnalytics and updates the .text-gradient utility in globals.css. This simplifies the visual style for headings and revenue display.
2026-01-13 10:14:34 +00:00
g
4f4f9404c3 yurr
All checks were successful
Build Frontend / build (push) Successful in 1m13s
2026-01-13 10:13:03 +00:00
g
e412392545 make dat shit a 4 wide grid
Some checks failed
Build Frontend / build (push) Has been cancelled
2026-01-13 10:11:56 +00:00
g
f0b89d675c add new quotes
All checks were successful
Build Frontend / build (push) Successful in 1m11s
2026-01-13 10:05:10 +00:00
g
fe6d544511 Refactor AdminAnalytics helpers and formatting
All checks were successful
Build Frontend / build (push) Successful in 1m12s
Refactored helper functions (transformChartData, combineOrdersAndRevenue, CustomTooltip, formatCurrency, calculateBestMonth) to be defined outside the main component for improved readability and maintainability. Updated code formatting and indentation for consistency, with no changes to business logic.
2026-01-13 08:55:03 +00:00
g
38779c2033 Add AI-powered smart insights to admin analytics
All checks were successful
Build Frontend / build (push) Successful in 1m14s
Introduces a getSmartInsights function to generate actionable AI-driven insights based on analytics and growth data. Displays up to four prioritized insights in the admin dashboard, including AI forecasts, revenue and order trends, customer acquisition, and loyalty metrics. Updates AnalyticsData interface to support new comparison and prediction fields.
2026-01-13 08:01:09 +00:00
g
600ba1e10e Enhance analytics charts with interactivity and skeletons
All checks were successful
Build Frontend / build (push) Successful in 1m16s
Added interactive active segment highlighting to the customer segments pie chart and improved the monthly revenue/orders chart with gradient areas and labeled axes. Replaced loading spinners with ChartSkeleton components for a more consistent loading state. Refactored SkeletonLoaders to accept className and improved code style.
2026-01-13 06:05:52 +00:00
g
66964a3218 Refactor admin analytics stat cards to reusable component
Extracted repeated stat card logic in AdminAnalytics to a new AdminStatCard component and moved the trend indicator to its own file. Updated AdminAnalytics to use AdminStatCard for orders, revenue, vendors, and products, improving code maintainability and consistency. Also updated chart and loading skeleton handling for better UX.
2026-01-13 06:01:39 +00:00
g
a07ca55a1e Improve dashboard prefetching and analytics charts
All checks were successful
Build Frontend / build (push) Successful in 1m14s
Removed dashboard prefetching from the login page to avoid unnecessary middleware redirects for unauthenticated users. Added delayed prefetching of dashboard routes after initial load for better navigation performance. Updated AdminAnalytics to use AreaChart instead of BarChart for daily metrics, improving visual clarity. Enhanced middleware to allow prefetch requests through without redirecting to login, supporting better caching and navigation.
2026-01-13 05:49:14 +00:00
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
182 changed files with 7059 additions and 2840 deletions

View File

@@ -4,9 +4,9 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/common/label";
import { toast } from "sonner"; import { toast } from "sonner";
import { Loader2, ArrowRight } from "lucide-react"; import { Loader2, ArrowRight } from "lucide-react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
@@ -186,4 +186,4 @@ export default function LoginForm() {
</div> </div>
</motion.div> </motion.div>
); );
} }

View File

@@ -1,6 +1,7 @@
"use client" "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 // Use lazy loading for the form component
@@ -30,6 +31,10 @@ function LoginLoading() {
// Main page component // Main page component
export default function LoginPage() { export default function LoginPage() {
const router = useRouter();
// Removed prefetch to avoid middleware redirects when unauthenticated
return ( return (
<div className="relative flex items-center justify-center min-h-screen overflow-hidden"> <div className="relative flex items-center justify-center min-h-screen overflow-hidden">
<AuthBackground /> <AuthBackground />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,12 +2,12 @@
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
import React, { Suspense, lazy, useState, useEffect, Component, ReactNode } from "react"; 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 Link from "next/link";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/common/tabs";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/common/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { AlertCircle, RefreshCw } from "lucide-react"; import { AlertCircle, RefreshCw } from "lucide-react";
// Error Boundary Component // Error Boundary Component
@@ -403,11 +403,12 @@ export default function AdminPage() {
</div> </div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6"> <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 <TabsTrigger
value="analytics" value="analytics"
onMouseEnter={() => handleTabHover("analytics")} onMouseEnter={() => handleTabHover("analytics")}
onFocus={() => handleTabFocus("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 Analytics
</TabsTrigger> </TabsTrigger>
@@ -415,6 +416,7 @@ export default function AdminPage() {
value="management" value="management"
onMouseEnter={() => handleTabHover("management")} onMouseEnter={() => handleTabHover("management")}
onFocus={() => handleTabFocus("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 Management
</TabsTrigger> </TabsTrigger>
@@ -464,3 +466,4 @@ export default function AdminPage() {
} }

View File

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

View File

@@ -1,15 +1,15 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 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 { Search, Ban, UserCheck, Package, DollarSign, Loader2, Repeat, Users, ShoppingBag, CreditCard, UserX } from "lucide-react";
import { fetchClient } from "@/lib/api-client"; import { fetchClient } from "@/lib/api/api-client";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/lib/hooks/use-toast";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
interface TelegramUser { interface TelegramUser {
@@ -50,6 +50,14 @@ export default function AdminUsersPage() {
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [users, setUsers] = useState<TelegramUser[]>([]); 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 [searchQuery, setSearchQuery] = useState("");
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null); const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
@@ -198,20 +206,14 @@ export default function AdminUsersPage() {
<AnimatePresence mode="popLayout"> <AnimatePresence mode="popLayout">
{loading ? ( {loading ? (
<TableRow> <TableRow>
<TableCell colSpan={8} className="h-32 text-center"> <TableCell colSpan={8} className="h-24 text-center">
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground"> <div className="flex items-center justify-center gap-2 text-muted-foreground">
<Loader2 className="h-8 w-8 animate-spin opacity-25" /> <Loader2 className="h-4 w-4 animate-spin" />
<p>Loading users...</p> Loading users...
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
) : users.length === 0 ? ( ) : users.length > 0 ? (
<TableRow>
<TableCell colSpan={8} className="h-32 text-center text-muted-foreground">
{searchQuery ? "No users found matching your search" : "No users found"}
</TableCell>
</TableRow>
) : (
users.map((user, index) => ( users.map((user, index) => (
<motion.tr <motion.tr
key={user.telegramUserId} key={user.telegramUserId}
@@ -219,84 +221,75 @@ export default function AdminUsersPage() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.2, delay: index * 0.03 }} transition={{ duration: 0.2, delay: index * 0.03 }}
className="group hover:bg-muted/40 border-b border-border/50 transition-colors" className={`group border-b border-border/50 transition-colors ${user.isBlocked ? "bg-destructive/5 hover:bg-destructive/10" : "hover:bg-muted/40"}`}
> >
<TableCell> <TableCell className="font-mono text-xs">{user.telegramUserId}</TableCell>
<div className="font-mono text-xs text-muted-foreground/80">{user.telegramUserId}</div>
</TableCell>
<TableCell>
<div className="font-medium flex items-center gap-2">
{user.telegramUsername !== "Unknown" ? (
<>
<span className="text-blue-500/80">@</span>
{user.telegramUsername}
</>
) : (
<span className="text-muted-foreground italic">Unknown</span>
)}
</div>
</TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium">{user.totalOrders}</span> <span className="font-medium">@{user.telegramUsername || "Unknown"}</span>
{user.completedOrders > 0 && ( {user.isBlocked && (
<Badge variant="outline" className="text-[10px] h-5 px-1.5 bg-green-500/10 text-green-600 border-green-200 dark:border-green-900"> <Badge variant="destructive" className="h-5 px-1.5 text-[10px]">Blocked</Badge>
{user.completedOrders} done
</Badge>
)} )}
</div> </div>
</TableCell> </TableCell>
<TableCell>{user.totalOrders}</TableCell>
<TableCell>{formatCurrency(user.totalSpent)}</TableCell>
<TableCell> <TableCell>
<span className="font-medium tabular-nums">{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> <TableCell className="text-xs text-muted-foreground">
{user.isBlocked ? ( {user.firstOrderDate ? new Date(user.firstOrderDate).toLocaleDateString() : "-"}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="destructive" className="items-center gap-1">
<Ban className="h-3 w-3" />
Blocked
</Badge>
</TooltipTrigger>
{user.blockedReason && (
<TooltipContent>
<p className="max-w-xs">{user.blockedReason}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
) : user.totalOrders > 0 ? (
<Badge variant="default" className="bg-green-600 hover:bg-green-700">Active</Badge>
) : (
<Badge variant="secondary">No Orders</Badge>
)}
</TableCell> </TableCell>
<TableCell className="text-sm text-muted-foreground"> <TableCell className="text-xs text-muted-foreground">
{user.firstOrderDate {user.lastOrderDate ? new Date(user.lastOrderDate).toLocaleDateString() : "-"}
? new Date(user.firstOrderDate).toLocaleDateString()
: '-'}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{user.lastOrderDate
? new Date(user.lastOrderDate).toLocaleDateString()
: '-'}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex items-center justify-end gap-1"> <div className="flex justify-end gap-1">
{!user.isBlocked ? ( {user.isBlocked ? (
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"> <TooltipProvider>
<Ban className="h-4 w-4" /> <Tooltip>
</Button> <TooltipTrigger asChild>
<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>
<TooltipContent>
<p>Unblock this user</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : ( ) : (
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-green-600 hover:bg-green-500/10"> <TooltipProvider>
<UserCheck className="h-4 w-4" /> <Tooltip>
</Button> <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> </div>
</TableCell> </TableCell>
</motion.tr> </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> </AnimatePresence>
</TableBody> </TableBody>
@@ -335,3 +328,5 @@ export default function AdminUsersPage() {
</div> </div>
); );
} }

View File

@@ -1,15 +1,25 @@
"use client"; "use client";
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
import { Search, MoreHorizontal, UserCheck, UserX, Mail, Loader2, Store, Shield, ShieldAlert, Clock, Calendar } from "lucide-react"; import { Search, MoreHorizontal, UserCheck, UserX, Mail, Loader2, Store, Shield, ShieldAlert, Clock, Calendar, Pencil, Plus } from "lucide-react";
import { fetchClient } from "@/lib/api-client"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/common/dialog";
import { useToast } from "@/hooks/use-toast"; 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 { motion, AnimatePresence } from "framer-motion";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/common/dropdown-menu";
interface Vendor { interface Vendor {
_id: string; _id: string;
@@ -38,9 +48,73 @@ export default function AdminVendorsPage() {
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [vendors, setVendors] = useState<Vendor[]>([]); 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 [page, setPage] = useState(1);
const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null); const [pagination, setPagination] = useState<PaginationResponse['pagination'] | null>(null);
const [searchQuery, setSearchQuery] = useState(""); 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 () => { const fetchVendors = useCallback(async () => {
try { try {
@@ -173,8 +247,8 @@ export default function AdminVendorsPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<AnimatePresence mode="popLayout"> {isFirefox ? (
{loading ? ( loading ? (
<TableRow> <TableRow>
<TableCell colSpan={6} className="h-32 text-center"> <TableCell colSpan={6} className="h-32 text-center">
<div className="flex flex-col items-center justify-center gap-2 text-muted-foreground"> <div className="flex flex-col items-center justify-center gap-2 text-muted-foreground">
@@ -195,7 +269,6 @@ export default function AdminVendorsPage() {
key={vendor._id} key={vendor._id}
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, delay: index * 0.03 }} transition={{ duration: 0.2, delay: index * 0.03 }}
className="group hover:bg-muted/40 border-b border-border/50 transition-colors" className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
> >
@@ -209,9 +282,29 @@ export default function AdminVendorsPage() {
</TableCell> </TableCell>
<TableCell> <TableCell>
{vendor.storeId ? ( {vendor.storeId ? (
<span className="font-mono text-xs">{vendor.storeId}</span> <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>
) : ( ) : (
<span className="text-muted-foreground italic text-xs">No store</span> <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>
<TableCell> <TableCell>
@@ -243,22 +336,173 @@ export default function AdminVendorsPage() {
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex items-center justify-end space-x-1"> <DropdownMenu>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-primary"> <DropdownMenuTrigger asChild>
<UserCheck className="h-4 w-4" /> <Button variant="ghost" className="h-8 w-8 p-0">
</Button> <span className="sr-only">Open menu</span>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"> <MoreHorizontal className="h-4 w-4" />
<UserX className="h-4 w-4" /> </Button>
</Button> </DropdownMenuTrigger>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground"> <DropdownMenuContent align="end">
<MoreHorizontal className="h-4 w-4" /> <DropdownMenuLabel>Actions</DropdownMenuLabel>
</Button> <DropdownMenuItem
</div> 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> </TableCell>
</motion.tr> </motion.tr>
)) ))
)} )
</AnimatePresence> ) : (
<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> </TableBody>
</Table> </Table>
</div> </div>
@@ -291,6 +535,41 @@ export default function AdminVendorsPage() {
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div>
<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 { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/common/card";
import Layout from "@/components/layout/layout"; import Layout from "@/components/layout/layout";
import { SnowLoader } from "@/components/snow-loader"; import { SnowLoader } from "@/components/snow-loader";
@@ -61,4 +61,4 @@ export default function AnalyticsLoading() {
</div> </div>
</Layout> </Layout>
); );
} }

View File

@@ -5,7 +5,7 @@ import Dashboard from "@/components/dashboard/dashboard";
import AnalyticsDashboard from '@/components/analytics/AnalyticsDashboard'; import AnalyticsDashboard from '@/components/analytics/AnalyticsDashboard';
import AnalyticsDashboardSkeleton from '@/components/analytics/AnalyticsDashboardSkeleton'; import AnalyticsDashboardSkeleton from '@/components/analytics/AnalyticsDashboardSkeleton';
import StoreSelector from '@/components/analytics/StoreSelector'; 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 { fetchServer } from '@/lib/api';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { Info, GitCommit, User, Zap, BarChart3 } from 'lucide-react'; import { Info, GitCommit, User, Zap, BarChart3 } from 'lucide-react';
@@ -34,14 +34,14 @@ export default async function AnalyticsPage({
// Await searchParams as required by Next.js 15+ // Await searchParams as required by Next.js 15+
const resolvedSearchParams = await searchParams; const resolvedSearchParams = await searchParams;
// Check for storeId in query parameters (for staff users) // Check for storeId in query parameters (for staff users)
const storeId = resolvedSearchParams?.storeId; const storeId = resolvedSearchParams?.storeId;
// Check for storeId in cookies (alternative method for staff users) // Check for storeId in cookies (alternative method for staff users)
const cookieStore = await cookies(); const cookieStore = await cookies();
const cookieStoreId = cookieStore.get('storeId')?.value; const cookieStoreId = cookieStore.get('storeId')?.value;
// Use query parameter first, then cookie, then undefined (for vendors) // Use query parameter first, then cookie, then undefined (for vendors)
const finalStoreId = storeId || cookieStoreId; const finalStoreId = storeId || cookieStoreId;
@@ -73,7 +73,7 @@ export default async function AnalyticsPage({
<div className="fixed bottom-2 right-2 text-xs text-muted-foreground bg-background/80 backdrop-blur-sm px-2 py-1 rounded border border-border/50 z-50 flex items-center space-x-2"> <div className="fixed bottom-2 right-2 text-xs text-muted-foreground bg-background/80 backdrop-blur-sm px-2 py-1 rounded border border-border/50 z-50 flex items-center space-x-2">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Info size={12} className="text-muted-foreground/80" /> <Info size={12} className="text-muted-foreground/80" />
<span>v{panelVersion}</span> <span>v{panelVersion}</span>
</div> </div>
@@ -100,12 +100,12 @@ export default async function AnalyticsPage({
); );
} catch (error) { } catch (error) {
console.error('Error fetching analytics data:', error); console.error('Error fetching analytics data:', error);
// If it's a 401/403 error, redirect to login // If it's a 401/403 error, redirect to login
if (error instanceof Error && error.message.includes('401')) { 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 // If it's a 400 error (missing storeId for staff), show store selector
if (error instanceof Error && error.message.includes('400')) { if (error instanceof Error && error.message.includes('400')) {
return ( return (
@@ -122,7 +122,7 @@ export default async function AnalyticsPage({
</Dashboard> </Dashboard>
); );
} }
// For other errors, show a fallback // For other errors, show a fallback
return ( return (
<Dashboard> <Dashboard>
@@ -142,4 +142,4 @@ export default async function AnalyticsPage({
</Dashboard> </Dashboard>
); );
} }
} }

View File

@@ -2,8 +2,8 @@
import { useState } from "react"; import { useState } from "react";
import Dashboard from "@/components/dashboard/dashboard"; import Dashboard from "@/components/dashboard/dashboard";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/common/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Wallet, Bitcoin, Coins, DollarSign, ArrowUpRight } from "lucide-react"; import { Wallet, Bitcoin, Coins, DollarSign, ArrowUpRight } from "lucide-react";
import { toast } from "sonner"; 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 { useState, useEffect, useRef } from "react";
import Layout from "@/components/layout/layout"; import Layout from "@/components/layout/layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Plus, Pencil, Trash2, ChevronRight, ChevronDown, MoveVertical, FolderTree } from "lucide-react"; import { Plus, Pencil, Trash2, ChevronRight, ChevronDown, MoveVertical, FolderTree } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -12,7 +12,7 @@ import {
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/common/select";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -22,10 +22,10 @@ import {
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/common/alert-dialog";
import { apiRequest } from "@/lib/api"; import { apiRequest } from "@/lib/api";
import type { Category } from "@/models/categories"; import type { Category } from "@/lib/models/categories";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/common/card";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
// Drag and Drop imports // Drag and Drop imports
@@ -263,10 +263,10 @@ export default function CategoriesPage() {
> >
<div <div
ref={ref} ref={ref}
className={`group flex items-center p-2 rounded-md transition-all duration-200 border border-transparent className={`group flex items-center p-3 rounded-xl transition-all duration-200 border mb-2
${isEditing ? 'bg-muted/50 border-primary/20' : ''} ${isEditing ? 'bg-indigo-500/10 border-indigo-500/30' : ''}
${isOver ? 'bg-muted border-primary/20' : 'hover:bg-muted/50 hover:border-border/50'} ${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-50' : 'opacity-100'}`} ${isDragging ? 'opacity-30' : 'opacity-100'} backdrop-blur-sm`}
style={{ marginLeft: `${level * 24}px` }} style={{ marginLeft: `${level * 24}px` }}
data-handler-id={handlerId} data-handler-id={handlerId}
> >
@@ -384,51 +384,53 @@ export default function CategoriesPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 xl:grid-cols-5 gap-6 lg:gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 xl:grid-cols-5 gap-6 lg:gap-8">
{/* Add Category Card */} {/* Add Category Card */}
<Card className="lg:col-span-2 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm h-fit sticky top-6"> <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-muted/20 border-b border-border/40 pb-4"> <CardHeader className="bg-white/[0.02] border-b border-white/5 pb-4">
<CardTitle className="text-lg font-medium flex items-center"> <CardTitle className="text-lg font-bold flex items-center text-white">
<Plus className="mr-2 h-4 w-4 text-primary" /> <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 Add New Category
</CardTitle> </CardTitle>
<CardDescription> <CardDescription className="text-zinc-400">
Create a new category or subcategory Create a new category or subcategory
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium leading-none"> <label className="text-sm font-medium leading-none text-zinc-300">
Category Name Category Name
</label> </label>
<Input <Input
value={newCategoryName} value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)} onChange={(e) => setNewCategoryName(e.target.value)}
placeholder="e.g. Electronics, Clothing..." placeholder="e.g. Electronics, Clothing..."
className="h-10 border-border/50 bg-background/50 focus:bg-background transition-colors" className="h-10 border-white/10 bg-black/20 focus:bg-black/40 transition-colors text-white placeholder:text-zinc-600"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium leading-none"> <label className="text-sm font-medium leading-none text-zinc-300">
Parent Category Parent Category
</label> </label>
<Select <Select
value={selectedParentId || "none"} value={selectedParentId || "none"}
onValueChange={setSelectedParentId} onValueChange={setSelectedParentId}
> >
<SelectTrigger className="h-10 border-border/50 bg-background/50 focus:bg-background transition-colors"> <SelectTrigger className="h-10 border-white/10 bg-black/20 focus:bg-black/40 transition-colors text-white">
<SelectValue placeholder="Select parent category" /> <SelectValue placeholder="Select parent category" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent className="bg-zinc-900 border-white/10 text-white">
<SelectItem value="none">No parent (root category)</SelectItem> <SelectItem value="none" className="focus:bg-zinc-800">No parent (root category)</SelectItem>
{categories.map((cat) => ( {categories.map((cat) => (
<SelectItem key={cat._id} value={cat._id}> <SelectItem key={cat._id} value={cat._id} className="focus:bg-zinc-800">
{cat.name} {cat.name}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<Button onClick={handleAddCategory} className="w-full mt-2" size="lg"> <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" /> <Plus className="h-4 w-4 mr-2" />
Add Category Add Category
</Button> </Button>
@@ -437,14 +439,14 @@ export default function CategoriesPage() {
</Card> </Card>
{/* Category List Card */} {/* Category List Card */}
<Card className="lg:col-span-3 border-border/40 bg-background/50 backdrop-blur-sm shadow-sm"> <Card className="lg:col-span-3 border-none bg-transparent shadow-none">
<CardHeader className="bg-muted/20 border-b border-border/40 pb-4"> <CardHeader className="pl-0 pt-0 pb-4">
<CardTitle className="text-lg font-medium">Structure</CardTitle> <CardTitle className="text-lg font-bold text-white">Structure</CardTitle>
<CardDescription> <CardDescription className="text-zinc-400">
Drag and drop to reorder categories Drag and drop to reorder categories
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="pt-6"> <CardContent className="p-0">
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<div className="space-y-2 min-h-[300px]"> <div className="space-y-2 min-h-[300px]">
{loading ? ( {loading ? (
@@ -492,4 +494,5 @@ export default function CategoriesPage() {
</div> </div>
</Layout> </Layout>
); );
} }

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Metadata, Viewport } from "next"; import { Metadata, Viewport } from "next";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import Dashboard from "@/components/dashboard/dashboard"; import Dashboard from "@/components/dashboard/dashboard";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -42,4 +42,4 @@ export default function NewChatPage() {
</div> </div>
</Dashboard> </Dashboard>
); );
} }

View File

@@ -5,10 +5,10 @@ import { useRouter } from "next/navigation";
import Dashboard from "@/components/dashboard/dashboard"; import Dashboard from "@/components/dashboard/dashboard";
import { MessageCircle, AlertCircle, RefreshCw } from "lucide-react"; import { MessageCircle, AlertCircle, RefreshCw } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/common/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
// Error Boundary Component // Error Boundary Component
interface ErrorBoundaryState { interface ErrorBoundaryState {
@@ -95,7 +95,7 @@ function ChatTableSkeleton() {
<Card className="animate-in fade-in duration-500 relative"> <Card className="animate-in fade-in duration-500 relative">
{/* Subtle loading indicator */} {/* Subtle loading indicator */}
<div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden rounded-t-lg"> <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" <div className="h-full bg-primary w-1/3"
style={{ style={{
background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)', background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)',
backgroundSize: '200% 100%', backgroundSize: '200% 100%',
@@ -103,7 +103,7 @@ function ChatTableSkeleton() {
}} }}
/> />
</div> </div>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Skeleton className="h-6 w-32" /> <Skeleton className="h-6 w-32" />
@@ -115,8 +115,8 @@ function ChatTableSkeleton() {
<div className="border-b p-4"> <div className="border-b p-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{['Customer', 'Last Message', 'Date', 'Status', 'Actions'].map((header, i) => ( {['Customer', 'Last Message', 'Date', 'Status', 'Actions'].map((header, i) => (
<Skeleton <Skeleton
key={i} key={i}
className="h-4 w-20 flex-1 animate-in fade-in" className="h-4 w-20 flex-1 animate-in fade-in"
style={{ style={{
animationDelay: `${i * 50}ms`, animationDelay: `${i * 50}ms`,
@@ -127,10 +127,10 @@ function ChatTableSkeleton() {
))} ))}
</div> </div>
</div> </div>
{[...Array(6)].map((_, i) => ( {[...Array(6)].map((_, i) => (
<div <div
key={i} key={i}
className="border-b last:border-b-0 p-4 animate-in fade-in" className="border-b last:border-b-0 p-4 animate-in fade-in"
style={{ style={{
animationDelay: `${300 + i * 50}ms`, animationDelay: `${300 + i * 50}ms`,
@@ -163,19 +163,6 @@ function ChatTableSkeleton() {
} }
export default function ChatsPage() { 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 ( return (
<Dashboard> <Dashboard>
<div className="space-y-6"> <div className="space-y-6">
@@ -185,7 +172,7 @@ export default function ChatsPage() {
Customer Chats Customer Chats
</h1> </h1>
</div> </div>
<ErrorBoundary componentName="Chat Table"> <ErrorBoundary componentName="Chat Table">
<Suspense fallback={<ChatTableSkeleton />}> <Suspense fallback={<ChatTableSkeleton />}>
<ChatTable /> <ChatTable />
@@ -194,4 +181,4 @@ export default function ChatsPage() {
</div> </div>
</Dashboard> </Dashboard>
); );
} }

View File

@@ -1,11 +1,13 @@
"use client"; "use client";
import { Component, ReactNode, useState, useEffect, Suspense } from "react"; import { Component, ReactNode, useState, useEffect, Suspense } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { AlertCircle, RefreshCw } from "lucide-react"; import { AlertCircle, RefreshCw } from "lucide-react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/common/card";
import { useRouter } from "next/navigation";
import { sidebarConfig } from "@/config/sidebar";
// Error Boundary Component // Error Boundary Component
interface ErrorBoundaryState { interface ErrorBoundaryState {
@@ -91,14 +93,14 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
} }
// Suspense wrapper with timeout // Suspense wrapper with timeout
function SuspenseWithTimeout({ function SuspenseWithTimeout({
children, children,
fallback, fallback,
timeout = 5000, timeout = 5000,
timeoutFallback timeoutFallback
}: { }: {
children: ReactNode; children: ReactNode;
fallback: ReactNode; fallback: ReactNode;
timeout?: number; timeout?: number;
timeoutFallback?: ReactNode; timeoutFallback?: ReactNode;
}) { }) {
@@ -157,7 +159,7 @@ function DashboardContentSkeletonWithWarning() {
// Import the skeleton from the page // Import the skeleton from the page
function DashboardContentSkeleton() { function DashboardContentSkeleton() {
return ( return (
<div <div
className="space-y-6 animate-in fade-in duration-500 relative" className="space-y-6 animate-in fade-in duration-500 relative"
role="status" role="status"
aria-label="Loading dashboard content" aria-label="Loading dashboard content"
@@ -165,7 +167,7 @@ function DashboardContentSkeleton() {
> >
{/* Subtle loading indicator */} {/* Subtle loading indicator */}
<div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden rounded-full"> <div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden rounded-full">
<div className="h-full bg-primary w-1/3" <div className="h-full bg-primary w-1/3"
style={{ style={{
background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)', background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)',
backgroundSize: '200% 100%', backgroundSize: '200% 100%',
@@ -183,7 +185,7 @@ function DashboardContentSkeleton() {
{/* Stats cards skeleton */} {/* Stats cards skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<Card <Card
key={i} key={i}
className="animate-in fade-in slide-in-from-bottom-4" className="animate-in fade-in slide-in-from-bottom-4"
style={{ style={{
@@ -219,8 +221,8 @@ function DashboardContentSkeleton() {
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
{[...Array(5)].map((_, i) => ( {[...Array(5)].map((_, i) => (
<div <div
key={i} key={i}
className="flex items-center gap-4 animate-in fade-in" className="flex items-center gap-4 animate-in fade-in"
style={{ style={{
animationDelay: `${400 + i * 50}ms`, animationDelay: `${400 + i * 50}ms`,
@@ -247,9 +249,29 @@ function DashboardContentSkeleton() {
} }
export default function DashboardContentWrapper({ children }: { children: ReactNode }) { export default function DashboardContentWrapper({ children }: { children: ReactNode }) {
const router = useRouter();
useEffect(() => {
// Prefetch main dashboard routes for snappier navigation
const prefetchRoutes = async () => {
// Small delay to prioritize initial page load
await new Promise(resolve => setTimeout(resolve, 2000));
sidebarConfig.forEach(section => {
section.items.forEach(item => {
if (item.href && item.href !== "/dashboard") {
router.prefetch(item.href);
}
});
});
};
prefetchRoutes();
}, [router]);
return ( return (
<ErrorBoundary componentName="Dashboard Content"> <ErrorBoundary componentName="Dashboard Content">
<SuspenseWithTimeout <SuspenseWithTimeout
fallback={<DashboardContentSkeleton />} fallback={<DashboardContentSkeleton />}
timeout={5000} timeout={5000}
timeoutFallback={<DashboardContentSkeletonWithWarning />} timeoutFallback={<DashboardContentSkeletonWithWarning />}
@@ -260,3 +282,4 @@ export default function DashboardContentWrapper({ children }: { children: ReactN
); );
} }

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import Layout from "@/components/layout/layout"; import Layout from "@/components/layout/layout";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { SnowLoader } from "@/components/snow-loader"; import { SnowLoader } from "@/components/snow-loader";
@@ -77,4 +77,4 @@ export default function DashboardLoading() {
</div> </div>
</Layout> </Layout>
); );
} }

View File

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

View File

@@ -4,10 +4,10 @@ import { fetchData } from '@/lib/api';
import { clientFetch } from '@/lib/api'; import { clientFetch } from '@/lib/api';
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/common/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/common/textarea";
import { import {
Table, Table,
TableBody, TableBody,
@@ -15,13 +15,13 @@ import {
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/common/table";
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/common/card";
import { Clipboard, Truck, Package, ArrowRight, ChevronDown, AlertTriangle, Copy, Loader2, RefreshCw, MessageCircle } from "lucide-react"; import { Clipboard, Truck, Package, ArrowRight, ChevronDown, AlertTriangle, Copy, Loader2, RefreshCw, MessageCircle } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -36,9 +36,9 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog"; } from "@/components/common/alert-dialog";
import Layout from "@/components/layout/layout"; 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 OrderTimeline from "@/components/orders/order-timeline";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";

View File

@@ -5,10 +5,10 @@ import { useRouter } from "next/navigation";
import Dashboard from "@/components/dashboard/dashboard"; import Dashboard from "@/components/dashboard/dashboard";
import { Package, AlertCircle, RefreshCw } from "lucide-react"; import { Package, AlertCircle, RefreshCw } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/common/card";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
// Error Boundary Component // Error Boundary Component
interface ErrorBoundaryState { interface ErrorBoundaryState {
@@ -95,7 +95,7 @@ function OrderTableSkeleton() {
<Card className="animate-in fade-in duration-500 relative"> <Card className="animate-in fade-in duration-500 relative">
{/* Subtle loading indicator */} {/* Subtle loading indicator */}
<div className="absolute top-0 left-0 right-0 h-1 bg-muted overflow-hidden rounded-t-lg"> <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" <div className="h-full bg-primary w-1/3"
style={{ style={{
background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)', background: 'linear-gradient(90deg, transparent, hsl(var(--primary)), transparent)',
backgroundSize: '200% 100%', backgroundSize: '200% 100%',
@@ -103,7 +103,7 @@ function OrderTableSkeleton() {
}} }}
/> />
</div> </div>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Skeleton className="h-6 w-32" /> <Skeleton className="h-6 w-32" />
@@ -119,8 +119,8 @@ function OrderTableSkeleton() {
<div className="border-b p-4"> <div className="border-b p-4">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{['Order ID', 'Customer', 'Status', 'Total', 'Date', 'Actions'].map((header, i) => ( {['Order ID', 'Customer', 'Status', 'Total', 'Date', 'Actions'].map((header, i) => (
<Skeleton <Skeleton
key={i} key={i}
className="h-4 w-20 flex-1 animate-in fade-in" className="h-4 w-20 flex-1 animate-in fade-in"
style={{ style={{
animationDelay: `${i * 50}ms`, animationDelay: `${i * 50}ms`,
@@ -131,11 +131,11 @@ function OrderTableSkeleton() {
))} ))}
</div> </div>
</div> </div>
{/* Table rows skeleton */} {/* Table rows skeleton */}
{[...Array(8)].map((_, i) => ( {[...Array(8)].map((_, i) => (
<div <div
key={i} key={i}
className="border-b last:border-b-0 p-4 animate-in fade-in" className="border-b last:border-b-0 p-4 animate-in fade-in"
style={{ style={{
animationDelay: `${300 + i * 50}ms`, animationDelay: `${300 + i * 50}ms`,
@@ -163,19 +163,6 @@ function OrderTableSkeleton() {
} }
export default function OrdersPage() { 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 ( return (
<Dashboard> <Dashboard>
<div className="space-y-6"> <div className="space-y-6">
@@ -194,4 +181,4 @@ export default function OrdersPage() {
</div> </div>
</Dashboard> </Dashboard>
); );
} }

View File

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

View File

@@ -3,16 +3,16 @@
import { useState, useEffect, ChangeEvent, Suspense } from "react"; import { useState, useEffect, ChangeEvent, Suspense } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Layout from "@/components/layout/layout"; import Layout from "@/components/layout/layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Product } from "@/models/products"; import { Product } from "@/lib/models/products";
import { Plus, Upload, Search, RefreshCw, Package2 } from "lucide-react"; import { Plus, Upload, Search, RefreshCw, Package2 } from "lucide-react";
import { clientFetch } from "@/lib/api"; import { clientFetch } from "@/lib/api";
import { Category } from "@/models/categories"; import { Category } from "@/lib/models/categories";
import { toast } from "sonner"; import { toast } from "sonner";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Card, CardContent, CardHeader } from "@/components/common/card";
// Lazy load heavy components with error handling // Lazy load heavy components with error handling
const ProductTable = dynamic(() => import("@/components/tables/product-table").catch((err) => { const ProductTable = dynamic(() => import("@/components/tables/product-table").catch((err) => {
@@ -156,16 +156,6 @@ export default function ProductsPage() {
// Fetch products and categories // Fetch products and categories
useEffect(() => { useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
return;
}
const fetchDataAsync = async () => { const fetchDataAsync = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -194,7 +184,7 @@ export default function ProductsPage() {
}; };
fetchDataAsync(); fetchDataAsync();
}, [router]); }, []);
const handleAddTier = () => { const handleAddTier = () => {
setProductData((prev) => ({ setProductData((prev) => ({
@@ -552,3 +542,5 @@ export default function ProductsPage() {
</Layout> </Layout>
); );
} }

View File

@@ -4,8 +4,8 @@ import { useState, useEffect, ChangeEvent, Suspense } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Layout from "@/components/layout/layout"; import Layout from "@/components/layout/layout";
import { Edit, Plus, Trash, Truck } from "lucide-react"; import { Edit, Plus, Trash, Truck } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { import {
fetchShippingMethods, fetchShippingMethods,
addShippingMethod, addShippingMethod,
@@ -15,7 +15,7 @@ import {
ShippingData ShippingData
} from "@/lib/services/shipping-service"; } from "@/lib/services/shipping-service";
import dynamic from "next/dynamic"; 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 // Lazy load components with error handling
const ShippingModal = dynamic(() => import("@/components/modals/shipping-modal").then(mod => ({ default: mod.ShippingModal })).catch((err) => { const ShippingModal = dynamic(() => import("@/components/modals/shipping-modal").then(mod => ({ default: mod.ShippingModal })).catch((err) => {
@@ -142,11 +142,6 @@ export default function ShippingPage() {
.find((row) => row.startsWith("Authorization=")) .find((row) => row.startsWith("Authorization="))
?.split("=")[1]; ?.split("=")[1];
if (!authToken) {
router.push("/login");
return;
}
const fetchedMethods: ShippingMethod[] = await fetchShippingMethods( const fetchedMethods: ShippingMethod[] = await fetchShippingMethods(
authToken authToken
); );
@@ -317,4 +312,4 @@ export default function ShippingPage() {
/> />
</Layout> </Layout>
); );
} }

View File

@@ -3,12 +3,12 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Layout from "@/components/layout/layout"; import Layout from "@/components/layout/layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"; import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/common/table";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/common/switch";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/common/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -16,12 +16,12 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator DropdownMenuSeparator
} from "@/components/ui/dropdown-menu"; } from "@/components/common/dropdown-menu";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/common/popover";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -32,12 +32,12 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog"; } from "@/components/common/alert-dialog";
import { Product } from "@/models/products"; import { Product } from "@/lib/models/products";
import { Package, RefreshCw, ChevronDown, CheckSquare, XSquare, Boxes, Download, Calendar, Search, Filter, Save, X, Edit2 } from "lucide-react"; import { Package, RefreshCw, ChevronDown, CheckSquare, XSquare, Boxes, Download, Calendar, Search, Filter, Save, X, Edit2 } from "lucide-react";
import { clientFetch } from "@/lib/api"; import { clientFetch } from "@/lib/api";
import { toast } from "sonner"; 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 { DateRange } from "react-day-picker";
import { addDays, startOfDay, endOfDay, format, isSameDay } from "date-fns"; import { addDays, startOfDay, endOfDay, format, isSameDay } from "date-fns";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
@@ -72,16 +72,6 @@ export default function StockManagementPage() {
const [isExporting, setIsExporting] = useState<boolean>(false); const [isExporting, setIsExporting] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
return;
}
const fetchDataAsync = async () => { const fetchDataAsync = async () => {
try { try {
const response = await clientFetch<Product[]>('api/products'); const response = await clientFetch<Product[]>('api/products');
@@ -105,7 +95,7 @@ export default function StockManagementPage() {
}; };
fetchDataAsync(); fetchDataAsync();
}, [router]); }, []);
const handleEditStock = (productId: string) => { const handleEditStock = (productId: string) => {
setEditingStock({ setEditingStock({
@@ -687,4 +677,5 @@ export default function StockManagementPage() {
</AlertDialog> </AlertDialog>
</Layout> </Layout>
); );
} }

View File

@@ -2,7 +2,7 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback } from "react";
import { getCustomers, type CustomerStats } from "@/lib/api"; import { getCustomers, type CustomerStats } from "@/lib/api";
import { formatCurrency } from "@/utils/format"; import { formatCurrency } from "@/lib/utils/format";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
import Layout from "@/components/layout/layout"; import Layout from "@/components/layout/layout";
@@ -13,14 +13,14 @@ import {
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/common/table";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/common/select";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -28,8 +28,8 @@ import {
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
DialogFooter, DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/common/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
@@ -43,23 +43,32 @@ import {
X, X,
CreditCard, CreditCard,
Calendar, Calendar,
ShoppingBag ShoppingBag,
Truck,
CheckCircle,
} from "lucide-react"; } from "lucide-react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/common/card";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
} from "@/components/ui/dropdown-menu"; } from "@/components/common/dropdown-menu";
export default function CustomerManagementPage() { export default function CustomerManagementPage() {
const router = useRouter(); const router = useRouter();
const [customers, setCustomers] = useState<CustomerStats[]>([]); 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 [filteredCustomers, setFilteredCustomers] = useState<CustomerStats[]>([]);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -117,16 +126,6 @@ export default function CustomerManagementPage() {
fetchCustomers(); fetchCustomers();
}, [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 // Add filter function to filter customers when search query changes
useEffect(() => { useEffect(() => {
@@ -336,13 +335,12 @@ export default function CustomerManagementPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<AnimatePresence mode="popLayout"> {isFirefox ? (
{filteredCustomers.map((customer, index) => ( filteredCustomers.map((customer, index) => (
<motion.tr <motion.tr
key={customer.userId} key={customer.userId}
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2, delay: index * 0.03 }} 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" : ""}`} 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)} onClick={() => setSelectedCustomer(customer)}
@@ -409,8 +407,84 @@ export default function CustomerManagementPage() {
)} )}
</TableCell> </TableCell>
</motion.tr> </motion.tr>
))} ))
</AnimatePresence> ) : (
<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> </TableBody>
</Table> </Table>
</div> </div>
@@ -469,118 +543,168 @@ export default function CustomerManagementPage() {
</Card> </Card>
{/* Customer Details Dialog */} {/* Customer Details Dialog */}
{selectedCustomer && ( <AnimatePresence>
<Dialog open={!!selectedCustomer} onOpenChange={(open) => !open && setSelectedCustomer(null)}> {selectedCustomer && (
<DialogContent className="max-w-[95vw] sm:max-w-2xl w-full overflow-y-auto max-h-[90vh] z-[80]"> <Dialog open={!!selectedCustomer} onOpenChange={(open) => !open && setSelectedCustomer(null)}>
<DialogHeader> <DialogContent className="max-w-[95vw] sm:max-w-2xl w-full overflow-y-auto max-h-[90vh] z-[80]">
<DialogTitle className="text-base"> <DialogHeader>
Customer Details <DialogTitle className="text-lg flex items-center gap-3">
</DialogTitle> <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-lg">
</DialogHeader> {selectedCustomer.telegramUsername ? selectedCustomer.telegramUsername.substring(0, 1).toUpperCase() : 'U'}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 py-4"> <div>
{/* Customer Information */} <div className="font-bold">Customer Details</div>
<div> <div className="text-sm font-normal text-muted-foreground flex items-center gap-2">
<div className="mb-4"> @{selectedCustomer.telegramUsername || "Unknown"}
<h3 className="text-sm font-medium mb-2">Customer Information</h3> <span className="w-1 h-1 rounded-full bg-primary" />
<div className="space-y-3"> <span className="font-mono text-xs opacity-70">ID: {selectedCustomer.telegramUserId}</span>
<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>
<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>
<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> </div>
</div> </div>
</DialogTitle>
</DialogHeader>
<div className="p-6 space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Customer Information */}
<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 group">
<div className="text-muted-foreground flex items-center gap-2">
<Users className="h-4 w-4 opacity-50" />
Username
</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 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>
<Button
variant="outline"
size="sm"
className="w-full"
onClick={() => {
window.open(`https://t.me/${selectedCustomer.telegramUsername || selectedCustomer.telegramUserId}`, '_blank');
}}
>
<MessageCircle className="h-4 w-4 mr-2" />
Open Telegram Chat
</Button>
</div>
</motion.div>
{/* Order Statistics */}
<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="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 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 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> </div>
{/* Order Status Breakdown */}
<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/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/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-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 className="pt-4 border-t">
<Button <Button
variant="outline" variant="ghost"
size="sm" onClick={() => setSelectedCustomer(null)}
className="w-full" className=""
onClick={() => { >
window.open(`https://t.me/${selectedCustomer.telegramUsername || selectedCustomer.telegramUserId}`, '_blank'); Close Profile
}} </Button>
<Button
onClick={() => router.push(`/dashboard/storefront/chat/new?userId=${selectedCustomer.telegramUserId}`)}
className=""
> >
<MessageCircle className="h-4 w-4 mr-2" /> <MessageCircle className="h-4 w-4 mr-2" />
Open Telegram Chat Message Customer
</Button> </Button>
</div> </DialogFooter>
</DialogContent>
{/* Order Statistics */} </Dialog>
<div> )}
<h3 className="text-sm font-medium mb-2">Order Statistics</h3> </AnimatePresence>
<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>
</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>
<div className="flex justify-between items-center text-sm">
<div className="text-muted-foreground">First Order:</div>
<div className="font-medium">
{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">
{formatDate(selectedCustomer.lastOrderDate)}
</div>
</div>
</div>
</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>
</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>
<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>
<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>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setSelectedCustomer(null)}
>
Close
</Button>
<Button
onClick={() => router.push(`/dashboard/storefront/chat/new?userId=${selectedCustomer.telegramUserId}`)}
>
<MessageCircle className="h-4 w-4 mr-2" />
Start Chat
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div> </div>
</Layout> </Layout>
); );
} }

View File

@@ -3,17 +3,17 @@
import { useState, useEffect, ChangeEvent } from "react"; import { useState, useEffect, ChangeEvent } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Layout from "@/components/layout/layout"; import Layout from "@/components/layout/layout";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/common/textarea";
import { Save, Send, Key, MessageSquare, Shield, Globe, Wallet, RefreshCw } from "lucide-react"; import { Save, Send, Key, MessageSquare, Shield, Globe, Wallet, RefreshCw } from "lucide-react";
import { apiRequest } from "@/lib/api"; import { apiRequest } from "@/lib/api";
import { toast } from "sonner"; import { toast } from "sonner";
import BroadcastDialog from "@/components/modals/broadcast-dialog"; import BroadcastDialog from "@/components/modals/broadcast-dialog";
import Dashboard from "@/components/dashboard/dashboard"; import Dashboard from "@/components/dashboard/dashboard";
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/common/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/common/label";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { import {
Select, Select,
@@ -21,9 +21,9 @@ import {
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/common/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/common/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/common/tooltip";
const SHIPPING_REGIONS = [ const SHIPPING_REGIONS = [
{ value: "UK", label: "United Kingdom", emoji: "🇬🇧" }, { value: "UK", label: "United Kingdom", emoji: "🇬🇧" },
@@ -106,16 +106,6 @@ export default function StorefrontPage() {
const [saving, setSaving] = useState<boolean>(false); const [saving, setSaving] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
const authToken = document.cookie
.split("; ")
.find((row) => row.startsWith("Authorization="))
?.split("=")[1];
if (!authToken) {
router.push("/login");
return;
}
const fetchStorefront = async () => { const fetchStorefront = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -465,3 +455,5 @@ export default function StorefrontPage() {
</Dashboard > </Dashboard >
); );
} }

View File

@@ -440,7 +440,7 @@ body {
} }
.text-gradient { .text-gradient {
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/60; @apply text-foreground;
} }
.bg-gradient-premium { .bg-gradient-premium {

View File

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

View File

@@ -1,77 +1,43 @@
import { getPlatformStatsServer } from "@/lib/server-api"; import { getPlatformStatsServer } from "@/lib/api/server-api";
import { HomeNavbar } from "@/components/home-navbar"; import { HomeNavbar } from "@/components/layout/home-navbar";
import { Suspense } from "react"; import { Shield, LineChart, Zap, ArrowRight, Sparkles } from "lucide-react";
import { Shield, LineChart, Zap, ArrowRight, CheckCircle2, Sparkles } from "lucide-react"; import { Button } from "@/components/common/button";
import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
import { AnimatedStatsSection } from "@/components/animated-stats-section"; import { AnimatedStatsSection } from "@/components/animated-stats-section";
import { isDecember } from "@/lib/utils/christmas"; import { MotionWrapper } from "@/components/common/motion-wrapper";
import { MotionWrapper } from "@/components/ui/motion-wrapper";
export const dynamic = 'force-dynamic'; 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() { export default async function Home() {
try { try {
const stats = await getPlatformStatsServer(); const stats = await getPlatformStatsServer();
const isDec = isDecember();
return ( return (
<div className={`flex flex-col min-h-screen bg-black text-white ${isDec ? 'christmas-theme' : ''}`}> <div className="relative flex flex-col min-h-screen bg-black text-white">
<div className={`absolute inset-0 bg-gradient-to-br pointer-events-none scale-100 ${isDec <div className="absolute inset-0 bg-gradient-to-br from-indigo-500/10 via-purple-500/5 to-transparent pointer-events-none scale-100" />
? 'from-red-500/10 via-green-500/5 to-transparent'
: 'from-[#D53F8C]/10 via-[#D53F8C]/3 to-transparent'
}`} />
<HomeNavbar /> <HomeNavbar />
{/* Hero Section */} {/* Hero Section */}
<section className="relative overflow-hidden"> <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="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="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 <div className="inline-flex items-center px-4 py-2 rounded-full border border-indigo-500/20 bg-indigo-500/10 mb-4">
? 'bg-red-500/10 border-red-500/20' <Sparkles className="h-4 w-4 mr-2 text-indigo-400" />
: 'bg-[#D53F8C]/10 border-[#D53F8C]/20' <span className="text-sm font-medium text-indigo-400">
}`}>
<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]'}`}>
Secure Crypto Payments Secure Crypto Payments
</span> </span>
</div> </div>
<h1 className="text-4xl md:text-6xl font-bold tracking-tight"> <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> </h1>
<p className="text-lg md:text-xl text-zinc-400 max-w-2xl"> <p className="text-lg md:text-xl text-zinc-400 max-w-2xl">
{isDec Streamline your online business with our all-in-one platform. Secure payments, order tracking, and analytics in one place.
? '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.'
}
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 mt-4"> <div className="flex flex-col sm:flex-row gap-4 mt-4">
<Link href="/dashboard"> <Link href="/dashboard">
<Button <Button
size="lg" size="lg"
className={`gap-2 text-white border-0 h-12 px-8 ${isDec className="gap-2 text-white border-0 h-12 px-8 bg-indigo-600 hover:bg-indigo-700"
? 'bg-gradient-to-r from-red-500 to-green-500 hover:from-red-600 hover:to-green-600'
: 'bg-[#D53F8C] hover:bg-[#B83280]'
}`}
> >
Get Started Get Started
<ArrowRight className="h-4 w-4" /> <ArrowRight className="h-4 w-4" />
@@ -103,35 +69,21 @@ export default async function Home() {
title: "Lightning Fast", title: "Lightning Fast",
description: "Optimized for speed with real-time updates and instant notifications." description: "Optimized for speed with real-time updates and instant notifications."
} }
].map((feature, i) => { ].map((feature, i) => (
const christmasColors = ['from-red-500/5', 'from-green-500/5', 'from-yellow-500/5']; <div
const christmasBorders = ['border-red-500/30', 'border-green-500/30', 'border-yellow-500/30']; key={i}
const christmasIcons = ['text-red-400', 'text-green-400', 'text-yellow-400']; 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"
const christmasBgs = ['bg-red-500/10', 'bg-green-500/10', 'bg-yellow-500/10']; >
<div className="absolute inset-0 bg-gradient-to-b from-indigo-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
return ( <div className="relative">
<div <div className="h-12 w-12 flex items-center justify-center rounded-lg mb-4 bg-indigo-500/10">
key={i} <feature.icon className="h-6 w-6 text-indigo-500" />
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'
}`}
>
<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="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>
<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>
<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>
})} ))}
</div> </div>
</MotionWrapper> </MotionWrapper>
@@ -145,13 +97,6 @@ export default async function Home() {
{/* Footer */} {/* Footer */}
<footer className="relative py-12 px-4 mt-auto"> <footer className="relative py-12 px-4 mt-auto">
<div className="max-w-7xl mx-auto flex flex-col items-center"> <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"> <div className="text-sm text-zinc-500">
© {new Date().getFullYear()} Ember. All rights reserved. © {new Date().getFullYear()} Ember. All rights reserved.
</div> </div>
@@ -164,3 +109,6 @@ export default async function Home() {
return <div>Error loading page</div>; return <div>Error loading page</div>;
} }
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,166 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card";
import { Skeleton } from "@/components/common/skeleton";
import { Area, AreaChart, ResponsiveContainer, Tooltip, TooltipProps } from "recharts";
import { TrendIndicator } from "./TrendIndicator";
import { cn } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
import { formatGBP } from "@/lib/utils/format";
interface ChartDataPoint {
formattedDate: string;
value: number;
orders?: number;
revenue?: number;
[key: string]: any;
}
interface AdminStatCardProps {
title: string;
icon: LucideIcon;
iconColorClass: string;
iconBgClass: string;
value: string | number;
subtext?: React.ReactNode;
trend?: {
current: number;
previous: number;
};
loading?: boolean;
chartData?: ChartDataPoint[];
chartColor: string;
chartGradientId: string;
tooltipPrefix?: string; // "£" or ""
hideChart?: boolean;
children?: React.ReactNode;
}
const CustomTooltip = ({ active, payload, label, prefix = "" }: TooltipProps<any, any> & { prefix?: string }) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-[#050505]/90 p-3 rounded-lg shadow-xl border border-white/10 backdrop-blur-md ring-1 ring-white/5">
<p className="font-bold text-[10px] uppercase tracking-wide text-muted-foreground mb-2 border-b border-white/5 pb-1">
{data.formattedDate || label}
</p>
<div className="flex items-center justify-between gap-4">
<span className="text-[11px] font-semibold text-primary">
{prefix === "£" ? "Revenue" : "Count"}
</span>
<span className="text-[11px] font-bold text-foreground tabular-nums">
{prefix}{prefix === "£" ? (data.value || 0).toFixed(2) : data.value}
</span>
</div>
</div>
);
}
return null;
};
export function AdminStatCard({
title,
icon: Icon,
iconColorClass,
iconBgClass,
value,
subtext,
trend,
loading,
chartData,
chartColor,
chartGradientId,
tooltipPrefix = "",
hideChart = false,
children,
}: AdminStatCardProps) {
if (loading) {
return (
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden h-full">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-8 rounded-md" />
</div>
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-32 mb-2" />
<div className="flex items-center gap-2 mb-4">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-12 ml-auto" />
</div>
{!hideChart && <Skeleton className="h-14 w-full rounded-md" />}
</CardContent>
</Card>
);
}
return (
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden hover:shadow-md transition-shadow duration-300 h-full flex flex-col">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
<div className={cn("p-2 rounded-md", iconBgClass)}>
<Icon className={cn("h-4 w-4", iconColorClass)} />
</div>
</div>
</CardHeader>
<CardContent className="flex-1 flex flex-col">
<div className="text-2xl font-bold">{value}</div>
<div className="flex items-center text-xs text-muted-foreground mt-1">
{subtext}
{trend && (
<div className="ml-auto">
<TrendIndicator current={trend.current} previous={trend.previous} />
</div>
)}
</div>
{children && <div className="mt-2">{children}</div>}
{!hideChart && (
chartData && chartData.length > 0 ? (
<div className="mt-auto pt-4 h-[72px] -mx-2">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={chartData}
margin={{ top: 5, right: 0, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id={chartGradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={chartColor} stopOpacity={0.3} />
<stop offset="95%" stopColor={chartColor} stopOpacity={0} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="value"
stroke={chartColor}
fillOpacity={1}
fill={`url(#${chartGradientId})`}
strokeWidth={2}
activeDot={{ r: 4, strokeWidth: 0, fill: chartColor }}
/>
<Tooltip
content={<CustomTooltip prefix={tooltipPrefix} />}
cursor={{ stroke: 'rgba(255,255,255,0.1)', strokeWidth: 1 }}
/>
</AreaChart>
</ResponsiveContainer>
</div>
) : (
<div className="mt-4 h-14 flex items-center justify-center text-xs text-muted-foreground bg-muted/20 rounded">
No chart data
</div>
)
)}
{/* Fill space if chart is hidden but we want structure consistency */}
{hideChart && <div className="mt-auto pt-4" />}
</CardContent>
</Card>
);
}

View File

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

View File

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

View File

@@ -1,48 +1,107 @@
"use client";
import { useState } from "react"; 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() { export default function InviteVendorCard() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [code, setCode] = useState<string | null>(null); const [code, setCode] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const { toast } = useToast();
async function handleInvite() { async function handleInvite() {
setLoading(true); setLoading(true);
setMessage(null);
setCode(null); setCode(null);
setCopied(false);
try { try {
const res = await fetchClient<{ code: string }>("/admin/invitations", { method: "POST" }); const res = await fetchClient<{ code: string }>("/admin/invitations", { method: "POST" });
setMessage("Invitation created");
setCode(res.code); setCode(res.code);
toast({
title: "Invitation Created",
description: "New vendor invitation code generated successfully.",
});
} catch (e: any) { } catch (e: any) {
setMessage(e?.message || "Failed to send invitation"); toast({
title: "Error",
description: e?.message || "Failed to generate invitation",
variant: "destructive",
});
} finally { } finally {
setLoading(false); 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 ( return (
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]"> <Card className="h-full border-border/40 bg-background/50 backdrop-blur-sm shadow-sm flex flex-col">
<h2 className="font-medium">Invite Vendor</h2> <CardHeader className="pb-3">
<p className="text-sm text-muted-foreground mt-1">Generate a new invitation code</p> <div className="flex items-center justify-between">
<div className="mt-4 space-y-3"> <CardTitle className="text-base font-medium flex items-center gap-2">
<button <Ticket className="h-4 w-4 text-primary" />
onClick={handleInvite} Invite Vendor
className="inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground disabled:opacity-60" </CardTitle>
disabled={loading} </div>
> <CardDescription>Generate a one-time invitation code.</CardDescription>
{loading ? "Generating..." : "Generate Invite Code"} </CardHeader>
</button> <CardContent className="flex-1 flex flex-col justify-center gap-4">
{message && <p className="text-xs text-muted-foreground">{message}</p>} {code ? (
{code && ( <div className="space-y-3 animate-in fade-in zoom-in-95 duration-300">
<div className="text-sm"> <div className="p-3 rounded-md bg-muted/50 border border-border/50 text-center relative group">
Code: <span className="font-mono px-1.5 py-0.5 rounded bg-muted">{code}</span> <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}
>
{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>
</div> <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, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/common/dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/common/select";
import { fetchClient } from "@/lib/api-client"; import { fetchClient } from "@/lib/api/api-client";
import { toast } from "sonner"; import { toast } from "sonner";
import { Package, User, Calendar, DollarSign, MapPin, Truck, CheckCircle, XCircle, Clock, Wallet, Copy, ExternalLink } from "lucide-react"; 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"; "use client";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/common/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/common/select";
import { Search, Filter, Eye, ChevronLeft, ChevronRight } from "lucide-react"; import { Search, Filter, Eye, ChevronLeft, ChevronRight } from "lucide-react";
import { List } from 'react-window'; import { List } from 'react-window';
import OrderDetailsModal from "./OrderDetailsModal"; import OrderDetailsModal from "./OrderDetailsModal";
@@ -366,3 +366,4 @@ export default function OrdersTable({ orders, enableModal = true }: OrdersTableP
</Card> </Card>
); );
} }

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
"use client";
import { TrendingDown, TrendingUp } from "lucide-react";
// Trend indicator component for metric cards
export const TrendIndicator = ({
current,
previous,
}: {
current: number;
previous: number;
}) => {
if (!current || !previous) return null;
const percentChange = ((current - previous) / previous) * 100;
if (Math.abs(percentChange) < 0.1) return null;
return (
<div
className={`flex items-center text-xs font-medium ${percentChange >= 0 ? "text-green-500" : "text-red-500"}`}
>
{percentChange >= 0 ? (
<TrendingUp className="h-3 w-3 mr-1" />
) : (
<TrendingDown className="h-3 w-3 mr-1" />
)}
{Math.abs(percentChange).toFixed(1)}%
</div>
);
};

View File

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

View File

@@ -7,17 +7,17 @@ import {
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/common/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/common/tabs";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/common/select";
import { import {
TrendingUp, TrendingUp,
ShoppingCart, ShoppingCart,
@@ -32,21 +32,21 @@ import {
EyeOff, EyeOff,
Calculator, Calculator,
} from "lucide-react"; } from "lucide-react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/lib/hooks/use-toast";
import MetricsCard from "./MetricsCard"; import MetricsCard from "./MetricsCard";
import { import {
getAnalyticsOverviewWithStore, getAnalyticsOverviewWithStore,
type AnalyticsOverview, type AnalyticsOverview,
} from "@/lib/services/analytics-service"; } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format"; import { formatGBP, formatNumber } from "@/lib/utils/format";
import { MetricsCardSkeleton } from "./SkeletonLoaders"; import { MetricsCardSkeleton } from "./SkeletonLoaders";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { DateRangePicker } from "@/components/ui/date-picker"; import { DateRangePicker } from "@/components/common/date-picker";
import { DateRange } from "react-day-picker"; import { DateRange } from "react-day-picker";
import { addDays, startOfDay, endOfDay } from "date-fns"; import { addDays, startOfDay, endOfDay } from "date-fns";
import type { DateRange as ProfitDateRange } from "@/lib/services/profit-analytics-service"; 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"; import { motion } from "framer-motion";
const RevenueChart = dynamic(() => import("./RevenueChart"), { const RevenueChart = dynamic(() => import("./RevenueChart"), {
@@ -170,7 +170,7 @@ export default function AnalyticsDashboard({
}, },
{ {
title: "Total Orders", title: "Total Orders",
value: maskValue(data.orders.total.toLocaleString()), value: maskValue(formatNumber(data.orders.total)),
description: "All-time orders", description: "All-time orders",
icon: ShoppingCart, icon: ShoppingCart,
trend: data.orders.completed > 0 ? ("up" as const) : ("neutral" as const), trend: data.orders.completed > 0 ? ("up" as const) : ("neutral" as const),
@@ -178,7 +178,7 @@ export default function AnalyticsDashboard({
}, },
{ {
title: "Unique Customers", title: "Unique Customers",
value: maskValue(data.customers.unique.toLocaleString()), value: maskValue(formatNumber(data.customers.unique)),
description: "Total customers", description: "Total customers",
icon: Users, icon: Users,
trend: "neutral" as const, trend: "neutral" as const,
@@ -186,7 +186,7 @@ export default function AnalyticsDashboard({
}, },
{ {
title: "Products", title: "Products",
value: maskValue(data.products.total.toLocaleString()), value: maskValue(formatNumber(data.products.total)),
description: "Active products", description: "Active products",
icon: Package, icon: Package,
trend: "neutral" as const, trend: "neutral" as const,
@@ -451,3 +451,5 @@ export default function AnalyticsDashboard({
</div> </div>
); );
} }

View File

@@ -1,6 +1,6 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/common/tabs";
import { MetricsCardSkeleton } from './SkeletonLoaders'; import { MetricsCardSkeleton } from './SkeletonLoaders';
import { import {
TrendingUp, TrendingUp,
@@ -201,4 +201,4 @@ export default function AnalyticsDashboardSkeleton() {
</div> </div>
</div> </div>
); );
} }

View File

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

View File

@@ -7,9 +7,9 @@ import {
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/common/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/lib/hooks/use-toast";
import { RefreshCw } from "lucide-react"; import { RefreshCw } from "lucide-react";
import { import {
getGrowthAnalyticsWithStore, getGrowthAnalyticsWithStore,
@@ -26,6 +26,7 @@ import {
ResponsiveContainer, ResponsiveContainer,
Area, Area,
} from "recharts"; } from "recharts";
import { formatGBP, formatNumber } from "@/lib/utils/format";
interface GrowthAnalyticsChartProps { interface GrowthAnalyticsChartProps {
hideNumbers?: boolean; hideNumbers?: boolean;
@@ -63,14 +64,6 @@ export default function GrowthAnalyticsChart({
fetchGrowthData(); fetchGrowthData();
}; };
const formatCurrency = (value: number) => {
if (hideNumbers) return "£***";
return new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
maximumFractionDigits: 0,
}).format(value);
};
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -115,9 +108,7 @@ export default function GrowthAnalyticsChart({
Total Orders Total Orders
</div> </div>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{hideNumbers {hideNumbers ? "***" : formatNumber(growthData.cumulative.orders)}
? "***"
: growthData.cumulative.orders.toLocaleString()}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -127,7 +118,7 @@ export default function GrowthAnalyticsChart({
Total Revenue Total Revenue
</div> </div>
<div className="text-2xl font-bold text-green-600"> <div className="text-2xl font-bold text-green-600">
{formatCurrency(growthData.cumulative.revenue)} {hideNumbers ? "£***" : formatGBP(growthData.cumulative.revenue)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -137,9 +128,7 @@ export default function GrowthAnalyticsChart({
Customers Customers
</div> </div>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{hideNumbers {hideNumbers ? "***" : formatNumber(growthData.cumulative.customers)}
? "***"
: growthData.cumulative.customers.toLocaleString()}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -149,9 +138,7 @@ export default function GrowthAnalyticsChart({
Products Products
</div> </div>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{hideNumbers {hideNumbers ? "***" : formatNumber(growthData.cumulative.products)}
? "***"
: growthData.cumulative.products.toLocaleString()}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -161,7 +148,7 @@ export default function GrowthAnalyticsChart({
Avg Order Value Avg Order Value
</div> </div>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{formatCurrency(growthData.cumulative.avgOrderValue)} {hideNumbers ? "£***" : formatGBP(growthData.cumulative.avgOrderValue)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -236,16 +223,16 @@ export default function GrowthAnalyticsChart({
Orders:{" "} Orders:{" "}
{hideNumbers {hideNumbers
? "***" ? "***"
: data.orders.toLocaleString()} : formatNumber(data.orders)}
</p> </p>
<p className="text-sm text-green-600"> <p className="text-sm text-green-600">
Revenue: {formatCurrency(data.revenue)} Revenue: {hideNumbers ? "£***" : formatGBP(data.revenue)}
</p> </p>
<p className="text-sm text-purple-600"> <p className="text-sm text-purple-600">
Customers:{" "} Customers:{" "}
{hideNumbers {hideNumbers
? "***" ? "***"
: data.customers.toLocaleString()} : formatNumber(data.customers)}
</p> </p>
{data.newCustomers !== undefined && ( {data.newCustomers !== undefined && (
<p className="text-sm text-cyan-600"> <p className="text-sm text-cyan-600">
@@ -327,16 +314,16 @@ export default function GrowthAnalyticsChart({
)} )}
</td> </td>
<td className="text-right p-2"> <td className="text-right p-2">
{hideNumbers ? "***" : month.orders.toLocaleString()} {hideNumbers ? "***" : formatNumber(month.orders)}
</td> </td>
<td className="text-right p-2 text-green-600"> <td className="text-right p-2 text-green-600">
{formatCurrency(month.revenue)} {hideNumbers ? "£***" : formatGBP(month.revenue)}
</td> </td>
<td className="text-right p-2"> <td className="text-right p-2">
{hideNumbers ? "***" : month.customers.toLocaleString()} {hideNumbers ? "***" : formatNumber(month.customers)}
</td> </td>
<td className="text-right p-2"> <td className="text-right p-2">
{formatCurrency(month.avgOrderValue)} {hideNumbers ? "£***" : formatGBP(month.avgOrderValue)}
</td> </td>
<td className="text-right p-2"> <td className="text-right p-2">
{hideNumbers ? "***" : (month.newCustomers ?? 0)} {hideNumbers ? "***" : (month.newCustomers ?? 0)}
@@ -352,3 +339,5 @@ export default function GrowthAnalyticsChart({
</div> </div>
); );
} }

View File

@@ -1,6 +1,6 @@
"use client" "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 { TrendingUp, TrendingDown, Minus } from "lucide-react";
import { LucideIcon } from "lucide-react"; import { LucideIcon } from "lucide-react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
@@ -112,4 +112,4 @@ export default function MetricsCard({
</Card> </Card>
</motion.div> </motion.div>
); );
} }

View File

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

View File

@@ -7,16 +7,16 @@ import {
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/common/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/common/button";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/common/select";
import { import {
TrendingUp, TrendingUp,
TrendingDown, TrendingDown,
@@ -32,8 +32,8 @@ import {
Info, Info,
Download, Download,
} from "lucide-react"; } from "lucide-react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/lib/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import CountUp from "react-countup"; import CountUp from "react-countup";
import { import {
getPredictionsOverviewWithStore, getPredictionsOverviewWithStore,
@@ -41,7 +41,7 @@ import {
type PredictionsOverview, type PredictionsOverview,
type StockPredictionsResponse, type StockPredictionsResponse,
} from "@/lib/services/analytics-service"; } from "@/lib/services/analytics-service";
import { formatGBP } from "@/utils/format"; import { formatGBP } from "@/lib/utils/format";
import { import {
Table, Table,
TableBody, TableBody,
@@ -49,7 +49,7 @@ import {
TableHead, TableHead,
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/common/table";
import { format } from "date-fns"; import { format } from "date-fns";
import { import {
AreaChart, AreaChart,
@@ -65,9 +65,9 @@ import {
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/common/tooltip";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/common/alert";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/common/slider";
interface PredictionsChartProps { interface PredictionsChartProps {
timeRange?: number; timeRange?: number;
@@ -933,3 +933,5 @@ export default function PredictionsChart({
</Card > </Card >
); );
} }

View File

@@ -1,14 +1,14 @@
"use client" "use client"
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/common/table";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/lib/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { Package } from "lucide-react"; import { Package } from "lucide-react";
import { getProductPerformanceWithStore, type ProductPerformance } from "@/lib/services/analytics-service"; 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'; import { TableSkeleton } from './SkeletonLoaders';
export default function ProductPerformanceChart() { export default function ProductPerformanceChart() {
@@ -123,10 +123,10 @@ export default function ProductPerformanceChart() {
<TableRow key={product.productId}> <TableRow key={product.productId}>
<TableCell> <TableCell>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div <div
className="h-10 w-10 bg-cover bg-center rounded border flex-shrink-0" className="h-10 w-10 bg-cover bg-center rounded border flex-shrink-0"
style={{ style={{
backgroundImage: product.image backgroundImage: product.image
? `url(/api/products/${product.productId}/image)` ? `url(/api/products/${product.productId}/image)`
: 'none' : 'none'
}} }}
@@ -137,7 +137,7 @@ export default function ProductPerformanceChart() {
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-right font-medium"> <TableCell className="text-right font-medium">
{parseInt(product.totalSold.toFixed(0)).toLocaleString()} {product.unitType} {formatNumber(parseInt(product.totalSold.toFixed(0)))} {product.unitType}
</TableCell> </TableCell>
<TableCell className="text-right font-medium text-green-600"> <TableCell className="text-right font-medium text-green-600">
{formatGBP(product.totalRevenue)} {formatGBP(product.totalRevenue)}
@@ -155,4 +155,5 @@ export default function ProductPerformanceChart() {
</CardContent> </CardContent>
</Card> </Card>
); );
} }

View File

@@ -1,9 +1,9 @@
"use client" "use client"
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/common/badge";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/common/alert";
import { import {
TrendingUp, TrendingUp,
TrendingDown, TrendingDown,
@@ -14,10 +14,10 @@ import {
AlertTriangle, AlertTriangle,
Package Package
} from "lucide-react"; } from "lucide-react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/lib/hooks/use-toast";
import { formatGBP } from "@/utils/format"; import { formatGBP } from "@/lib/utils/format";
import { getProfitOverview, type ProfitOverview, type DateRange } from "@/lib/services/profit-analytics-service"; 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 { interface ProfitAnalyticsChartProps {
timeRange?: string; timeRange?: string;
@@ -379,3 +379,5 @@ export default function ProfitAnalyticsChart({ timeRange, dateRange, hideNumbers
</div> </div>
); );
} }

View File

@@ -1,12 +1,12 @@
"use client" "use client"
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/lib/hooks/use-toast";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { TrendingUp, DollarSign } from "lucide-react"; import { TrendingUp, DollarSign } from "lucide-react";
import { getRevenueTrendsWithStore, type RevenueData } from "@/lib/services/analytics-service"; 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 { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
import { ChartSkeleton } from './SkeletonLoaders'; import { ChartSkeleton } from './SkeletonLoaders';
@@ -239,4 +239,5 @@ export default function RevenueChart({ timeRange, hideNumbers = false }: Revenue
</CardContent> </CardContent>
</Card> </Card>
); );
} }

View File

@@ -1,20 +1,24 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/common/skeleton";
import { cn } from "@/lib/utils";
// Chart skeleton for revenue trends and order analytics // Chart skeleton for revenue trends and order analytics
export function ChartSkeleton({ export function ChartSkeleton({
title, title,
description, description,
icon: Icon, icon: Icon,
showStats = false showStats = false,
}: { className,
title: string; }: {
description: string; title: string;
description: string;
icon: any; icon: any;
showStats?: boolean; showStats?: boolean;
className?: string;
}) { }) {
return ( return (
<Card> <Card className={cn(className)}>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Icon className="h-5 w-5" /> <Icon className="h-5 w-5" />
@@ -26,7 +30,7 @@ export function ChartSkeleton({
<div className="space-y-6"> <div className="space-y-6">
{/* Chart area */} {/* Chart area */}
<div className="h-64 bg-muted/20 rounded-md animate-pulse" /> <div className="h-64 bg-muted/20 rounded-md animate-pulse" />
{/* Summary stats if applicable */} {/* Summary stats if applicable */}
{showStats && ( {showStats && (
<div className="grid grid-cols-3 gap-4 pt-4 border-t"> <div className="grid grid-cols-3 gap-4 pt-4 border-t">
@@ -45,15 +49,15 @@ export function ChartSkeleton({
} }
// Table skeleton for product performance // Table skeleton for product performance
export function TableSkeleton({ export function TableSkeleton({
title, title,
description, description,
icon: Icon, icon: Icon,
rows = 5, rows = 5,
columns = 5 columns = 5
}: { }: {
title: string; title: string;
description: string; description: string;
icon: any; icon: any;
rows?: number; rows?: number;
columns?: number; columns?: number;
@@ -75,7 +79,7 @@ export function TableSkeleton({
<Skeleton key={i} className="h-4 w-full" /> <Skeleton key={i} className="h-4 w-full" />
))} ))}
</div> </div>
{/* Table rows */} {/* Table rows */}
{[...Array(rows)].map((_, rowIndex) => ( {[...Array(rows)].map((_, rowIndex) => (
<div key={rowIndex} className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}> <div key={rowIndex} className="grid gap-4" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
@@ -96,13 +100,13 @@ export function TableSkeleton({
} }
// Customer insights skeleton with segments // Customer insights skeleton with segments
export function CustomerInsightsSkeleton({ export function CustomerInsightsSkeleton({
title, title,
description, description,
icon: Icon icon: Icon
}: { }: {
title: string; title: string;
description: string; description: string;
icon: any; icon: any;
}) { }) {
return ( return (
@@ -125,7 +129,7 @@ export function CustomerInsightsSkeleton({
</div> </div>
))} ))}
</div> </div>
{/* Top customers table */} {/* Top customers table */}
<div className="space-y-4"> <div className="space-y-4">
<Skeleton className="h-6 w-32" /> <Skeleton className="h-6 w-32" />
@@ -167,4 +171,4 @@ export function MetricsCardSkeleton() {
</CardContent> </CardContent>
</Card> </Card>
); );
} }

View File

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

View File

@@ -20,12 +20,12 @@ export function AnimatedStatsSection({ stats }: AnimatedStatsProps) {
return ( return (
<div className="mx-auto grid max-w-5xl grid-cols-1 gap-6 md:grid-cols-3"> <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="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"> <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"> <div className="mt-4 text-3xl font-bold text-white">
<AnimatedCounter <AnimatedCounter
value={stats.orders.completed} value={stats.orders.completed}
duration={2000} duration={2000}
suffix="+" suffix="+"
/> />
@@ -38,14 +38,14 @@ export function AnimatedStatsSection({ stats }: AnimatedStatsProps) {
</p> </p>
</div> </div>
</div> </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="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"> <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"> <div className="mt-4 text-3xl font-bold text-white">
<AnimatedCounter <AnimatedCounter
value={stats.vendors.total} value={stats.vendors.total}
duration={2000} duration={2000}
suffix="+" suffix="+"
/> />
@@ -58,14 +58,14 @@ export function AnimatedStatsSection({ stats }: AnimatedStatsProps) {
</p> </p>
</div> </div>
</div> </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="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"> <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"> <div className="mt-4 text-3xl font-bold text-white">
<AnimatedCounter <AnimatedCounter
value={stats.transactions.volume} value={stats.transactions.volume}
duration={2500} duration={2500}
formatter={formatCurrency} formatter={formatCurrency}
/> />
@@ -80,4 +80,4 @@ export function AnimatedStatsSection({ stats }: AnimatedStatsProps) {
</div> </div>
</div> </div>
); );
} }

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react" import { Search } from "lucide-react"
import { cn } from "@/lib/utils/styles"; import { cn } from "@/lib/utils/styles";
import { Dialog, DialogContent } from "@/components/ui/dialog" import { Dialog, DialogContent } from "@/components/common/dialog"
const Command = React.forwardRef< const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>, React.ElementRef<typeof CommandPrimitive>,
@@ -15,7 +15,7 @@ const Command = React.forwardRef<
<CommandPrimitive <CommandPrimitive
ref={ref} ref={ref}
className={cn( 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 className
)} )}
{...props} {...props}
@@ -26,8 +26,8 @@ Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => { const CommandDialog = ({ children, ...props }: DialogProps) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg"> <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-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"> <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} {children}
</Command> </Command>
</DialogContent> </DialogContent>
@@ -39,12 +39,12 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>, React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper=""> <div className="flex items-center border-b border-white/5 px-4 bg-white/5" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> <Search className="mr-3 h-5 w-5 shrink-0 text-primary opacity-70" />
<CommandPrimitive.Input <CommandPrimitive.Input
ref={ref} ref={ref}
className={cn( 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 className
)} )}
{...props} {...props}
@@ -60,7 +60,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} 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} {...props}
/> />
)) ))
@@ -115,7 +115,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item <CommandPrimitive.Item
ref={ref} ref={ref}
className={cn( 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 className
)} )}
{...props} {...props}
@@ -151,3 +151,4 @@ export {
CommandShortcut, CommandShortcut,
CommandSeparator, CommandSeparator,
} }

View File

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

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

View File

@@ -2,7 +2,7 @@ import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react" import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils/styles"; 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">) => ( const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav <nav
@@ -115,3 +115,4 @@ export {
PaginationNext, PaginationNext,
PaginationPrevious, 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 { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react" 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 { cn } from "@/lib/utils/styles";
import { Button } from "@/components/ui/button" import { Button } from "@/components/common/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/common/input"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/common/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet" import { Sheet, SheetContent } from "@/components/common/sheet"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/common/skeleton"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip" } from "@/components/common/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar:state" const SIDEBAR_COOKIE_NAME = "sidebar:state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
@@ -761,3 +761,5 @@ export {
SidebarTrigger, SidebarTrigger,
useSidebar, useSidebar,
} }

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