feat: wire Home page to live Supabase product and service data
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
padding: var(--space-xl) var(--space-lg);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #a33;
|
||||
}
|
||||
|
||||
.headline {
|
||||
font-family: var(--font-script);
|
||||
font-size: 3rem;
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user