feat: wire Home page to live site content, testimonials, and featured products

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-13 09:11:46 +02:00
parent 004ba924b2
commit 36c042bb90
2 changed files with 58 additions and 25 deletions

View File

@@ -5,9 +5,14 @@ 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) => {
const testimonialsOrderMock = vi.fn()
const siteContentSingleMock = vi.fn()
const siteContentEqMock = vi.fn(() => ({ single: siteContentSingleMock }))
const fromMock = vi.fn<(table: string) => { select: () => { order: typeof productsOrderMock } | { eq: typeof siteContentEqMock } }>((table: string) => {
if (table === 'products') return { select: () => ({ order: productsOrderMock }) }
if (table === 'services') return { select: () => ({ order: servicesOrderMock }) }
if (table === 'testimonials') return { select: () => ({ order: testimonialsOrderMock }) }
if (table === 'site_content') return { select: () => ({ eq: siteContentEqMock }) }
throw new Error(`unexpected table: ${table}`)
})
@@ -19,11 +24,24 @@ vi.mock('../lib/resolveImageUrl', () => ({
}))
const PRODUCT_ROWS = [
{ id: 'edge-control', name: 'Gel Contour Soyeux', category: 'hair-care', price: 12, image: '', description: '', in_stock: true },
{ id: 'edge-control', name: 'Gel Contour Soyeux', category: 'hair-care', price: 12, image: '', description: '', in_stock: true, featured: true, featured_order: 0 },
{ id: 'braid-oil', name: 'Huile Nourrissante', category: 'hair-care', price: 16, image: '', description: '', in_stock: true, featured: false, featured_order: null },
]
const SERVICE_ROWS = [
{ id: 'knotless', name: 'Tresses sans nœuds', duration_minutes: 240, price_from: 150, description: '', image: '' },
]
const TESTIMONIAL_ROWS = [
{ id: 't1', quote: 'Fantastique service !', author: 'Amara T.', avatar_image: '', sort_order: 0 },
]
const SITE_CONTENT_ROW = {
id: 'default',
hero_headline: 'Vos cheveux, votre couronne',
hero_tagline: 'Coiffures protectrices par Mira',
about_bio: 'Bio de test.',
about_portrait_image: '',
contact_email: 'hello@braidsbymira.com',
contact_hours: 'Mar–Sam, 9h à 18h',
}
import Home from './Home'
import Book from './Book'
@@ -43,24 +61,33 @@ describe('Home page', () => {
beforeEach(() => {
productsOrderMock.mockReset().mockResolvedValue({ data: PRODUCT_ROWS, error: null })
servicesOrderMock.mockReset().mockResolvedValue({ data: SERVICE_ROWS, error: null })
testimonialsOrderMock.mockReset().mockResolvedValue({ data: TESTIMONIAL_ROWS, error: null })
siteContentSingleMock.mockReset().mockResolvedValue({ data: SITE_CONTENT_ROW, error: null })
fromMock.mockClear()
})
it('renders the hero headline and all signature styles', async () => {
renderHome()
expect(screen.getByText(/vos cheveux, votre couronne/i)).toBeInTheDocument()
expect(await screen.findByText(/vos cheveux, votre couronne/i)).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', async () => {
it('renders only featured products in shop highlights, and a link to the rules page', async () => {
renderHome()
await screen.findByText('Gel Contour Soyeux')
expect(await screen.findByText('Gel Contour Soyeux')).toBeInTheDocument()
expect(screen.queryByText('Huile Nourrissante')).not.toBeInTheDocument()
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('renders testimonials from site content', async () => {
renderHome()
expect(await screen.findByText(/fantastique service/i)).toBeInTheDocument()
expect(screen.getByText('Amara T.')).toBeInTheDocument()
})
it('navigates to /book with the chosen service preselected when a signature style is clicked', async () => {
renderHome()
const button = await screen.findByRole('button', { name: new RegExp(SERVICE_ROWS[0].name) })
@@ -68,8 +95,9 @@ describe('Home page', () => {
expect(screen.getByLabelText(/choisissez une date/i)).toBeInTheDocument()
})
it('renders the About Mira portrait', () => {
it('renders the About Mira portrait and bio from site content', async () => {
renderHome()
expect(await screen.findByText('Bio de test.')).toBeInTheDocument()
expect(screen.getByRole('img', { name: /mira/i })).toBeInTheDocument()
})
})

View File

@@ -7,14 +7,10 @@ import ServiceCard from '../components/ServiceCard'
import ProductCard from '../components/ProductCard'
import { fetchServices, type Service } from '../data/services'
import { fetchProducts, type Product } from '../data/products'
import { fetchTestimonials, type Testimonial } from '../data/testimonials'
import { fetchSiteContent, type SiteContent } from '../data/siteContent'
import styles from './Home.module.css'
const TESTIMONIALS = [
{ quote: "Mira m'a fait sentir comme une reine sur son fauteuil. Mes tresses ont tenu 8 semaines !", author: 'Amara T.', avatar: '/testimonials/amara.jpg' },
{ quote: "La tresseuse la plus douce et la plus précise que j'aie jamais consultée.", author: 'Keisha R.', avatar: '/testimonials/keisha.jpg' },
{ quote: 'La réservation était si simple, et le résultat parle de lui-même.', author: 'Nadia F.', avatar: '/testimonials/nadia.jpg' },
]
function Home() {
const navigate = useNavigate()
const { scrollYProgress } = useScroll()
@@ -23,15 +19,24 @@ function Home() {
const [services, setServices] = useState<Service[]>([])
const [products, setProducts] = useState<Product[]>([])
const [testimonials, setTestimonials] = useState<Testimonial[]>([])
const [content, setContent] = useState<SiteContent | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
useEffect(() => {
async function load() {
try {
const [servicesResult, productsResult] = await Promise.all([fetchServices(), fetchProducts()])
const [servicesResult, productsResult, testimonialsResult, contentResult] = await Promise.all([
fetchServices(),
fetchProducts(),
fetchTestimonials(),
fetchSiteContent(),
])
setServices(servicesResult)
setProducts(productsResult)
setTestimonials(testimonialsResult)
setContent(contentResult)
} catch (err) {
setError(err && typeof err === 'object' && 'message' in err ? String((err as { message: unknown }).message) : String(err))
} finally {
@@ -41,12 +46,16 @@ function Home() {
void load()
}, [])
const featuredProducts = products
.filter((p) => p.featured)
.sort((a, b) => (a.featuredOrder ?? 0) - (b.featuredOrder ?? 0))
return (
<div>
<section className={styles.hero}>
<Hero3DScene />
<h1 className={styles.headline}>Vos cheveux, votre couronne</h1>
<p className={styles.tagline}>Coiffures protectrices par Mira</p>
<h1 className={styles.headline}>{content?.heroHeadline ?? 'Vos cheveux, votre couronne'}</h1>
<p className={styles.tagline}>{content?.heroTagline ?? 'Coiffures protectrices par Mira'}</p>
<div className={styles.ctas}>
<Link to="/book">Réserver maintenant</Link>
<Link to="/shop">Voir les produits</Link>
@@ -60,18 +69,14 @@ function Home() {
<section className={styles.altSection}>
<div className={styles.aboutGrid}>
<motion.img
src="/about/mira-portrait.jpg"
src={content?.aboutPortraitImage}
alt="Mira, propriétaire et coiffeuse chez Braids by Mira"
className={styles.portrait}
style={{ y: portraitY }}
/>
<div>
<h2>À propos de Mira</h2>
<p>
Avec plus d'une décennie d'expérience derrière le fauteuil, Mira aborde chaque
installation comme une œuvre d'art — douce pour le cuir chevelu, précise à la
racine, et achevée pour une couronne que vous aurez envie d'afficher.
</p>
<p>{content?.aboutBio}</p>
</div>
</div>
</section>
@@ -96,7 +101,7 @@ function Home() {
<section className={styles.altSection}>
<h2>Sélection boutique</h2>
<div className={styles.row}>
{products.filter((p) => p.inStock).slice(0, 3).map((product) => (
{featuredProducts.map((product) => (
<ProductCard key={product.id} product={product} onInquire={() => navigate('/shop')} />
))}
</div>
@@ -108,9 +113,9 @@ function Home() {
<section className={styles.section}>
<h2>Avis de nos clientes</h2>
<div className={styles.row}>
{TESTIMONIALS.map((t) => (
<blockquote key={t.author} className={styles.testimonial}>
<img src={t.avatar} alt={t.author} className={styles.avatar} />
{testimonials.map((t) => (
<blockquote key={t.id} className={styles.testimonial}>
<img src={t.avatarImage} alt={t.author} className={styles.avatar} />
<p>&ldquo;{t.quote}&rdquo;</p>
<cite>{t.author}</cite>
</blockquote>