feat: upgrade deps; fix lint for test pages

This commit is contained in:
Simon
2025-10-12 00:12:57 +08:00
parent 9f88439ee4
commit f923e8a6db
4 changed files with 1014 additions and 722 deletions

View File

@@ -3,18 +3,19 @@ import reactDom from 'eslint-plugin-react-dom'
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh' import reactRefresh from 'eslint-plugin-react-refresh'
import reactX from 'eslint-plugin-react-x' import reactX from 'eslint-plugin-react-x'
import { globalIgnores } from 'eslint/config' import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals' import globals from 'globals'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
export default tseslint.config([ export default defineConfig([
globalIgnores(['dist', 'test-pages']), globalIgnores(['dist', 'test-pages']),
reactHooks.configs.flat.recommended,
{ {
files: ['**/*.{ts,tsx}'], files: ['**/*.{ts,tsx}'],
extends: [ extends: [
js.configs.recommended, js.configs.recommended,
tseslint.configs.recommended, tseslint.configs.recommended,
reactHooks.configs['recommended-latest'], // reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite, reactRefresh.configs.vite,
// Remove tseslint.configs.recommended and replace with this // Remove tseslint.configs.recommended and replace with this

1259
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,46 +50,43 @@
"prepare": "husky" "prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/openai": "^2.0.22", "@ai-sdk/openai": "^2.0.49",
"ai": "^5.0.26", "ai": "^5.0.68",
"ai-motion": "^0.4.7", "ai-motion": "^0.4.7",
"chalk": "^5.6.0", "chalk": "^5.6.2",
"zod": "^4.1.3" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^20.1.0", "@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0", "@commitlint/config-conventional": "^20.0.0",
"@eslint/js": "^9.30.1", "@eslint/js": "^9.37.0",
"@microsoft/api-extractor": "^7.52.13", "@microsoft/api-extractor": "^7.53.1",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.14",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/react": "^19.1.8", "@types/react": "^19.2.2",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.2.1",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"dotenv": "^17.2.1", "dotenv": "^17.2.3",
"eslint": "^9.31.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-dom": "^2.0.2", "eslint-plugin-react-dom": "^2.0.6",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.23",
"eslint-plugin-react-x": "^2.0.2", "eslint-plugin-react-x": "^2.0.6",
"globals": "^16.3.0", "globals": "^16.4.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.1.2", "lint-staged": "^16.2.4",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"react": "^19.1.1", "react": "^19.2.0",
"react-dom": "^19.1.1", "react-dom": "^19.2.0",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.14",
"typescript": "^5.9.2", "typescript": "^5.9.3",
"typescript-eslint": "^8.39.0", "typescript-eslint": "^8.46.0",
"unplugin-dts": "^1.0.0-beta.6", "unplugin-dts": "^1.0.0-beta.6",
"vite": "^7.0.4", "vite": "^7.1.9",
"vite-plugin-css-injected-by-js": "^3.5.2", "vite-plugin-css-injected-by-js": "^3.5.2",
"wouter": "^3.7.1" "wouter": "^3.7.1"
}, },
"overrides": {
"zod": "^4.1.3"
},
"lint-staged": { "lint-staged": {
"*.{js,ts,cjs,cts,mjs,mts}": [ "*.{js,ts,cjs,cts,mjs,mts}": [
"npx prettier --write --ignore-unknown", "npx prettier --write --ignore-unknown",

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useEffect, useMemo, useState } from 'react'
import { Link } from 'wouter' import { Link } from 'wouter'
interface Product { interface Product {
@@ -27,13 +27,177 @@ const generateProducts = (count: number): Product[] => {
rating: Math.round((Math.random() * 2 + 3) * 10) / 10, rating: Math.round((Math.random() * 2 + 3) * 10) / 10,
image: `https://picsum.photos/200/200?random=${i}`, image: `https://picsum.photos/200/200?random=${i}`,
description: `这是一款优秀的${categories[i % categories.length]}产品,具有出色的性能和设计。`, description: `这是一款优秀的${categories[i % categories.length]}产品,具有出色的性能和设计。`,
tags: ['热销', '新品', '推荐'].slice(0, Math.floor(Math.random() * 3) + 1) tags: ['热销', '新品', '推荐'].slice(0, Math.floor(Math.random() * 3) + 1),
})) }))
} }
// Loading skeleton component
const LoadingSkeleton = () => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: 12 }, (_, i) => `skeleton-item-${i}`).map((id) => (
<div key={id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 animate-pulse">
<div className="bg-gray-300 dark:bg-gray-600 h-48 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-3/4"></div>
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/2"></div>
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/4"></div>
</div>
</div>
))}
</div>
)
// Product card component
const ProductCard = ({ product }: { product: Product }) => (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-4">
<div className="relative mb-4">
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover rounded-lg"
loading="lazy"
/>
<div className="absolute top-2 right-2 flex flex-wrap gap-1">
{product.tags.map((tag) => (
<span key={tag} className="bg-red-500 text-white text-xs px-2 py-1 rounded-full">
{tag}
</span>
))}
</div>
</div>
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 line-clamp-2">
{product.name}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-3 line-clamp-2">
{product.description}
</p>
<div className="flex items-center justify-between mb-3">
<span className="text-2xl font-bold text-blue-600 dark:text-blue-400">
¥{product.price.toLocaleString()}
</span>
<div className="flex items-center">
<span className="text-yellow-400"></span>
<span className="text-sm text-gray-600 dark:text-gray-300 ml-1">{product.rating}</span>
</div>
</div>
<div className="flex items-center justify-between mb-4">
<span className="text-sm text-gray-500 dark:text-gray-400">: {product.stock}</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{product.category}</span>
</div>
<button className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
</button>
</div>
)
// Product list item component
const ProductListItem = ({ product }: { product: Product }) => (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex items-center space-x-4">
<img
src={product.image}
alt={product.name}
className="w-20 h-20 object-cover rounded-lg flex-shrink-0"
loading="lazy"
/>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">{product.name}</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-2 line-clamp-1">
{product.description}
</p>
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
<span>{product.category}</span>
<span>: {product.stock}</span>
<div className="flex items-center">
<span className="text-yellow-400"></span>
<span className="ml-1">{product.rating}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<span className="text-xl font-bold text-blue-600 dark:text-blue-400">
¥{product.price.toLocaleString()}
</span>
<button className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
</button>
</div>
</div>
)
// Pagination component
interface PaginationProps {
currentPage: number
totalPages: number
startIndex: number
endIndex: number
totalItems: number
onPageChange: (page: number) => void
}
const Pagination = ({
currentPage,
totalPages,
startIndex,
endIndex,
totalItems,
onPageChange,
}: PaginationProps) => {
const getPageNumbers = () => {
const pages = []
const maxVisible = 5
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2))
const end = Math.min(totalPages, start + maxVisible - 1)
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1)
}
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
}
return (
<div className="flex items-center justify-between mt-8">
<div className="text-sm text-gray-700 dark:text-gray-300">
{startIndex + 1}-{Math.min(endIndex, totalItems)} {totalItems}
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>
</button>
{getPageNumbers().map((page) => (
<button
key={page}
onClick={() => onPageChange(page)}
className={`px-3 py-2 text-sm font-medium rounded-md ${
page === currentPage
? 'bg-blue-600 text-white'
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
}`}
>
{page}
</button>
))}
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>
</button>
</div>
</div>
)
}
export default function ListTestPage() { export default function ListTestPage() {
const [products, setProducts] = useState<Product[]>([]) const [products, setProducts] = useState<Product[]>([])
const [filteredProducts, setFilteredProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
const [selectedCategory, setSelectedCategory] = useState('全部') const [selectedCategory, setSelectedCategory] = useState('全部')
@@ -45,32 +209,53 @@ export default function ListTestPage() {
const categories = ['全部', '手机', '电脑', '平板', '耳机', '手表', '相机'] const categories = ['全部', '手机', '电脑', '平板', '耳机', '手表', '相机']
// Helper to set filters and reset page
const handleSearchChange = (term: string) => {
setSearchTerm(term)
setCurrentPage(1)
}
const handleCategoryChange = (category: string) => {
setSelectedCategory(category)
setCurrentPage(1)
}
const handleSortChange = (sort: string) => {
setSortBy(sort)
setCurrentPage(1)
}
const handleSortOrderChange = (order: 'asc' | 'desc') => {
setSortOrder(order)
setCurrentPage(1)
}
// 模拟数据加载 // 模拟数据加载
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
setLoading(true) setLoading(true)
// 模拟网络延迟 // 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1500)) await new Promise((resolve) => setTimeout(resolve, 1500))
const data = generateProducts(150) const data = generateProducts(150)
setProducts(data) setProducts(data)
setFilteredProducts(data)
setLoading(false) setLoading(false)
} }
loadData() loadData()
}, []) }, [])
// 搜索和过滤 // 搜索和过滤 - Use useMemo to compute filtered products
useEffect(() => { const filteredProducts = useMemo(() => {
let filtered = products let filtered = [...products]
// 按类别过滤 // 按类别过滤
if (selectedCategory !== '全部') { if (selectedCategory !== '全部') {
filtered = filtered.filter(product => product.category === selectedCategory) filtered = filtered.filter((product) => product.category === selectedCategory)
} }
// 按搜索词过滤 // 按搜索词过滤
if (searchTerm) { if (searchTerm) {
filtered = filtered.filter(product => filtered = filtered.filter(
(product) =>
product.name.toLowerCase().includes(searchTerm.toLowerCase()) || product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description.toLowerCase().includes(searchTerm.toLowerCase()) product.description.toLowerCase().includes(searchTerm.toLowerCase())
) )
@@ -93,8 +278,7 @@ export default function ListTestPage() {
} }
}) })
setFilteredProducts(filtered) return filtered
setCurrentPage(1) // 重置到第一页
}, [products, searchTerm, selectedCategory, sortBy, sortOrder]) }, [products, searchTerm, selectedCategory, sortBy, sortOrder])
// 分页计算 // 分页计算
@@ -109,173 +293,12 @@ export default function ListTestPage() {
window.scrollTo({ top: 0, behavior: 'smooth' }) window.scrollTo({ top: 0, behavior: 'smooth' })
} }
const LoadingSkeleton = () => (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 animate-pulse">
<div className="bg-gray-300 dark:bg-gray-600 h-48 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-3/4"></div>
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/2"></div>
<div className="bg-gray-300 dark:bg-gray-600 h-4 rounded w-1/4"></div>
</div>
</div>
))}
</div>
)
const ProductCard = ({ product }: { product: Product }) => (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow p-4">
<div className="relative mb-4">
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover rounded-lg"
loading="lazy"
/>
<div className="absolute top-2 right-2 flex flex-wrap gap-1">
{product.tags.map((tag, index) => (
<span
key={index}
className="bg-red-500 text-white text-xs px-2 py-1 rounded-full"
>
{tag}
</span>
))}
</div>
</div>
<h3 className="font-semibold text-gray-900 dark:text-white mb-2 line-clamp-2">
{product.name}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-3 line-clamp-2">
{product.description}
</p>
<div className="flex items-center justify-between mb-3">
<span className="text-2xl font-bold text-blue-600 dark:text-blue-400">
¥{product.price.toLocaleString()}
</span>
<div className="flex items-center">
<span className="text-yellow-400"></span>
<span className="text-sm text-gray-600 dark:text-gray-300 ml-1">
{product.rating}
</span>
</div>
</div>
<div className="flex items-center justify-between mb-4">
<span className="text-sm text-gray-500 dark:text-gray-400">
: {product.stock}
</span>
<span className="text-sm text-gray-500 dark:text-gray-400">
{product.category}
</span>
</div>
<button className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
</button>
</div>
)
const ProductListItem = ({ product }: { product: Product }) => (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4 flex items-center space-x-4">
<img
src={product.image}
alt={product.name}
className="w-20 h-20 object-cover rounded-lg flex-shrink-0"
loading="lazy"
/>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 dark:text-white mb-1">
{product.name}
</h3>
<p className="text-gray-600 dark:text-gray-300 text-sm mb-2 line-clamp-1">
{product.description}
</p>
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
<span>{product.category}</span>
<span>: {product.stock}</span>
<div className="flex items-center">
<span className="text-yellow-400"></span>
<span className="ml-1">{product.rating}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-4">
<span className="text-xl font-bold text-blue-600 dark:text-blue-400">
¥{product.price.toLocaleString()}
</span>
<button className="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition-colors">
</button>
</div>
</div>
)
const Pagination = () => {
const getPageNumbers = () => {
const pages = []
const maxVisible = 5
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2))
const end = Math.min(totalPages, start + maxVisible - 1)
if (end - start + 1 < maxVisible) {
start = Math.max(1, end - maxVisible + 1)
}
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
}
return (
<div className="flex items-center justify-between mt-8">
<div className="text-sm text-gray-700 dark:text-gray-300">
{startIndex + 1}-{Math.min(endIndex, filteredProducts.length)}
{filteredProducts.length}
</div>
<div className="flex items-center space-x-2">
<button
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>
</button>
{getPageNumbers().map(page => (
<button
key={page}
onClick={() => handlePageChange(page)}
className={`px-3 py-2 text-sm font-medium rounded-md ${
page === currentPage
? 'bg-blue-600 text-white'
: 'text-gray-500 bg-white border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700'
}`}
>
{page}
</button>
))}
<button
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-2 text-sm font-medium text-gray-500 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
>
</button>
</div>
</div>
)
}
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
<div className="max-w-7xl mx-auto px-4"> <div className="max-w-7xl mx-auto px-4">
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"> <h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2"></h1>
<p className="text-gray-600 dark:text-gray-300"></p>
</h1>
<p className="text-gray-600 dark:text-gray-300">
</p>
</div> </div>
{/* 搜索和过滤栏 */} {/* 搜索和过滤栏 */}
@@ -289,7 +312,7 @@ export default function ListTestPage() {
<input <input
type="text" type="text"
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => handleSearchChange(e.target.value)}
placeholder="输入产品名称或描述..." placeholder="输入产品名称或描述..."
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
/> />
@@ -302,11 +325,13 @@ export default function ListTestPage() {
</label> </label>
<select <select
value={selectedCategory} value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)} onChange={(e) => handleCategoryChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
> >
{categories.map(category => ( {categories.map((category) => (
<option key={category} value={category}>{category}</option> <option key={category} value={category}>
{category}
</option>
))} ))}
</select> </select>
</div> </div>
@@ -318,7 +343,7 @@ export default function ListTestPage() {
</label> </label>
<select <select
value={sortBy} value={sortBy}
onChange={(e) => setSortBy(e.target.value)} onChange={(e) => handleSortChange(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
> >
<option value="name"></option> <option value="name"></option>
@@ -335,7 +360,7 @@ export default function ListTestPage() {
</label> </label>
<select <select
value={sortOrder} value={sortOrder}
onChange={(e) => setSortOrder(e.target.value as 'asc' | 'desc')} onChange={(e) => handleSortOrderChange(e.target.value as 'asc' | 'desc')}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white" className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
> >
<option value="asc"></option> <option value="asc"></option>
@@ -362,9 +387,7 @@ export default function ListTestPage() {
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300"> <span className="text-sm font-medium text-gray-700 dark:text-gray-300">:</span>
:
</span>
<button <button
onClick={() => setViewMode('grid')} onClick={() => setViewMode('grid')}
className={`p-2 rounded-md ${ className={`p-2 rounded-md ${
@@ -402,27 +425,32 @@ export default function ListTestPage() {
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2"> <h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
</h3> </h3>
<p className="text-gray-600 dark:text-gray-300"> <p className="text-gray-600 dark:text-gray-300"></p>
</p>
</div> </div>
) : ( ) : (
<> <>
{viewMode === 'grid' ? ( {viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{currentProducts.map(product => ( {currentProducts.map((product: Product) => (
<ProductCard key={product.id} product={product} /> <ProductCard key={product.id} product={product} />
))} ))}
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{currentProducts.map(product => ( {currentProducts.map((product: Product) => (
<ProductListItem key={product.id} product={product} /> <ProductListItem key={product.id} product={product} />
))} ))}
</div> </div>
)} )}
<Pagination /> <Pagination
currentPage={currentPage}
totalPages={totalPages}
startIndex={startIndex}
endIndex={endIndex}
totalItems={filteredProducts.length}
onPageChange={handlePageChange}
/>
</> </>
)} )}
@@ -432,7 +460,12 @@ export default function ListTestPage() {
className="fixed bottom-8 right-8 bg-blue-600 hover:bg-blue-700 text-white p-3 rounded-full shadow-lg transition-colors" className="fixed bottom-8 right-8 bg-blue-600 hover:bg-blue-700 text-white p-3 rounded-full shadow-lg transition-colors"
> >
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" /> <path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 10l7-7m0 0l7 7m-7-7v18"
/>
</svg> </svg>
</button> </button>