feat: wire Shop page to live Supabase product data

This commit is contained in:
belviskhoremk
2026-08-12 20:32:43 +02:00
parent c8d24a2a3a
commit 6a27127208
4 changed files with 73 additions and 12 deletions

View File

@@ -1,16 +1,27 @@
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi } from 'vitest'
import App from './App'
vi.mock('./lib/supabase', () => ({
supabase: {
from: () => ({
select: () => ({
order: () => Promise.resolve({ data: [], error: null }),
eq: () => ({ single: () => Promise.resolve({ data: null, error: null }) }),
}),
}),
},
}))
describe('App routing', () => {
it('renders the Home page at /', async () => {
render(<App initialEntries={['/']} />)
expect(await screen.findByText(/vos cheveux, votre couronne/i)).toBeInTheDocument()
})
it('renders the Shop page at /shop', () => {
it('renders the Shop page at /shop', async () => {
render(<App initialEntries={['/shop']} />)
expect(screen.getByRole('heading', { name: /boutique/i })).toBeInTheDocument()
expect(await screen.findByRole('heading', { name: /boutique/i })).toBeInTheDocument()
})
it('renders the Book page at /book', () => {

View File

@@ -2,6 +2,10 @@
padding: var(--space-xl) var(--space-lg);
}
.error {
color: #a33;
}
.filters {
display: flex;
gap: var(--space-sm);

View File

@@ -1,19 +1,41 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi, beforeEach } from 'vitest'
const orderMock = vi.fn()
const selectMock = vi.fn(() => ({ order: orderMock }))
const fromMock = vi.fn<(...args: unknown[]) => { select: typeof selectMock }>(() => ({ select: selectMock }))
vi.mock('../lib/supabase', () => ({
supabase: { from: (...args: unknown[]) => fromMock(...args) },
}))
vi.mock('../lib/resolveImageUrl', () => ({
resolveImageUrl: (path: string) => path,
}))
const PRODUCT_ROWS = [
{ id: 'edge-control', name: 'Gel Contour Soyeux', category: 'hair-care', price: 12, image: '', description: '', in_stock: true },
{ id: 'rat-tail-comb', name: 'Peigne à Tige en Acier', category: 'tools', price: 6, image: '', description: '', in_stock: true },
]
import Shop from './Shop'
import { products } from '../data/products'
describe('Shop page', () => {
it('renders all products by default', () => {
beforeEach(() => {
orderMock.mockReset().mockResolvedValue({ data: PRODUCT_ROWS, error: null })
fromMock.mockClear()
})
it('renders all products by default', async () => {
render(<Shop />)
for (const p of products) {
expect(screen.getByText(p.name)).toBeInTheDocument()
for (const p of PRODUCT_ROWS) {
expect(await screen.findByText(p.name)).toBeInTheDocument()
}
})
it('filters products by category', async () => {
render(<Shop />)
await screen.findByText('Gel Contour Soyeux')
await userEvent.click(screen.getByRole('button', { name: /^outils$/i }))
expect(screen.getByText('Peigne à Tige en Acier')).toBeInTheDocument()
expect(screen.queryByText('Gel Contour Soyeux')).not.toBeInTheDocument()
@@ -21,8 +43,14 @@ describe('Shop page', () => {
it('opens the inquiry modal when a product is inquired about', async () => {
render(<Shop />)
const buttons = screen.getAllByRole('button', { name: /poser une question/i })
const buttons = await screen.findAllByRole('button', { name: /poser une question/i })
await userEvent.click(buttons[0])
expect(screen.getByRole('button', { name: /envoyer la demande/i })).toBeInTheDocument()
})
it('shows an error message when the fetch fails', async () => {
orderMock.mockResolvedValue({ data: null, error: { message: 'network error' } })
render(<Shop />)
expect(await screen.findByText(/network error/i)).toBeInTheDocument()
})
})

View File

@@ -1,8 +1,8 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { motion, useReducedMotion } from 'framer-motion'
import ProductCard from '../components/ProductCard'
import InquiryModal from '../components/InquiryModal'
import { getProductsByCategory, type Product } from '../data/products'
import { fetchProducts, getProductsByCategory, type Product } from '../data/products'
import styles from './Shop.module.css'
const CATEGORIES: (Product['category'] | 'all')[] = ['all', 'hair-care', 'extensions', 'tools', 'accessories']
@@ -16,13 +16,31 @@ const CATEGORY_LABELS: Record<Product['category'] | 'all', string> = {
}
function Shop() {
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [category, setCategory] = useState<Product['category'] | 'all'>('all')
const [inquiring, setInquiring] = useState<Product | null>(null)
const prefersReducedMotion = useReducedMotion()
useEffect(() => {
async function load() {
try {
setProducts(await fetchProducts())
} catch (err) {
setError(err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) : String(err))
} finally {
setLoading(false)
}
}
void load()
}, [])
return (
<div className={styles.page}>
<h1>Boutique</h1>
{loading && <p>Chargement…</p>}
{error && <p className={styles.error}>{error}</p>}
<div className={styles.filters}>
{CATEGORIES.map((c) => (
<button
@@ -43,7 +61,7 @@ function Shop() {
))}
</div>
<div className={styles.grid}>
{getProductsByCategory(category).map((product) => (
{getProductsByCategory(products, category).map((product) => (
<ProductCard key={product.id} product={product} onInquire={setInquiring} />
))}
</div>