Try fix brand-master
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
ad96ec33e6
commit
f02f8106f9
@ -43,3 +43,8 @@ class Product(Base):
|
|||||||
return float(self.model.base_price)
|
return float(self.model.base_price)
|
||||||
else:
|
else:
|
||||||
return 0.0 # Default fallback
|
return 0.0 # Default fallback
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category_name(self):
|
||||||
|
"""Return the name of the associated category, or None if not set."""
|
||||||
|
return self.category.name if self.category else None
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
from sqlalchemy import func
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -40,7 +41,7 @@ def list_products(
|
|||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
query = db.query(Product).options(joinedload(Product.model))
|
query = db.query(Product).options(joinedload(Product.model), joinedload(Product.category))
|
||||||
|
|
||||||
if category_id:
|
if category_id:
|
||||||
query = query.filter(Product.category_id == category_id)
|
query = query.filter(Product.category_id == category_id)
|
||||||
@ -101,9 +102,42 @@ def search(q: str, skip: int = 0, limit: int = 20, db: Session = Depends(get_db)
|
|||||||
return products
|
return products
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/count")
|
||||||
|
def get_products_count(
|
||||||
|
category_id: Optional[int] = None,
|
||||||
|
model_id: Optional[int] = None,
|
||||||
|
brand: Optional[str] = None,
|
||||||
|
gender: Optional[str] = None,
|
||||||
|
min_price: Optional[float] = None,
|
||||||
|
max_price: Optional[float] = None,
|
||||||
|
on_sale: Optional[bool] = None,
|
||||||
|
featured: Optional[bool] = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Return the total number of products matching the given filters."""
|
||||||
|
query = db.query(func.count(Product.id))
|
||||||
|
if category_id:
|
||||||
|
query = query.filter(Product.category_id == category_id)
|
||||||
|
if model_id:
|
||||||
|
query = query.filter(Product.model_id == model_id)
|
||||||
|
if brand:
|
||||||
|
query = query.filter(Product.brand == brand)
|
||||||
|
if gender:
|
||||||
|
query = query.filter(Product.gender == gender)
|
||||||
|
if min_price is not None:
|
||||||
|
query = query.filter(Product.price >= min_price)
|
||||||
|
if max_price is not None:
|
||||||
|
query = query.filter(Product.price <= max_price)
|
||||||
|
if on_sale is not None:
|
||||||
|
query = query.filter(Product.is_on_sale == on_sale)
|
||||||
|
if featured is not None:
|
||||||
|
query = query.filter(Product.is_featured == featured)
|
||||||
|
return {"count": query.scalar()}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{product_id}", response_model=ProductResponse)
|
@router.get("/{product_id}", response_model=ProductResponse)
|
||||||
def get_product(product_id: int, db: Session = Depends(get_db)):
|
def get_product(product_id: int, db: Session = Depends(get_db)):
|
||||||
product = get_product_by_id(db, product_id)
|
product = db.query(Product).options(joinedload(Product.model), joinedload(Product.category)).filter(Product.id == product_id).first()
|
||||||
if not product:
|
if not product:
|
||||||
raise HTTPException(status_code=404, detail="Product not found")
|
raise HTTPException(status_code=404, detail="Product not found")
|
||||||
|
|
||||||
|
|||||||
@ -72,6 +72,7 @@ class ProductResponse(BaseModel):
|
|||||||
is_on_sale: bool
|
is_on_sale: bool
|
||||||
override_price: Optional[Decimal]
|
override_price: Optional[Decimal]
|
||||||
override_sizes: Optional[List[str]]
|
override_sizes: Optional[List[str]]
|
||||||
|
category_name: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
|
|||||||
@ -13,7 +13,7 @@ def get_products(
|
|||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> List[Product]:
|
) -> List[Product]:
|
||||||
query = db.query(Product).options(joinedload(Product.model))
|
query = db.query(Product).options(joinedload(Product.model), joinedload(Product.category))
|
||||||
|
|
||||||
if category_id:
|
if category_id:
|
||||||
query = query.filter(Product.category_id == category_id)
|
query = query.filter(Product.category_id == category_id)
|
||||||
@ -67,7 +67,7 @@ def delete_product(db: Session, product_id: int) -> bool:
|
|||||||
def search_products(db: Session, query: str, skip: int = 0, limit: int = 10) -> List[Product]:
|
def search_products(db: Session, query: str, skip: int = 0, limit: int = 10) -> List[Product]:
|
||||||
return (
|
return (
|
||||||
db.query(Product)
|
db.query(Product)
|
||||||
.options(joinedload(Product.model))
|
.options(joinedload(Product.model), joinedload(Product.category))
|
||||||
.filter(
|
.filter(
|
||||||
Product.name.ilike(f"%{query}%") | Product.brand.ilike(f"%{query}%")
|
Product.name.ilike(f"%{query}%") | Product.brand.ilike(f"%{query}%")
|
||||||
)
|
)
|
||||||
|
|||||||
@ -53,7 +53,8 @@ export default function Admin() {
|
|||||||
const [filterModel, setFilterModel] = useState('')
|
const [filterModel, setFilterModel] = useState('')
|
||||||
const [brands, setBrands] = useState([])
|
const [brands, setBrands] = useState([])
|
||||||
const [allProducts, setAllProducts] = useState([]) // Store all products for filtering
|
const [allProducts, setAllProducts] = useState([]) // Store all products for filtering
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(25) // Products per page in admin list
|
const [itemsPerPage, setItemsPerPage] = useState(25) // 25, 50, 100, or 'all'
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
const [showBrandForm, setShowBrandForm] = useState(false)
|
const [showBrandForm, setShowBrandForm] = useState(false)
|
||||||
const [editingBrand, setEditingBrand] = useState(null)
|
const [editingBrand, setEditingBrand] = useState(null)
|
||||||
const [brandFormData, setBrandFormData] = useState({ name: '' })
|
const [brandFormData, setBrandFormData] = useState({ name: '' })
|
||||||
@ -97,7 +98,8 @@ export default function Admin() {
|
|||||||
|
|
||||||
const fetchProducts = async () => {
|
const fetchProducts = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/products')
|
// Fetch all products so admin can see and filter the full catalogue
|
||||||
|
const response = await api.get('/products', { params: { limit: 10000, skip: 0 } })
|
||||||
setAllProducts(response.data)
|
setAllProducts(response.data)
|
||||||
setProducts(response.data)
|
setProducts(response.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -192,6 +194,7 @@ export default function Admin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProducts(filtered)
|
setProducts(filtered)
|
||||||
|
setCurrentPage(1) // Reset to first page whenever filters change
|
||||||
}, [searchQuery, filterBrand, filterCategory, filterModel, allProducts])
|
}, [searchQuery, filterBrand, filterCategory, filterModel, allProducts])
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
@ -1185,18 +1188,35 @@ export default function Admin() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
{(() => {
|
||||||
<h2>Products ({products.length})</h2>
|
const pageSize = itemsPerPage === 'all' ? products.length : itemsPerPage
|
||||||
|
const totalPages = pageSize === 0 ? 1 : Math.ceil(products.length / (pageSize || 1))
|
||||||
|
const safePage = Math.min(currentPage, totalPages || 1)
|
||||||
|
const startIdx = (safePage - 1) * (pageSize || products.length)
|
||||||
|
const endIdx = itemsPerPage === 'all' ? products.length : startIdx + pageSize
|
||||||
|
const pagedProducts = products.slice(startIdx, endIdx)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||||
|
<h2>
|
||||||
|
Products — {allProducts.length} total
|
||||||
|
{products.length !== allProducts.length && ` (${products.length} matching filters)`}
|
||||||
|
</h2>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
<label style={{ fontSize: '0.9rem', color: '#666' }}>Items per page:</label>
|
<label style={{ fontSize: '0.9rem', color: '#666' }}>Items per page:</label>
|
||||||
<select
|
<select
|
||||||
value={itemsPerPage}
|
value={itemsPerPage}
|
||||||
onChange={(e) => setItemsPerPage(e.target.value === 'all' ? products.length : parseInt(e.target.value))}
|
onChange={(e) => {
|
||||||
|
setItemsPerPage(e.target.value === 'all' ? 'all' : parseInt(e.target.value))
|
||||||
|
setCurrentPage(1)
|
||||||
|
}}
|
||||||
style={{ padding: '0.5rem', borderRadius: '4px', border: '1px solid #ddd' }}
|
style={{ padding: '0.5rem', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||||
>
|
>
|
||||||
<option value="25">25</option>
|
<option value={10}>10</option>
|
||||||
<option value="50">50</option>
|
<option value={25}>25</option>
|
||||||
<option value="100">100</option>
|
<option value={50}>50</option>
|
||||||
|
<option value={100}>100</option>
|
||||||
<option value="all">All</option>
|
<option value="all">All</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -1204,6 +1224,7 @@ export default function Admin() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
<div style={{ overflowX: 'auto' }}>
|
<div style={{ overflowX: 'auto' }}>
|
||||||
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '1rem' }}>
|
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '1rem' }}>
|
||||||
<thead>
|
<thead>
|
||||||
@ -1211,6 +1232,7 @@ export default function Admin() {
|
|||||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>ID</th>
|
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>ID</th>
|
||||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Name</th>
|
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Name</th>
|
||||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Brand</th>
|
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Brand</th>
|
||||||
|
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Category</th>
|
||||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Slug</th>
|
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Slug</th>
|
||||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Price</th>
|
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Price</th>
|
||||||
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Stock</th>
|
<th style={{ padding: '0.75rem', border: '1px solid #ddd' }}>Stock</th>
|
||||||
@ -1220,11 +1242,14 @@ export default function Admin() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{products.slice(0, itemsPerPage).map(product => (
|
{pagedProducts.map(product => (
|
||||||
<tr key={product.id}>
|
<tr key={product.id}>
|
||||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.id}</td>
|
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.id}</td>
|
||||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.name}</td>
|
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.name}</td>
|
||||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.brand}</td>
|
<td style={{ padding: '0.75rem', border: '1px solid #ddd' }}>{product.brand}</td>
|
||||||
|
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em' }}>
|
||||||
|
{product.category_name || categories.find(c => c.id === product.category_id)?.name || product.category_id}
|
||||||
|
</td>
|
||||||
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em', color: '#666' }}>
|
<td style={{ padding: '0.75rem', border: '1px solid #ddd', fontSize: '0.85em', color: '#666' }}>
|
||||||
{product.slug || '-'}
|
{product.slug || '-'}
|
||||||
</td>
|
</td>
|
||||||
@ -1251,7 +1276,47 @@ export default function Admin() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination controls */}
|
||||||
|
{itemsPerPage !== 'all' && totalPages > 1 && (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem', marginTop: '1rem', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
|
disabled={safePage <= 1}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', borderRadius: '4px', border: '1px solid #ddd', cursor: safePage <= 1 ? 'not-allowed' : 'pointer', opacity: safePage <= 1 ? 0.5 : 1 }}
|
||||||
|
>
|
||||||
|
← Prev
|
||||||
|
</button>
|
||||||
|
{Array.from({ length: Math.min(totalPages, 10) }, (_, i) => {
|
||||||
|
const page = totalPages <= 10 ? i + 1 : safePage <= 5 ? i + 1 : safePage + i - 4
|
||||||
|
if (page < 1 || page > totalPages) return null
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', borderRadius: '4px', border: '1px solid #ddd', cursor: 'pointer', backgroundColor: page === safePage ? '#333' : 'white', color: page === safePage ? 'white' : 'inherit' }}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
|
disabled={safePage >= totalPages}
|
||||||
|
style={{ padding: '0.4rem 0.8rem', borderRadius: '4px', border: '1px solid #ddd', cursor: safePage >= totalPages ? 'not-allowed' : 'pointer', opacity: safePage >= totalPages ? 0.5 : 1 }}
|
||||||
|
>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
<span style={{ fontSize: '0.85rem', color: '#666', marginLeft: '0.5rem' }}>
|
||||||
|
Page {safePage} of {totalPages} — showing {startIdx + 1}–{Math.min(endIdx, products.length)} of {products.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -208,7 +208,7 @@ export default function ProductDetail() {
|
|||||||
<div className="product-details">
|
<div className="product-details">
|
||||||
<h3>Product Details</h3>
|
<h3>Product Details</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Category:</strong> Shoes</li>
|
<li><strong>Category:</strong> {product.category_name || 'N/A'}</li>
|
||||||
<li><strong>Gender:</strong> {product.gender}</li>
|
<li><strong>Gender:</strong> {product.gender}</li>
|
||||||
<li><strong>Brand:</strong> {product.brand}</li>
|
<li><strong>Brand:</strong> {product.brand}</li>
|
||||||
<li><strong>SKU:</strong> {product.id}</li>
|
<li><strong>SKU:</strong> {product.id}</li>
|
||||||
|
|||||||
@ -5,61 +5,68 @@ import ProductCard from '../components/ProductCard'
|
|||||||
import ProductFilters from '../components/ProductFilters'
|
import ProductFilters from '../components/ProductFilters'
|
||||||
import '../styles/global.css'
|
import '../styles/global.css'
|
||||||
|
|
||||||
|
const PAGE_SIZE = 24
|
||||||
|
|
||||||
export default function Products() {
|
export default function Products() {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const [products, setProducts] = useState([])
|
const [products, setProducts] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [sortBy, setSortBy] = useState('latest')
|
const [sortBy, setSortBy] = useState('latest')
|
||||||
const [activeFilters, setActiveFilters] = useState({})
|
const [activeFilters, setActiveFilters] = useState({})
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [totalCount, setTotalCount] = useState(0)
|
||||||
|
const [showAll, setShowAll] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProducts()
|
fetchProducts()
|
||||||
}, [searchParams, activeFilters, sortBy])
|
}, [searchParams, activeFilters, sortBy, currentPage, showAll])
|
||||||
|
|
||||||
|
const buildFilterParams = () => {
|
||||||
|
const params = {}
|
||||||
|
|
||||||
|
const categorySlug = searchParams.get('category')
|
||||||
|
// category_id resolved later via async call; passed in fetchProducts
|
||||||
|
|
||||||
|
if (activeFilters.gender && activeFilters.gender !== '') params.gender = activeFilters.gender
|
||||||
|
if (activeFilters.brand && activeFilters.brand !== '') params.brand = activeFilters.brand
|
||||||
|
if (activeFilters.model_id && activeFilters.model_id !== '') params.model_id = parseInt(activeFilters.model_id)
|
||||||
|
if (activeFilters.min_price && activeFilters.min_price !== '') params.min_price = parseFloat(activeFilters.min_price)
|
||||||
|
if (activeFilters.max_price && activeFilters.max_price !== '') params.max_price = parseFloat(activeFilters.max_price)
|
||||||
|
if (activeFilters.onSale === true) params.on_sale = true
|
||||||
|
|
||||||
|
if (sortBy === 'price-low') params.sort_by = 'price_asc'
|
||||||
|
else if (sortBy === 'price-high') params.sort_by = 'price_desc'
|
||||||
|
else if (sortBy === 'name') params.sort_by = 'name'
|
||||||
|
else if (sortBy === 'brand') params.sort_by = 'brand'
|
||||||
|
else if (sortBy === 'popular') params.sort_by = 'popular'
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
const fetchProducts = async () => {
|
const fetchProducts = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const categorySlug = searchParams.get('category')
|
const params = buildFilterParams()
|
||||||
const params = { limit: 50 }
|
|
||||||
|
|
||||||
|
const categorySlug = searchParams.get('category')
|
||||||
if (categorySlug) {
|
if (categorySlug) {
|
||||||
// Get category by slug
|
|
||||||
const catRes = await api.get('/categories')
|
const catRes = await api.get('/categories')
|
||||||
const category = catRes.data.find((c) => c.slug === categorySlug)
|
const category = catRes.data.find((c) => c.slug === categorySlug)
|
||||||
if (category) params.category_id = category.id
|
if (category) params.category_id = category.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only add non-empty filter values to params
|
// Fetch total count for pagination
|
||||||
if (activeFilters.gender && activeFilters.gender !== '') {
|
const countRes = await api.get('/products/count', { params })
|
||||||
params.gender = activeFilters.gender
|
const total = countRes.data.count
|
||||||
}
|
setTotalCount(total)
|
||||||
if (activeFilters.brand && activeFilters.brand !== '') {
|
|
||||||
params.brand = activeFilters.brand
|
|
||||||
}
|
|
||||||
if (activeFilters.model_id && activeFilters.model_id !== '') {
|
|
||||||
params.model_id = parseInt(activeFilters.model_id)
|
|
||||||
}
|
|
||||||
if (activeFilters.min_price && activeFilters.min_price !== '') {
|
|
||||||
params.min_price = parseFloat(activeFilters.min_price)
|
|
||||||
}
|
|
||||||
if (activeFilters.max_price && activeFilters.max_price !== '') {
|
|
||||||
params.max_price = parseFloat(activeFilters.max_price)
|
|
||||||
}
|
|
||||||
if (activeFilters.onSale === true) {
|
|
||||||
params.on_sale = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map sortBy to backend parameter
|
// Fetch the page
|
||||||
if (sortBy === 'price-low') {
|
if (showAll) {
|
||||||
params.sort_by = 'price_asc'
|
params.skip = 0
|
||||||
} else if (sortBy === 'price-high') {
|
params.limit = total || 1000
|
||||||
params.sort_by = 'price_desc'
|
} else {
|
||||||
} else if (sortBy === 'name') {
|
params.skip = (currentPage - 1) * PAGE_SIZE
|
||||||
params.sort_by = 'name'
|
params.limit = PAGE_SIZE
|
||||||
} else if (sortBy === 'brand') {
|
|
||||||
params.sort_by = 'brand'
|
|
||||||
} else if (sortBy === 'popular') {
|
|
||||||
params.sort_by = 'popular'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await api.get('/products', { params })
|
const response = await api.get('/products', { params })
|
||||||
@ -73,8 +80,11 @@ export default function Products() {
|
|||||||
|
|
||||||
const handleFilter = (filters) => {
|
const handleFilter = (filters) => {
|
||||||
setActiveFilters(filters)
|
setActiveFilters(filters)
|
||||||
|
setCurrentPage(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalPages = showAll ? 1 : Math.ceil(totalCount / PAGE_SIZE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="products-page">
|
<div className="products-page">
|
||||||
<h1>Our Products</h1>
|
<h1>Our Products</h1>
|
||||||
@ -85,7 +95,7 @@ export default function Products() {
|
|||||||
<div className="products-content">
|
<div className="products-content">
|
||||||
<div className="sort-bar">
|
<div className="sort-bar">
|
||||||
<label>Sort by:</label>
|
<label>Sort by:</label>
|
||||||
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
|
<select value={sortBy} onChange={(e) => { setSortBy(e.target.value); setCurrentPage(1) }}>
|
||||||
<option value="latest">Latest</option>
|
<option value="latest">Latest</option>
|
||||||
<option value="popular">Most Popular</option>
|
<option value="popular">Most Popular</option>
|
||||||
<option value="brand">Brand</option>
|
<option value="brand">Brand</option>
|
||||||
@ -93,17 +103,62 @@ export default function Products() {
|
|||||||
<option value="price-low">Price: Low to High</option>
|
<option value="price-low">Price: Low to High</option>
|
||||||
<option value="price-high">Price: High to Low</option>
|
<option value="price-high">Price: High to Low</option>
|
||||||
</select>
|
</select>
|
||||||
<span className="product-count">{products.length} products</span>
|
<span className="product-count">
|
||||||
|
{showAll
|
||||||
|
? `${totalCount} products (all)`
|
||||||
|
: `${totalCount} products — page ${currentPage} of ${totalPages || 1}`}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowAll(s => !s); setCurrentPage(1) }}
|
||||||
|
style={{ marginLeft: '1rem', padding: '0.3rem 0.8rem', borderRadius: '4px', border: '1px solid #ccc', cursor: 'pointer', fontSize: '0.85rem' }}
|
||||||
|
>
|
||||||
|
{showAll ? 'Paginate' : 'Show All'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="loading">Loading products...</div>
|
<div className="loading">Loading products...</div>
|
||||||
) : products.length > 0 ? (
|
) : products.length > 0 ? (
|
||||||
|
<>
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
<ProductCard key={product.id} product={product} />
|
<ProductCard key={product.id} product={product} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination controls (hidden when Show All is active) */}
|
||||||
|
{!showAll && totalPages > 1 && (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem', marginTop: '2rem', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||||
|
disabled={currentPage <= 1}
|
||||||
|
style={{ padding: '0.5rem 1rem', borderRadius: '4px', border: '1px solid #ccc', cursor: currentPage <= 1 ? 'not-allowed' : 'pointer', opacity: currentPage <= 1 ? 0.5 : 1 }}
|
||||||
|
>
|
||||||
|
← Prev
|
||||||
|
</button>
|
||||||
|
{Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
|
||||||
|
const page = totalPages <= 7 ? i + 1 : currentPage <= 4 ? i + 1 : currentPage + i - 3
|
||||||
|
if (page < 1 || page > totalPages) return null
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
style={{ padding: '0.5rem 1rem', borderRadius: '4px', border: '1px solid #ccc', cursor: 'pointer', backgroundColor: page === currentPage ? '#333' : 'white', color: page === currentPage ? 'white' : 'inherit' }}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||||
|
disabled={currentPage >= totalPages}
|
||||||
|
style={{ padding: '0.5rem 1rem', borderRadius: '4px', border: '1px solid #ccc', cursor: currentPage >= totalPages ? 'not-allowed' : 'pointer', opacity: currentPage >= totalPages ? 0.5 : 1 }}
|
||||||
|
>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="no-products">
|
<div className="no-products">
|
||||||
<p>No products found.</p>
|
<p>No products found.</p>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user