From b72acd8a2697d676b097dcc5d6b5b27002ebac76 Mon Sep 17 00:00:00 2001 From: belviskhoremk Date: Thu, 13 Aug 2026 09:32:14 +0200 Subject: [PATCH] feat: add featured toggle/reorder to AdminProducts; apply shared admin design --- src/pages/admin/AdminProducts.module.css | 43 ++++------- src/pages/admin/AdminProducts.test.tsx | 16 ++++ src/pages/admin/AdminProducts.tsx | 96 ++++++++++++++++++++---- 3 files changed, 112 insertions(+), 43 deletions(-) diff --git a/src/pages/admin/AdminProducts.module.css b/src/pages/admin/AdminProducts.module.css index bc2825b..402c53a 100644 --- a/src/pages/admin/AdminProducts.module.css +++ b/src/pages/admin/AdminProducts.module.css @@ -1,45 +1,28 @@ .page { - display: flex; - flex-direction: column; - gap: var(--space-lg); + composes: page from './adminShared.module.css'; } .table { - width: 100%; - border-collapse: collapse; - background: white; -} - -.table th, -.table td { - text-align: left; - padding: var(--space-sm); - border-bottom: 1px solid var(--color-cream-alt); + composes: table from './adminShared.module.css'; } .form { - display: flex; - flex-direction: column; - gap: var(--space-sm); - max-width: 420px; - background: white; - padding: var(--space-md); + composes: form from './adminShared.module.css'; } -.form label { - display: flex; - flex-direction: column; - gap: 0.25rem; - color: var(--color-charcoal); +.button { + composes: button from './adminShared.module.css'; } -.form input, -.form select, -.form textarea { - padding: var(--space-sm); - border: 1px solid var(--color-gold); +.buttonSecondary { + composes: buttonSecondary from './adminShared.module.css'; + margin-right: var(--admin-space-xs); +} + +.buttonDanger { + composes: buttonDanger from './adminShared.module.css'; } .error { - color: #a33; + composes: error from './adminShared.module.css'; } diff --git a/src/pages/admin/AdminProducts.test.tsx b/src/pages/admin/AdminProducts.test.tsx index efcb67b..aaadcea 100644 --- a/src/pages/admin/AdminProducts.test.tsx +++ b/src/pages/admin/AdminProducts.test.tsx @@ -25,6 +25,8 @@ const EXISTING_PRODUCT = { image: 'products/edge-control.jpg', description: 'Hold with shine.', in_stock: true, + featured: false, + featured_order: null, } import AdminProducts from './AdminProducts' @@ -80,4 +82,18 @@ describe('AdminProducts', () => { expect(deleteMock).toHaveBeenCalled() expect(eqMock).toHaveBeenCalledWith('id', 'edge-control') }) + + it('marks a product as featured, assigning it the next featured_order', async () => { + upsertMock.mockResolvedValue({ error: null }) + render() + await screen.findByText('Silk Edge Control') + + await userEvent.click(screen.getByRole('checkbox', { name: /vedette/i })) + + await waitFor(() => + expect(upsertMock).toHaveBeenCalledWith( + expect.objectContaining({ id: 'edge-control', featured: true, featured_order: 0 }), + ), + ) + }) }) diff --git a/src/pages/admin/AdminProducts.tsx b/src/pages/admin/AdminProducts.tsx index bc939fa..2368ce1 100644 --- a/src/pages/admin/AdminProducts.tsx +++ b/src/pages/admin/AdminProducts.tsx @@ -12,6 +12,8 @@ interface AdminProduct { image: string description: string in_stock: boolean + featured: boolean + featured_order: number | null } const CATEGORIES: AdminProduct['category'][] = ['hair-care', 'extensions', 'tools', 'accessories'] @@ -24,7 +26,7 @@ const CATEGORY_LABELS: Record = { } function emptyForm(): AdminProduct { - return { id: '', name: '', category: 'hair-care', price: 0, image: '', description: '', in_stock: true } + return { id: '', name: '', category: 'hair-care', price: 0, image: '', description: '', in_stock: true, featured: false, featured_order: null } } function AdminProducts() { @@ -44,15 +46,7 @@ function AdminProducts() { } useEffect(() => { - async function load() { - const { data, error: fetchError } = await supabase.from('products').select('*').order('name') - if (fetchError) { - setError(fetchError.message) - return - } - setProducts((data ?? []) as AdminProduct[]) - } - void load() + void loadProducts() }, []) async function handleSubmit(event: FormEvent) { @@ -92,7 +86,52 @@ function AdminProducts() { await loadProducts() } + async function handleToggleFeatured(product: AdminProduct) { + setError('') + if (product.featured) { + const { error: upsertError } = await supabase.from('products').upsert({ ...product, featured: false, featured_order: null }) + if (upsertError) { + setError(upsertError.message) + return + } + } else { + const maxOrder = products.reduce((max, p) => (p.featured && p.featured_order !== null ? Math.max(max, p.featured_order) : max), -1) + const { error: upsertError } = await supabase.from('products').upsert({ ...product, featured: true, featured_order: maxOrder + 1 }) + if (upsertError) { + setError(upsertError.message) + return + } + } + await loadProducts() + } + + async function handleMoveFeatured(product: AdminProduct, direction: -1 | 1) { + const featured = products + .filter((p) => p.featured) + .sort((a, b) => (a.featured_order ?? 0) - (b.featured_order ?? 0)) + const index = featured.findIndex((p) => p.id === product.id) + const targetIndex = index + direction + if (targetIndex < 0 || targetIndex >= featured.length) return + const current = featured[index] + const target = featured[targetIndex] + setError('') + const { error: err1 } = await supabase.from('products').upsert({ ...current, featured_order: target.featured_order }) + if (err1) { + setError(err1.message) + return + } + const { error: err2 } = await supabase.from('products').upsert({ ...target, featured_order: current.featured_order }) + if (err2) { + setError(err2.message) + return + } + await loadProducts() + } + const isEditing = Boolean(form.id) && products.some((p) => p.id === form.id) + const featuredProducts = products + .filter((p) => p.featured) + .sort((a, b) => (a.featured_order ?? 0) - (b.featured_order ?? 0)) return (
@@ -104,6 +143,7 @@ function AdminProducts() { Catégorie Prix En stock + Vedette @@ -115,8 +155,38 @@ function AdminProducts() { {product.price} $ {product.in_stock ? 'Oui' : 'Non'} - - + + {product.featured && ( + <> + + + + )} + + + + ))} @@ -195,7 +265,7 @@ function AdminProducts() { En stock {error &&

{error}

} - +
)