feat: wire Home page to live Supabase product and service data

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-12 20:37:55 +02:00
parent 6a27127208
commit f45a5bf8ec
3 changed files with 63 additions and 9 deletions

View File

@@ -3,6 +3,10 @@
padding: var(--space-xl) var(--space-lg);
}
.error {
color: #a33;
}
.headline {
font-family: var(--font-script);
font-size: 3rem;

View File

@@ -1,10 +1,32 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter, Routes, Route } from 'react-router-dom'
import { describe, it, expect } from 'vitest'
import { describe, it, expect, vi, beforeEach } from 'vitest'
const productsOrderMock = vi.fn()
const servicesOrderMock = vi.fn()
const fromMock = vi.fn<(table: string) => { select: () => { order: typeof productsOrderMock } }>((table: string) => {
if (table === 'products') return { select: () => ({ order: productsOrderMock }) }
if (table === 'services') return { select: () => ({ order: servicesOrderMock }) }
throw new Error(`unexpected table: ${table}`)
})
vi.mock('../lib/supabase', () => ({
supabase: { from: (table: string) => fromMock(table) },
}))
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 },
]
const SERVICE_ROWS = [
{ id: 'knotless', name: 'Tresses sans nœuds', duration_minutes: 240, price_from: 150, description: '', image: '' },
]
import Home from './Home'
import Book from './Book'
import { services } from '../data/services'
function renderHome() {
return render(
@@ -18,23 +40,31 @@ function renderHome() {
}
describe('Home page', () => {
it('renders the hero headline and all four signature styles', () => {
beforeEach(() => {
productsOrderMock.mockReset().mockResolvedValue({ data: PRODUCT_ROWS, error: null })
servicesOrderMock.mockReset().mockResolvedValue({ data: SERVICE_ROWS, error: null })
fromMock.mockClear()
})
it('renders the hero headline and all signature styles', async () => {
renderHome()
expect(screen.getByText(/vos cheveux, votre couronne/i)).toBeInTheDocument()
for (const service of services) {
expect(screen.getByText(service.name)).toBeInTheDocument()
for (const service of SERVICE_ROWS) {
expect(await screen.findByText(service.name)).toBeInTheDocument()
}
})
it('renders shop highlights and a link to the rules page', () => {
it('renders shop highlights and a link to the rules page', async () => {
renderHome()
await screen.findByText('Gel Contour Soyeux')
expect(screen.getByRole('link', { name: /voir toute la boutique/i })).toHaveAttribute('href', '/shop')
expect(screen.getByRole('link', { name: /règlements du salon/i })).toHaveAttribute('href', '/rules')
})
it('navigates to /book with the chosen service preselected when a signature style is clicked', async () => {
renderHome()
await userEvent.click(screen.getByRole('button', { name: new RegExp(services[0].name) }))
const button = await screen.findByRole('button', { name: new RegExp(SERVICE_ROWS[0].name) })
await userEvent.click(button)
expect(screen.getByLabelText(/choisissez une date/i)).toBeInTheDocument()
})

View File

@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { motion, useScroll, useTransform, useReducedMotion } from 'framer-motion'
import Hero3DScene from '../components/Hero3DScene'
import ScrollReveal from '../components/ScrollReveal'
import ServiceCard from '../components/ServiceCard'
import ProductCard from '../components/ProductCard'
import { services } from '../data/services'
import { products } from '../data/products'
import { fetchServices, type Service } from '../data/services'
import { fetchProducts, type Product } from '../data/products'
import styles from './Home.module.css'
const TESTIMONIALS = [
@@ -20,6 +21,23 @@ function Home() {
const prefersReducedMotion = useReducedMotion()
const portraitY = useTransform(scrollYProgress, [0, 1], prefersReducedMotion ? [0, 0] : [0, -40])
const [services, setServices] = useState<Service[]>([])
const [products, setProducts] = useState<Product[]>([])
const [error, setError] = useState('')
useEffect(() => {
async function load() {
try {
const [servicesResult, productsResult] = await Promise.all([fetchServices(), fetchProducts()])
setServices(servicesResult)
setProducts(productsResult)
} catch (err) {
setError(err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) : String(err))
}
}
void load()
}, [])
return (
<div>
<section className={styles.hero}>
@@ -32,6 +50,8 @@ function Home() {
</div>
</section>
{error && <p className={styles.error}>{error}</p>}
<ScrollReveal>
<section className={styles.altSection}>
<div className={styles.aboutGrid}>