tr]:last:border-b-0",
46 | className
47 | )}
48 | {...props}
49 | />
50 | )
51 | }
52 |
53 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
54 | return (
55 |
63 | )
64 | }
65 |
66 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
67 | return (
68 | [role=checkbox]]:translate-y-[2px]",
72 | className
73 | )}
74 | {...props}
75 | />
76 | )
77 | }
78 |
79 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
80 | return (
81 | | [role=checkbox]]:translate-y-[2px]",
85 | className
86 | )}
87 | {...props}
88 | />
89 | )
90 | }
91 |
92 | function TableCaption({
93 | className,
94 | ...props
95 | }: React.ComponentProps<"caption">) {
96 | return (
97 |
102 | )
103 | }
104 |
105 | export {
106 | Table,
107 | TableHeader,
108 | TableBody,
109 | TableFooter,
110 | TableHead,
111 | TableRow,
112 | TableCell,
113 | TableCaption,
114 | }
115 |
--------------------------------------------------------------------------------
/public/placeholder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/admin/pages/products/AdminProductsPage.tsx:
--------------------------------------------------------------------------------
1 | import { AdminTitle } from '@/admin/components/AdminTitle';
2 | import { CustomFullScreenLoading } from '@/components/custom/CustomFullScreenLoading';
3 | import { CustomPagination } from '@/components/custom/CustomPagination';
4 | import { Button } from '@/components/ui/button';
5 | import {
6 | Table,
7 | TableHeader,
8 | TableRow,
9 | TableHead,
10 | TableBody,
11 | TableCell,
12 | } from '@/components/ui/table';
13 | import { currencyFormatter } from '@/lib/currency-formatter';
14 | import { useProducts } from '@/shop/hooks/useProducts';
15 | import { PencilIcon, PlusIcon } from 'lucide-react';
16 | import { Link } from 'react-router';
17 |
18 | export const AdminProductsPage = () => {
19 | const { data, isLoading } = useProducts();
20 |
21 | if (isLoading) {
22 | return ;
23 | }
24 |
25 | return (
26 | <>
27 |
28 |
32 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | Imagen
47 | Nombre
48 | Precio
49 | Categoría
50 | Inventario
51 | Tallas
52 | Acciones
53 |
54 |
55 |
56 | {data!.products.map((product) => (
57 |
58 |
59 |
64 |
65 |
66 |
70 | {product.title}
71 |
72 |
73 | {currencyFormatter(product.price)}
74 | {product.gender}
75 | {product.stock} stock
76 | {product.sizes.join(', ')}
77 |
78 | {/* Editar */}
79 |
80 |
81 |
82 |
83 |
84 | ))}
85 |
86 |
87 |
88 |
89 | >
90 | );
91 | };
92 |
--------------------------------------------------------------------------------
/src/mocks/products.mock.ts:
--------------------------------------------------------------------------------
1 | // Import all product images
2 | import blackTshirt from '@/assets/product-black-tshirt.jpg';
3 | import whiteHoodie from '@/assets/product-white-hoodie.jpg';
4 | import greySweatshirt from '@/assets/product-grey-sweatshirt.jpg';
5 | import blackJacket from '@/assets/product-black-jacket.jpg';
6 | import whiteCap from '@/assets/product-white-cap.jpg';
7 | import blackBackpack from '@/assets/product-black-backpack.jpg';
8 |
9 | export interface Product {
10 | id: string;
11 | name: string;
12 | price: number;
13 | image: string;
14 | category: string;
15 | description: string;
16 | sizes: string[];
17 | colors: string[];
18 | }
19 |
20 | export const products: Product[] = [
21 | {
22 | id: '1',
23 | name: 'Camiseta Tesla Negro',
24 | price: 35,
25 | image: blackTshirt,
26 | category: 'Camisetas',
27 | description:
28 | 'Camiseta de algodón premium con diseño minimalista inspirado en Tesla.',
29 | sizes: ['S', 'M', 'L', 'XL', 'XXL'],
30 | colors: ['Negro', 'Blanco', 'Gris'],
31 | },
32 | {
33 | id: '2',
34 | name: 'Sudadera Tesla Blanca',
35 | price: 85,
36 | image: whiteHoodie,
37 | category: 'Sudaderas',
38 | description: 'Sudadera con capucha de alta calidad con logo Tesla bordado.',
39 | sizes: ['S', 'M', 'L', 'XL', 'XXL'],
40 | colors: ['Blanco', 'Negro', 'Gris'],
41 | },
42 | {
43 | id: '3',
44 | name: 'Sudadera Tesla Gris',
45 | price: 75,
46 | image: greySweatshirt,
47 | category: 'Sudaderas',
48 | description: 'Sudadera clásica sin capucha con corte moderno y cómodo.',
49 | sizes: ['S', 'M', 'L', 'XL'],
50 | colors: ['Gris', 'Negro', 'Azul Marino'],
51 | },
52 | {
53 | id: '4',
54 | name: 'Chaqueta Tesla Negro',
55 | price: 150,
56 | image: blackJacket,
57 | category: 'Chaquetas',
58 | description: 'Chaqueta técnica resistente al agua con diseño elegante.',
59 | sizes: ['S', 'M', 'L', 'XL', 'XXL'],
60 | colors: ['Negro', 'Gris Oscuro'],
61 | },
62 | {
63 | id: '5',
64 | name: 'Gorra Tesla Blanca',
65 | price: 25,
66 | image: whiteCap,
67 | category: 'Accesorios',
68 | description: 'Gorra ajustable con logo Tesla bordado en alta calidad.',
69 | sizes: ['Único'],
70 | colors: ['Blanco', 'Negro', 'Gris'],
71 | },
72 | {
73 | id: '6',
74 | name: 'Mochila Tesla Negro',
75 | price: 120,
76 | image: blackBackpack,
77 | category: 'Accesorios',
78 | description:
79 | 'Mochila minimalista con compartimentos organizados y diseño ergonómico.',
80 | sizes: ['Único'],
81 | colors: ['Negro', 'Gris'],
82 | },
83 | {
84 | id: '7',
85 | name: 'Camiseta Tesla Blanca',
86 | price: 35,
87 | image: blackTshirt, // Reutilizamos la imagen por simplicidad
88 | category: 'Camisetas',
89 | description: 'Camiseta blanca de algodón orgánico con logo Tesla discreto.',
90 | sizes: ['S', 'M', 'L', 'XL', 'XXL'],
91 | colors: ['Blanco', 'Negro', 'Gris'],
92 | },
93 | {
94 | id: '8',
95 | name: 'Sudadera Tesla Negro',
96 | price: 85,
97 | image: whiteHoodie,
98 | category: 'Sudaderas',
99 | description: 'Sudadera negra con capucha y bolsillo frontal tipo canguro.',
100 | sizes: ['S', 'M', 'L', 'XL', 'XXL'],
101 | colors: ['Negro', 'Gris', 'Azul Marino'],
102 | },
103 | ];
104 |
--------------------------------------------------------------------------------
/src/shop/components/CustomFooter.tsx:
--------------------------------------------------------------------------------
1 | import { CustomLogo } from '@/components/custom/CustomLogo';
2 |
3 | export const CustomFooter = () => {
4 | return (
5 |
103 | );
104 | };
105 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/admin/components/AdminSidebar.tsx:
--------------------------------------------------------------------------------
1 | import { Link, useLocation } from 'react-router';
2 | import {
3 | Home,
4 | Users,
5 | BarChart3,
6 | Settings,
7 | FileText,
8 | ShoppingCart,
9 | Bell,
10 | HelpCircle,
11 | ChevronLeft,
12 | ChevronRight,
13 | } from 'lucide-react';
14 | import { CustomLogo } from '@/components/custom/CustomLogo';
15 | import { useAuthStore } from '@/auth/store/auth.store';
16 |
17 | interface SidebarProps {
18 | isCollapsed: boolean;
19 | onToggle: () => void;
20 | }
21 |
22 | export const AdminSidebar: React.FC = ({
23 | isCollapsed,
24 | onToggle,
25 | }) => {
26 | const { pathname } = useLocation();
27 | const { user } = useAuthStore();
28 |
29 | const menuItems = [
30 | { icon: Home, label: 'Dashboard', to: '/admin' },
31 | { icon: BarChart3, label: 'Productos', to: '/admin/products' },
32 | { icon: Users, label: 'Usuarios' },
33 | { icon: ShoppingCart, label: 'Ordenes' },
34 | { icon: FileText, label: 'Reportes' },
35 | { icon: Bell, label: 'Notificaciones' },
36 | { icon: Settings, label: 'Ajustes' },
37 | { icon: HelpCircle, label: 'Ayuda' },
38 | ];
39 |
40 | const isActiveRoute = (to: string) => {
41 | // TODO: ajustarlo cuando estemos en la pantalla de producto
42 | if (pathname.includes('/admin/products/') && to === '/admin/products') {
43 | return true;
44 | }
45 |
46 | return pathname === to; // true, false
47 | };
48 |
49 | return (
50 |
55 | {/* Header */}
56 |
57 | {!isCollapsed && }
58 |
64 |
65 |
66 | {/* Navigation */}
67 |
91 |
92 | {/* User Profile */}
93 | {!isCollapsed && (
94 |
95 |
96 |
97 | {user?.fullName.substring(0, 2)}
98 |
99 |
100 |
101 | {user?.fullName}
102 |
103 | {user?.email}
104 |
105 |
106 |
107 | )}
108 |
109 | );
110 | };
111 |
--------------------------------------------------------------------------------
/src/shop/components/ProductsGrid.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import { Filter, Grid, List } from 'lucide-react';
3 | import { ProductCard } from './ProductCard';
4 | import { FilterSidebar } from './FilterSidebar';
5 | import { useSearchParams } from 'react-router';
6 | import { useState } from 'react';
7 | import type { Product } from '@/interfaces/product.interface';
8 |
9 | interface Props {
10 | products: Product[];
11 | }
12 |
13 | export const ProductsGrid = ({ products }: Props) => {
14 | const [searchParams, setSearchParams] = useSearchParams();
15 |
16 | const [showFilters, setShowFilters] = useState(false);
17 | const viewMode = searchParams.get('viewMode') || 'grid';
18 |
19 | const handleViewModeChange = (mode: 'grid' | 'list') => {
20 | searchParams.set('viewMode', mode);
21 | setSearchParams(searchParams);
22 | };
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | Productos
30 |
31 | ({products.length} productos)
32 |
33 |
34 |
35 |
36 |
45 |
46 |
47 |
55 |
63 |
64 |
65 |
66 |
67 |
68 | {/* Filters Sidebar - Desktop */}
69 |
70 |
71 |
72 |
73 | {/* Mobile Filters */}
74 | {showFilters && (
75 |
76 |
77 | Filtros
78 |
85 |
86 |
87 |
88 | )}
89 |
90 | {/* Products Grid */}
91 |
92 |
99 | {products.map((product) => (
100 |
109 | ))}
110 |
111 |
112 |
113 |
114 |
115 | );
116 | };
117 |
--------------------------------------------------------------------------------
/src/shop/components/FilterSidebar.tsx:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'react-router';
2 |
3 | import { Button } from '@/components/ui/button';
4 | import { Separator } from '@/components/ui/separator';
5 | import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
6 | import { Label } from '@/components/ui/label';
7 |
8 | export const FilterSidebar = () => {
9 | const [searchParams, setSearchParams] = useSearchParams();
10 |
11 | const currentSizes = searchParams.get('sizes')?.split(',') || []; // xs,l,xl
12 | const currentPrice = searchParams.get('price') || 'any';
13 |
14 | const handleSizeChanged = (size: string) => {
15 | const newSizes = currentSizes.includes(size)
16 | ? currentSizes.filter((s) => s !== size)
17 | : [...currentSizes, size];
18 |
19 | searchParams.set('page', '1');
20 | searchParams.set('sizes', newSizes.join(','));
21 | setSearchParams(searchParams);
22 | };
23 |
24 | const handlePriceChange = (price: string) => {
25 | searchParams.set('page', '1');
26 | searchParams.set('price', price);
27 | setSearchParams(searchParams);
28 | };
29 |
30 | const sizes = [
31 | { id: 'xs', label: 'XS' },
32 | { id: 's', label: 'S' },
33 | { id: 'm', label: 'M' },
34 | { id: 'l', label: 'L' },
35 | { id: 'xl', label: 'XL' },
36 | { id: 'xxl', label: 'XXL' },
37 | ];
38 |
39 | return (
40 |
41 |
42 | Filtros
43 |
44 |
45 | {/* Sizes */}
46 |
47 | Tallas
48 |
49 | {sizes.map((size) => (
50 |
59 | ))}
60 |
61 |
62 |
63 |
64 |
65 | {/* Price Range */}
66 |
67 | Precio
68 |
69 |
70 | handlePriceChange('any')}
75 | />
76 |
79 |
80 |
81 | handlePriceChange('0-50')}
86 | />
87 |
90 |
91 |
92 | handlePriceChange('50-100')}
97 | />
98 |
101 |
102 |
103 | handlePriceChange('100-200')}
108 | />
109 |
112 |
113 |
114 | handlePriceChange('200+')}
119 | />
120 |
123 |
124 |
125 |
126 |
127 | );
128 | };
129 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 | @import 'tw-animate-css';
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | @theme inline {
7 | --radius-sm: calc(var(--radius) - 4px);
8 | --radius-md: calc(var(--radius) - 2px);
9 | --radius-lg: var(--radius);
10 | --radius-xl: calc(var(--radius) + 4px);
11 | --color-background: var(--background);
12 | --color-foreground: var(--foreground);
13 | --color-card: var(--card);
14 | --color-card-foreground: var(--card-foreground);
15 | --color-popover: var(--popover);
16 | --color-popover-foreground: var(--popover-foreground);
17 | --color-primary: var(--primary);
18 | --color-primary-foreground: var(--primary-foreground);
19 | --color-secondary: var(--secondary);
20 | --color-secondary-foreground: var(--secondary-foreground);
21 | --color-muted: var(--muted);
22 | --color-muted-foreground: var(--muted-foreground);
23 | --color-accent: var(--accent);
24 | --color-accent-foreground: var(--accent-foreground);
25 | --color-destructive: var(--destructive);
26 | --color-border: var(--border);
27 | --color-input: var(--input);
28 | --color-ring: var(--ring);
29 | --color-chart-1: var(--chart-1);
30 | --color-chart-2: var(--chart-2);
31 | --color-chart-3: var(--chart-3);
32 | --color-chart-4: var(--chart-4);
33 | --color-chart-5: var(--chart-5);
34 | --color-sidebar: var(--sidebar);
35 | --color-sidebar-foreground: var(--sidebar-foreground);
36 | --color-sidebar-primary: var(--sidebar-primary);
37 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
38 | --color-sidebar-accent: var(--sidebar-accent);
39 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
40 | --color-sidebar-border: var(--sidebar-border);
41 | --color-sidebar-ring: var(--sidebar-ring);
42 | }
43 |
44 | :root {
45 | --radius: 0.625rem;
46 | --background: oklch(1 0 0);
47 | --foreground: oklch(0.145 0 0);
48 | --card: oklch(1 0 0);
49 | --card-foreground: oklch(0.145 0 0);
50 | --popover: oklch(1 0 0);
51 | --popover-foreground: oklch(0.145 0 0);
52 | --primary: oklch(0.205 0 0);
53 | --primary-foreground: oklch(0.985 0 0);
54 | --secondary: oklch(0.97 0 0);
55 | --secondary-foreground: oklch(0.205 0 0);
56 | --muted: oklch(0.97 0 0);
57 | --muted-foreground: oklch(0.556 0 0);
58 | --accent: oklch(0.97 0 0);
59 | --accent-foreground: oklch(0.205 0 0);
60 | --destructive: oklch(0.577 0.245 27.325);
61 | --border: oklch(0.922 0 0);
62 | --input: oklch(0.922 0 0);
63 | --ring: oklch(0.708 0 0);
64 | --chart-1: oklch(0.646 0.222 41.116);
65 | --chart-2: oklch(0.6 0.118 184.704);
66 | --chart-3: oklch(0.398 0.07 227.392);
67 | --chart-4: oklch(0.828 0.189 84.429);
68 | --chart-5: oklch(0.769 0.188 70.08);
69 | --sidebar: oklch(0.985 0 0);
70 | --sidebar-foreground: oklch(0.145 0 0);
71 | --sidebar-primary: oklch(0.205 0 0);
72 | --sidebar-primary-foreground: oklch(0.985 0 0);
73 | --sidebar-accent: oklch(0.97 0 0);
74 | --sidebar-accent-foreground: oklch(0.205 0 0);
75 | --sidebar-border: oklch(0.922 0 0);
76 | --sidebar-ring: oklch(0.708 0 0);
77 | }
78 |
79 | .dark {
80 | --background: oklch(0.145 0 0);
81 | --foreground: oklch(0.985 0 0);
82 | --card: oklch(0.205 0 0);
83 | --card-foreground: oklch(0.985 0 0);
84 | --popover: oklch(0.205 0 0);
85 | --popover-foreground: oklch(0.985 0 0);
86 | --primary: oklch(0.922 0 0);
87 | --primary-foreground: oklch(0.205 0 0);
88 | --secondary: oklch(0.269 0 0);
89 | --secondary-foreground: oklch(0.985 0 0);
90 | --muted: oklch(0.269 0 0);
91 | --muted-foreground: oklch(0.708 0 0);
92 | --accent: oklch(0.269 0 0);
93 | --accent-foreground: oklch(0.985 0 0);
94 | --destructive: oklch(0.704 0.191 22.216);
95 | --border: oklch(1 0 0 / 10%);
96 | --input: oklch(1 0 0 / 15%);
97 | --ring: oklch(0.556 0 0);
98 | --chart-1: oklch(0.488 0.243 264.376);
99 | --chart-2: oklch(0.696 0.17 162.48);
100 | --chart-3: oklch(0.769 0.188 70.08);
101 | --chart-4: oklch(0.627 0.265 303.9);
102 | --chart-5: oklch(0.645 0.246 16.439);
103 | --sidebar: oklch(0.205 0 0);
104 | --sidebar-foreground: oklch(0.985 0 0);
105 | --sidebar-primary: oklch(0.488 0.243 264.376);
106 | --sidebar-primary-foreground: oklch(0.985 0 0);
107 | --sidebar-accent: oklch(0.269 0 0);
108 | --sidebar-accent-foreground: oklch(0.985 0 0);
109 | --sidebar-border: oklch(1 0 0 / 10%);
110 | --sidebar-ring: oklch(0.556 0 0);
111 | }
112 |
113 | @layer base {
114 | * {
115 | @apply border-border outline-ring/50;
116 | }
117 | body {
118 | @apply bg-background text-foreground;
119 | }
120 | }
121 |
122 | @theme {
123 | --font-montserrat: 'Montserrat Alternates', sans-serif;
124 | }
125 |
--------------------------------------------------------------------------------
/src/shop/components/CustomHeader.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, type KeyboardEvent } from 'react';
2 | import { Search } from 'lucide-react';
3 | import { Button } from '@/components/ui/button';
4 | import { Input } from '@/components/ui/input';
5 | import { Link, useParams, useSearchParams } from 'react-router';
6 | import { cn } from '@/lib/utils';
7 | import { CustomLogo } from '@/components/custom/CustomLogo';
8 |
9 | import { useAuthStore } from '@/auth/store/auth.store';
10 |
11 | export const CustomHeader = () => {
12 | const [searchParams, setSearchParams] = useSearchParams();
13 | const { authStatus, isAdmin, logout } = useAuthStore();
14 |
15 | const { gender } = useParams();
16 |
17 | const inputRef = useRef(null);
18 | const query = searchParams.get('query') || '';
19 |
20 | const handleSearch = (event: KeyboardEvent) => {
21 | if (event.key !== 'Enter') return;
22 | const query = inputRef.current?.value;
23 |
24 | const newSearchParams = new URLSearchParams();
25 |
26 | if (!query) {
27 | newSearchParams.delete('query');
28 | } else {
29 | newSearchParams.set('query', inputRef.current!.value);
30 | }
31 |
32 | setSearchParams(newSearchParams);
33 | };
34 |
35 | return (
36 |
37 |
38 |
39 | {/* Logo */}
40 |
41 |
42 | {/* Navigation - Desktop */}
43 |
81 |
82 | {/* Search and Cart */}
83 |
84 |
96 |
97 |
100 |
101 | {authStatus === 'not-authenticated' ? (
102 |
103 |
106 |
107 | ) : (
108 |
116 | )}
117 |
118 | {isAdmin() && (
119 |
120 |
128 |
129 | )}
130 |
131 |
132 |
133 |
134 | );
135 | };
136 |
--------------------------------------------------------------------------------
/src/admin/pages/dashboard/DashboardPage.tsx:
--------------------------------------------------------------------------------
1 | import ActivityFeed from '@/admin/components/ActivityFeed';
2 | import { AdminTitle } from '@/admin/components/AdminTitle';
3 | import Chart from '@/admin/components/Chart';
4 | import QuickActions from '@/admin/components/QuickActions';
5 | import StatCard from '@/admin/components/StatCard';
6 |
7 | import {
8 | Users,
9 | DollarSign,
10 | ShoppingCart,
11 | TrendingUp,
12 | Eye,
13 | BarChart3,
14 | } from 'lucide-react';
15 |
16 | const stats = [
17 | {
18 | title: 'Total Users',
19 | value: '24,567',
20 | change: '+12.5% from last month',
21 | changeType: 'positive' as const,
22 | icon: Users,
23 | color: 'bg-blue-500',
24 | },
25 | {
26 | title: 'Revenue',
27 | value: '$84,230',
28 | change: '+8.2% from last month',
29 | changeType: 'positive' as const,
30 | icon: DollarSign,
31 | color: 'bg-green-500',
32 | },
33 | {
34 | title: 'Orders',
35 | value: '1,429',
36 | change: '-2.4% from last month',
37 | changeType: 'negative' as const,
38 | icon: ShoppingCart,
39 | color: 'bg-purple-500',
40 | },
41 | {
42 | title: 'Conversion Rate',
43 | value: '3.24%',
44 | change: '+0.3% from last month',
45 | changeType: 'positive' as const,
46 | icon: TrendingUp,
47 | color: 'bg-orange-500',
48 | },
49 | ];
50 |
51 | const chartData = [
52 | { label: 'Desktop', value: 65 },
53 | { label: 'Mobile', value: 28 },
54 | { label: 'Tablet', value: 7 },
55 | ];
56 |
57 | const performanceData = [
58 | { label: 'Page Views', value: 24567 },
59 | { label: 'Sessions', value: 18234 },
60 | { label: 'Users', value: 12847 },
61 | { label: 'Bounce Rate', value: 23 },
62 | ];
63 |
64 | export const DashboardPage = () => {
65 | return (
66 | <>
67 | {/* Welcome Section */}
68 |
72 |
73 | {/* Stats Grid */}
74 |
75 | {stats.map((stat, index) => (
76 |
77 | ))}
78 |
79 |
80 | {/* Charts and Activity Section */}
81 |
82 |
83 |
84 |
85 |
86 |
87 |
91 |
92 |
93 | {/* Additional Dashboard Section */}
94 |
95 |
96 |
97 | Top Pages
98 |
99 |
100 |
101 | {[
102 | { page: '/dashboard', views: 2847, change: '+12%' },
103 | { page: '/products', views: 1923, change: '+8%' },
104 | { page: '/analytics', views: 1456, change: '+15%' },
105 | { page: '/settings', views: 987, change: '-3%' },
106 | ].map((item, index) => (
107 |
111 |
112 | {item.page}
113 |
114 | {item.views.toLocaleString()} views
115 |
116 |
117 |
124 | {item.change}
125 |
126 |
127 | ))}
128 |
129 |
130 |
131 |
132 |
133 |
134 | System Status
135 |
136 |
137 |
138 |
139 | {[
140 | {
141 | service: 'API Server',
142 | status: 'Online',
143 | uptime: '99.9%',
144 | color: 'bg-green-500',
145 | },
146 | {
147 | service: 'Database',
148 | status: 'Online',
149 | uptime: '99.8%',
150 | color: 'bg-green-500',
151 | },
152 | {
153 | service: 'Cache Server',
154 | status: 'Warning',
155 | uptime: '98.2%',
156 | color: 'bg-yellow-500',
157 | },
158 | {
159 | service: 'CDN',
160 | status: 'Online',
161 | uptime: '99.9%',
162 | color: 'bg-green-500',
163 | },
164 | ].map((item, index) => (
165 |
166 |
167 |
168 |
169 | {item.service}
170 | {item.status}
171 |
172 |
173 |
174 | {item.uptime}
175 |
176 |
177 | ))}
178 |
179 |
180 |
181 | >
182 | );
183 | };
184 |
--------------------------------------------------------------------------------
/src/auth/pages/register/RegisterPage.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import { Card, CardContent } from '@/components/ui/card';
3 | import { Input } from '@/components/ui/input';
4 | import { Label } from '@/components/ui/label';
5 | import { CustomLogo } from '@/components/custom/CustomLogo';
6 | import { Link } from 'react-router';
7 |
8 | export const RegisterPage = () => {
9 | return (
10 |
11 |
12 |
13 |
103 |
104 | 
109 |
110 |
111 |
112 |
117 |
118 | );
119 | };
120 |
--------------------------------------------------------------------------------
/src/auth/pages/login/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import { useState, type FormEvent } from 'react';
2 | import { Link, useNavigate } from 'react-router';
3 | import { toast } from 'sonner';
4 |
5 | import { Button } from '@/components/ui/button';
6 | import { Card, CardContent } from '@/components/ui/card';
7 | import { Input } from '@/components/ui/input';
8 | import { Label } from '@/components/ui/label';
9 | import { CustomLogo } from '@/components/custom/CustomLogo';
10 |
11 | import { useAuthStore } from '@/auth/store/auth.store';
12 |
13 | export const LoginPage = () => {
14 | const navigate = useNavigate();
15 | const { login } = useAuthStore();
16 |
17 | const [isPosting, setIsPosting] = useState(false);
18 |
19 | const handleLogin = async (event: FormEvent) => {
20 | event.preventDefault();
21 | setIsPosting(true);
22 |
23 | const formData = new FormData(event.target as HTMLFormElement);
24 | const email = formData.get('email') as string;
25 | const password = formData.get('password') as string;
26 |
27 | const isValid = await login(email, password);
28 |
29 | if (isValid) {
30 | navigate('/');
31 | return;
32 | }
33 |
34 | toast.error('Correo o/y contraseña no validos');
35 | setIsPosting(false);
36 | };
37 |
38 | return (
39 |
40 |
41 |
42 |
127 |
128 | 
133 |
134 |
135 |
136 |
141 |
142 | );
143 | };
144 |
--------------------------------------------------------------------------------
/src/admin/pages/product/ui/ProductForm.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import { Link } from 'react-router';
3 |
4 | import { useForm } from 'react-hook-form';
5 |
6 | import { AdminTitle } from '@/admin/components/AdminTitle';
7 |
8 | import { Button } from '@/components/ui/button';
9 | import type { Product, Size } from '@/interfaces/product.interface';
10 | import { X, SaveAll, Tag, Plus, Upload } from 'lucide-react';
11 | import { cn } from '@/lib/utils';
12 |
13 | interface Props {
14 | title: string;
15 | subTitle: string;
16 | product: Product;
17 | isPending: boolean;
18 |
19 | // Methods
20 | onSubmit: (
21 | productLike: Partial & { files?: File[] }
22 | ) => Promise;
23 | }
24 |
25 | const availableSizes: Size[] = ['XS', 'S', 'M', 'L', 'XL', 'XXL'];
26 |
27 | interface FormInputs extends Product {
28 | files?: File[];
29 | }
30 |
31 | export const ProductForm = ({
32 | title,
33 | subTitle,
34 | product,
35 | onSubmit,
36 | isPending,
37 | }: Props) => {
38 | const [dragActive, setDragActive] = useState(false);
39 | const {
40 | register,
41 | handleSubmit,
42 | formState: { errors },
43 | getValues,
44 | setValue,
45 | watch,
46 | } = useForm({
47 | defaultValues: product,
48 | });
49 |
50 | const labelInputRef = useRef(null);
51 | const [files, setFiles] = useState([]);
52 |
53 | useEffect(() => {
54 | setFiles([]);
55 | }, [product]);
56 |
57 | const selectedSizes = watch('sizes');
58 | const selectedTags = watch('tags');
59 | const currentStock = watch('stock');
60 |
61 | const addTag = () => {
62 | const newTag = labelInputRef.current!.value;
63 | if (newTag === '') return;
64 |
65 | const newTagSet = new Set(getValues('tags'));
66 | newTagSet.add(newTag);
67 | setValue('tags', Array.from(newTagSet));
68 | };
69 |
70 | const removeTag = (tag: string) => {
71 | const newTagSet = new Set(getValues('tags'));
72 | newTagSet.delete(tag);
73 | setValue('tags', Array.from(newTagSet));
74 | };
75 |
76 | const addSize = (size: Size) => {
77 | const sizeSet = new Set(getValues('sizes'));
78 | sizeSet.add(size);
79 | setValue('sizes', Array.from(sizeSet));
80 | };
81 |
82 | const removeSize = (size: Size) => {
83 | const sizeSet = new Set(getValues('sizes'));
84 | sizeSet.delete(size);
85 | setValue('sizes', Array.from(sizeSet));
86 | };
87 |
88 | const handleDrag = (e: React.DragEvent) => {
89 | e.preventDefault();
90 | e.stopPropagation();
91 | if (e.type === 'dragenter' || e.type === 'dragover') {
92 | setDragActive(true);
93 | } else if (e.type === 'dragleave') {
94 | setDragActive(false);
95 | }
96 | };
97 |
98 | const handleDrop = (e: React.DragEvent) => {
99 | e.preventDefault();
100 | e.stopPropagation();
101 | setDragActive(false);
102 | const files = e.dataTransfer.files;
103 |
104 | if (!files) return;
105 |
106 | setFiles((prev) => [...prev, ...Array.from(files)]);
107 |
108 | const currentFiles = getValues('files') || [];
109 | setValue('files', [...currentFiles, ...Array.from(files)]);
110 | };
111 |
112 | const handleFileChange = (e: React.ChangeEvent) => {
113 | const files = e.target.files;
114 | if (!files) return;
115 |
116 | setFiles((prev) => [...prev, ...Array.from(files)]);
117 | const currentFiles = getValues('files') || [];
118 | setValue('files', [...currentFiles, ...Array.from(files)]);
119 | };
120 |
121 | return (
122 |
547 | );
548 | };
549 |
--------------------------------------------------------------------------------
|