optimization

This commit is contained in:
g
2026-01-11 07:35:52 +00:00
parent 027cc49430
commit 0fba981bca
8 changed files with 339 additions and 289 deletions

View File

@@ -5,6 +5,14 @@ export default function Document() {
<Html lang="en">
<Head>
<meta charSet="utf-8" />
{/* Font optimization */}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
{/* Audio preload */}
<link rel="preload" href="/notification.mp3" as="audio" type="audio/mpeg" />
</Head>
<body>

View File

@@ -1,12 +1,13 @@
"use client";
import { useState } from "react";
import { useState, useMemo } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Search, Filter, Eye, ChevronLeft, ChevronRight } from "lucide-react";
import { FixedSizeList as List } from 'react-window';
import OrderDetailsModal from "./OrderDetailsModal";
interface Order {
@@ -57,30 +58,85 @@ const getStatusStyle = (status: string) => {
* This component should only be used in admin contexts
*/
export default function OrdersTable({ orders, enableModal = true }: OrdersTableProps) {
const [currentPage, setCurrentPage] = useState(1);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [selectedOrderId, setSelectedOrderId] = useState<number | string | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [selectedOrderId, setSelectedOrderId] = useState<string | number | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const itemsPerPage = 10;
const itemsPerPage = 20;
// Filter orders based on search and status
const filteredOrders = orders.filter(order => {
const matchesSearch = order.orderId.toString().toLowerCase().includes(searchTerm.toLowerCase()) ||
order.userId.toLowerCase().includes(searchTerm.toLowerCase()) ||
(order.vendorUsername && order.vendorUsername.toLowerCase().includes(searchTerm.toLowerCase()));
const filteredOrders = useMemo(() => {
return orders.filter((order) => {
const matchesSearch = searchTerm === "" ||
order.orderId.toString().toLowerCase().includes(searchTerm.toLowerCase()) ||
order.userId.toLowerCase().includes(searchTerm.toLowerCase()) ||
(order.vendorUsername && order.vendorUsername.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesStatus = statusFilter === "all" || order.status === statusFilter;
const matchesStatus = statusFilter === "all" || order.status === statusFilter;
return matchesSearch && matchesStatus;
});
return matchesSearch && matchesStatus;
});
}, [orders, searchTerm, statusFilter]);
// Calculate pagination
// Pagination calculations
const totalPages = Math.ceil(filteredOrders.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentOrders = filteredOrders.slice(startIndex, endIndex);
// Virtual scrolling row renderer
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const order = currentOrders[index];
if (!order) return null;
return (
<div style={style}>
<TableRow>
<TableCell className="font-medium">{order.orderId}</TableCell>
<TableCell>{order.userId}</TableCell>
<TableCell>{order.vendorUsername || 'N/A'}</TableCell>
<TableCell className="max-w-[200px] truncate">
{order.items.length > 0 ? order.items[0].name : 'No items'}
</TableCell>
<TableCell>£{order.total.toFixed(2)}</TableCell>
<TableCell>
<div className={`px-3 py-1 rounded-full border ${getStatusStyle(order.status)}`}>
{order.status.toUpperCase()}
</div>
</TableCell>
<TableCell>N/A</TableCell>
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right">
{enableModal ? (
<Button
variant="outline"
size="sm"
onClick={() => handleViewOrder(order.orderId)}
title="View order details (Admin only)"
>
<Eye className="h-4 w-4" />
</Button>
) : (
<Button
variant="outline"
size="sm"
disabled
title="Order details modal disabled"
>
<Eye className="h-4 w-4" />
</Button>
)}
</TableCell>
</TableRow>
</div>
);
};
// Determine if we should use virtual scrolling (for performance with large datasets)
const useVirtualScrolling = currentOrders.length > 50;
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
@@ -152,47 +208,58 @@ export default function OrdersTable({ orders, enableModal = true }: OrdersTableP
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{currentOrders.map((order) => (
<TableRow key={order.orderId}>
<TableCell className="font-medium">{order.orderId}</TableCell>
<TableCell>{order.userId}</TableCell>
<TableCell>{order.vendorUsername || 'N/A'}</TableCell>
<TableCell className="max-w-[200px] truncate">
{order.items.length > 0 ? order.items[0].name : 'No items'}
</TableCell>
<TableCell>£{order.total.toFixed(2)}</TableCell>
<TableCell>
<div className={`px-3 py-1 rounded-full border ${getStatusStyle(order.status)}`}>
{order.status.toUpperCase()}
</div>
</TableCell>
<TableCell>N/A</TableCell>
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right">
{enableModal ? (
<Button
variant="outline"
size="sm"
onClick={() => handleViewOrder(order.orderId)}
title="View order details (Admin only)"
>
<Eye className="h-4 w-4" />
</Button>
) : (
<Button
variant="outline"
size="sm"
disabled
title="Order details modal disabled"
>
<Eye className="h-4 w-4" />
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
{useVirtualScrolling ? (
<List
height={400}
itemCount={currentOrders.length}
itemSize={60}
className="border"
>
{Row}
</List>
) : (
<TableBody>
{currentOrders.map((order) => (
<TableRow key={order.orderId}>
<TableCell className="font-medium">{order.orderId}</TableCell>
<TableCell>{order.userId}</TableCell>
<TableCell>{order.vendorUsername || 'N/A'}</TableCell>
<TableCell className="max-w-[200px] truncate">
{order.items.length > 0 ? order.items[0].name : 'No items'}
</TableCell>
<TableCell>£{order.total.toFixed(2)}</TableCell>
<TableCell>
<div className={`px-3 py-1 rounded-full border ${getStatusStyle(order.status)}`}>
{order.status.toUpperCase()}
</div>
</TableCell>
<TableCell>N/A</TableCell>
<TableCell>{new Date(order.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right">
{enableModal ? (
<Button
variant="outline"
size="sm"
onClick={() => handleViewOrder(order.orderId)}
title="View order details (Admin only)"
>
<Eye className="h-4 w-4" />
</Button>
) : (
<Button
variant="outline"
size="sm"
disabled
title="Order details modal disabled"
>
<Eye className="h-4 w-4" />
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
)}
</Table>
{/* Pagination */}

View File

@@ -47,7 +47,8 @@ import { DateRange } from "react-day-picker";
import { addDays, startOfDay, endOfDay } from "date-fns";
import type { DateRange as ProfitDateRange } from "@/lib/services/profit-analytics-service";
// Lazy load chart components
// Lazy load chart components - already handled individually below
const RevenueChart = dynamic(() => import("./RevenueChart"), {
loading: () => <ChartSkeleton />,
});

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, memo, useMemo, useCallback } from "react";
import {
Card,
CardContent,

View File

@@ -5,8 +5,8 @@ const withBundleAnalyzer = bundleAnalyzer({ enabled: process.env.ANALYZE === 'tr
/** @type {import('next').NextConfig} */
const baseConfig = {
output: 'standalone',
reactStrictMode: false,
compress: process.env.NODE_ENV === 'production',
// Enable compression for better performance
compress: true,
images: {
remotePatterns: [
{
@@ -58,10 +58,10 @@ const baseConfig = {
transform: 'date-fns/{{member}}',
},
},
// Webpack config (fallback if using --webpack flag)
// Enhanced webpack config for better bundle splitting
webpack: (config, { isServer, dev }) => {
if (!isServer && !dev) {
// Only apply aggressive code splitting in production
// Enhanced code splitting for better performance
config.optimization = {
...config.optimization,
moduleIds: 'deterministic',
@@ -70,41 +70,62 @@ const baseConfig = {
cacheGroups: {
default: false,
vendors: false,
recharts: {
test: /[\\/]node_modules[\\/]recharts[\\/]/,
name: 'recharts',
// Separate React and related libraries
react: {
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
name: 'react',
chunks: 'all',
priority: 30,
priority: 50,
enforce: true,
},
// Separate UI library components
radix: {
test: /[\\/]node_modules[\\/]@radix-ui[\\/]/,
name: 'radix-ui',
chunks: 'all',
priority: 40,
},
// Separate charting libraries
charts: {
test: /[\\/]node_modules[\\/](recharts|d3-.*)[\\/]/,
name: 'charts',
chunks: 'all',
priority: 35,
},
// Separate icon libraries
icons: {
test: /[\\/]node_modules[\\/]lucide-react[\\/]/,
name: 'icons',
chunks: 'all',
priority: 30,
},
// Separate date utilities
dateUtils: {
test: /[\\/]node_modules[\\/]date-fns[\\/]/,
name: 'date-utils',
chunks: 'all',
priority: 25,
},
reactMarkdown: {
test: /[\\/]node_modules[\\/]react-markdown[\\/]/,
name: 'react-markdown',
// Separate form libraries
forms: {
test: /[\\/]node_modules[\\/](react-hook-form|@hookform)[\\/]/,
name: 'forms',
chunks: 'all',
priority: 20,
},
dateFns: {
test: /[\\/]node_modules[\\/]date-fns[\\/]/,
name: 'date-fns',
// Group remaining vendor libraries
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
priority: 15,
},
framework: {
name: 'framework',
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
priority: 40,
priority: 10,
enforce: true,
},
commons: {
name: 'commons',
minChunks: 2,
priority: 10,
// Common application code
common: {
name: 'common',
minChunks: 3,
priority: 5,
},
},
},
@@ -112,10 +133,6 @@ const baseConfig = {
}
return config;
},
onDemandEntries: {
maxInactiveAge: 15 * 1000,
pagesBufferLength: 2,
},
productionBrowserSourceMaps: false,
typescript: {
ignoreBuildErrors: true,

View File

@@ -43,6 +43,7 @@
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
"@tanstack/react-virtual": "^3.13.18",
"autoprefixer": "^10.4.20",
"axios": "^1.8.1",
"class-variance-authority": "^0.7.1",
@@ -64,6 +65,8 @@
"react-hook-form": "^7.54.1",
"react-markdown": "^10.0.0",
"react-resizable-panels": "^2.1.7",
"react-window": "^2.2.4",
"react-window-infinite-loader": "^2.0.0",
"recharts": "^2.15.0",
"sonner": "^1.7.4",
"tailwind-merge": "^2.5.5",
@@ -72,7 +75,6 @@
"zod": "^3.25.0"
},
"devDependencies": {
"@distube/ytdl-core": "^4.16.12",
"@next/bundle-analyzer": "^16.1.1",
"@tailwindcss/typography": "^0.5.16",
"@types/lodash": "^4.17.16",

149
pnpm-lock.yaml generated
View File

@@ -86,6 +86,9 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.1.6
version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@tanstack/react-virtual':
specifier: ^3.13.18
version: 3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
autoprefixer:
specifier: ^10.4.20
version: 10.4.23(postcss@8.5.6)
@@ -149,6 +152,12 @@ importers:
react-resizable-panels:
specifier: ^2.1.7
version: 2.1.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react-window:
specifier: ^2.2.4
version: 2.2.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react-window-infinite-loader:
specifier: ^2.0.0
version: 2.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
recharts:
specifier: ^2.15.0
version: 2.15.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -168,9 +177,6 @@ importers:
specifier: ^3.25.0
version: 3.25.76
devDependencies:
'@distube/ytdl-core':
specifier: ^4.16.12
version: 4.16.12
'@next/bundle-analyzer':
specifier: ^16.1.1
version: 16.1.1
@@ -289,10 +295,6 @@ packages:
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
'@distube/ytdl-core@4.16.12':
resolution: {integrity: sha512-/NR8Jur1Q4E2oD+DJta7uwWu7SkqdEkhwERt7f4iune70zg7ZlLLTOHs1+jgg3uD2jQjpdk7RGC16FqstG4RsA==}
engines: {node: '>=20.18.1'}
'@emnapi/core@1.8.1':
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
@@ -1258,6 +1260,15 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tanstack/react-virtual@3.13.18':
resolution: {integrity: sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/virtual-core@3.13.18':
resolution: {integrity: sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -1506,10 +1517,6 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -2252,20 +2259,6 @@ packages:
html-url-attributes@3.0.1:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
http-cookie-agent@7.0.3:
resolution: {integrity: sha512-EeZo7CGhfqPW6R006rJa4QtZZUpBygDa2HZH3DJqsTzTjyRE6foDBVQIv/pjVsxHC8z2GIdbB1Hvn9SRorP3WQ==}
engines: {node: '>=20.0.0'}
peerDependencies:
tough-cookie: ^4.0.0 || ^5.0.0 || ^6.0.0
undici: ^7.0.0
peerDependenciesMeta:
undici:
optional: true
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -2524,10 +2517,6 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
m3u8stream@0.8.6:
resolution: {integrity: sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==}
engines: {node: '>=12'}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -2635,10 +2624,6 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
miniget@4.2.3:
resolution: {integrity: sha512-SjbDPDICJ1zT+ZvQwK0hUcRY4wxlhhNpHL9nJOB2MEAXRGagTljsO8MEDzQMTFf0Q8g4QNi8P9lEm/g7e+qgzA==}
engines: {node: '>=12'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -2973,6 +2958,18 @@ packages:
react: '>=16.6.0'
react-dom: '>=16.6.0'
react-window-infinite-loader@2.0.0:
resolution: {integrity: sha512-dioOyvShGheEqqFHcPNKCixCOc2evwb2VEt9sitfJfTZ1hir8m6b8W0CNBvcUj+8Y8IeWu4yb88DI7k88aYTQQ==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
react-window@2.2.4:
resolution: {integrity: sha512-FiZsQHvt2qbnTz6cN+/FXvX62v2xukQ+AajUivkm/Ivdp9rnU3bp0B1eDcCNpQXNaDBdqkEVGNYHlvIUGU9yBw==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
react@19.2.3:
resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
engines: {node: '>=0.10.0'}
@@ -3046,10 +3043,6 @@ packages:
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
engines: {node: '>= 0.4'}
sax@1.4.4:
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
engines: {node: '>=11.0.0'}
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
@@ -3219,13 +3212,6 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tldts-core@6.1.86:
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
tldts@6.1.86:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
hasBin: true
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -3234,10 +3220,6 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tough-cookie@5.1.2:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
engines: {node: '>=16'}
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@@ -3298,10 +3280,6 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici@7.18.2:
resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==}
engines: {node: '>=20.18.1'}
unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@@ -3544,18 +3522,6 @@ snapshots:
'@discoveryjs/json-ext@0.5.7': {}
'@distube/ytdl-core@4.16.12':
dependencies:
http-cookie-agent: 7.0.3(tough-cookie@5.1.2)(undici@7.18.2)
https-proxy-agent: 7.0.6
m3u8stream: 0.8.6
miniget: 4.2.3
sax: 1.4.4
tough-cookie: 5.1.2
undici: 7.18.2
transitivePeerDependencies:
- supports-color
'@emnapi/core@1.8.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
@@ -4512,6 +4478,14 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.19
'@tanstack/react-virtual@3.13.18(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@tanstack/virtual-core': 3.13.18
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
'@tanstack/virtual-core@3.13.18': {}
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@@ -4748,8 +4722,6 @@ snapshots:
acorn@8.15.0: {}
agent-base@7.1.4: {}
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -5670,20 +5642,6 @@ snapshots:
html-url-attributes@3.0.1: {}
http-cookie-agent@7.0.3(tough-cookie@5.1.2)(undici@7.18.2):
dependencies:
agent-base: 7.1.4
tough-cookie: 5.1.2
optionalDependencies:
undici: 7.18.2
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -5926,11 +5884,6 @@ snapshots:
dependencies:
react: 19.2.3
m3u8stream@0.8.6:
dependencies:
miniget: 4.2.3
sax: 1.4.4
math-intrinsics@1.1.0: {}
mdast-util-from-markdown@2.0.2:
@@ -6168,8 +6121,6 @@ snapshots:
dependencies:
mime-db: 1.52.0
miniget@4.2.3: {}
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -6495,6 +6446,16 @@ snapshots:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
react-window-infinite-loader@2.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
react-window@2.2.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
dependencies:
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
react@19.2.3: {}
read-cache@1.0.0:
@@ -6604,8 +6565,6 @@ snapshots:
es-errors: 1.3.0
is-regex: 1.2.1
sax@1.4.4: {}
scheduler@0.27.0: {}
semver@6.3.1: {}
@@ -6861,22 +6820,12 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tldts-core@6.1.86: {}
tldts@6.1.86:
dependencies:
tldts-core: 6.1.86
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
totalist@3.0.1: {}
tough-cookie@5.1.2:
dependencies:
tldts: 6.1.86
trim-lines@3.0.1: {}
trough@2.2.0: {}
@@ -6955,8 +6904,6 @@ snapshots:
undici-types@6.21.0: {}
undici@7.18.2: {}
unified@11.0.5:
dependencies:
'@types/unist': 3.0.3

View File

@@ -1,114 +1,122 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: [
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./src/**/*.{ts,tsx}",
"./lib/**/*.{js,ts,jsx,tsx,mdx}",
"./hooks/**/*.{js,ts,jsx,tsx,mdx}",
"./utils/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
shimmer: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(100%)' }
},
twinkle: {
'0%, 100%': { opacity: '1', transform: 'scale(1)' },
'50%': { opacity: '0.3', transform: 'scale(0.8)' }
},
snowflake: {
'0%': { transform: 'translateY(-100vh) rotate(0deg)', opacity: '1' },
'100%': { transform: 'translateY(100vh) rotate(360deg)', opacity: '0' }
},
sparkle: {
'0%, 100%': { opacity: '0', transform: 'scale(0) rotate(0deg)' },
'50%': { opacity: '1', transform: 'scale(1) rotate(180deg)' }
},
glow: {
'0%, 100%': {
boxShadow: '0 0 5px hsl(0 84% 50%), 0 0 10px hsl(0 84% 50%), 0 0 15px hsl(0 84% 50%)'
},
'50%': {
boxShadow: '0 0 10px hsl(142 76% 36%), 0 0 20px hsl(142 76% 36%), 0 0 30px hsl(142 76% 36%)'
}
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"shimmer": "shimmer 2s infinite",
"twinkle": "twinkle 2s ease-in-out infinite",
"snowflake": "snowflake 10s linear infinite",
"sparkle": "sparkle 1.5s ease-in-out infinite",
"glow": "glow 2s ease-in-out infinite"
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
shimmer: {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(100%)' }
},
twinkle: {
'0%, 100%': { opacity: '1', transform: 'scale(1)' },
'50%': { opacity: '0.3', transform: 'scale(0.8)' }
},
snowflake: {
'0%': { transform: 'translateY(-100vh) rotate(0deg)', opacity: '1' },
'100%': { transform: 'translateY(100vh) rotate(360deg)', opacity: '0' }
},
sparkle: {
'0%, 100%': { opacity: '0', transform: 'scale(0) rotate(0deg)' },
'50%': { opacity: '1', transform: 'scale(1) rotate(180deg)' }
},
glow: {
'0%, 100%': {
boxShadow: '0 0 5px hsl(0 84% 50%), 0 0 10px hsl(0 84% 50%), 0 0 15px hsl(0 84% 50%)'
},
'50%': {
boxShadow: '0 0 10px hsl(142 76% 36%), 0 0 20px hsl(142 76% 36%), 0 0 30px hsl(142 76% 36%)'
}
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"shimmer": "shimmer 2s infinite",
"twinkle": "twinkle 2s ease-in-out infinite",
"snowflake": "snowflake 10s linear infinite",
"sparkle": "sparkle 1.5s ease-in-out infinite",
"glow": "glow 2s ease-in-out infinite"
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
// Optimize for production
future: {
hoverOnlyWhenSupported: true,
},
};
export default config;