From a0e8a2ca6029e074325c26ebdafe54268bc59b73 Mon Sep 17 00:00:00 2001 From: belviskhoremk Date: Wed, 12 Aug 2026 20:12:53 +0200 Subject: [PATCH] feat: fetch services from Supabase instead of static mock data --- src/data/services.test.ts | 58 +++++++++++++++++++++++++++++++++++++++ src/data/services.ts | 30 ++++++++++++++++---- 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/data/services.test.ts diff --git a/src/data/services.test.ts b/src/data/services.test.ts new file mode 100644 index 0000000..c758d14 --- /dev/null +++ b/src/data/services.test.ts @@ -0,0 +1,58 @@ +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 })) +const resolveImageUrlMock = vi.fn((path: string) => `https://cdn.example/${path}`) + +vi.mock('../lib/supabase', () => ({ + supabase: { from: (...args: unknown[]) => fromMock(...args) }, +})) +vi.mock('../lib/resolveImageUrl', () => ({ + resolveImageUrl: (path: string) => resolveImageUrlMock(path), +})) + +const ROW = { + id: 'knotless', + name: 'Tresses sans nœuds', + duration_minutes: 240, + price_from: 150, + description: 'Tresses sans tension.', + image: 'styles/knotless.jpg', +} + +import { fetchServices, type Service } from './services' + +describe('fetchServices', () => { + beforeEach(() => { + orderMock.mockReset() + selectMock.mockClear() + fromMock.mockClear() + resolveImageUrlMock.mockClear() + }) + + it('fetches services ordered by name and maps snake_case columns to the Service shape', async () => { + orderMock.mockResolvedValue({ data: [ROW], error: null }) + + const services = await fetchServices() + + expect(fromMock).toHaveBeenCalledWith('services') + expect(orderMock).toHaveBeenCalledWith('name') + expect(resolveImageUrlMock).toHaveBeenCalledWith('styles/knotless.jpg') + expect(services).toEqual([ + { + id: 'knotless', + name: 'Tresses sans nœuds', + durationMinutes: 240, + priceFrom: 150, + description: 'Tresses sans tension.', + image: 'https://cdn.example/styles/knotless.jpg', + }, + ]) + }) + + it('throws when the fetch fails', async () => { + orderMock.mockResolvedValue({ data: null, error: { message: 'network error' } }) + await expect(fetchServices()).rejects.toEqual({ message: 'network error' }) + }) +}) diff --git a/src/data/services.ts b/src/data/services.ts index 2c94e90..1e1baf0 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -1,3 +1,6 @@ +import { supabase } from '../lib/supabase' +import { resolveImageUrl } from '../lib/resolveImageUrl' + export interface Service { id: string name: string @@ -7,9 +10,24 @@ export interface Service { image: string } -export const services: Service[] = [ - { id: 'knotless', name: 'Tresses sans nœuds', durationMinutes: 240, priceFrom: 150, description: 'Tresses sans tension avec une racine naturelle et invisible.', image: '/styles/knotless.jpg' }, - { id: 'boho', name: 'Tresses Boho', durationMinutes: 270, priceFrom: 180, description: 'Tresses sans nœuds agrémentées de mèches bouclées pour un look doux et romantique.', image: '/styles/boho.jpg' }, - { id: 'locs', name: 'Faux Locs', durationMinutes: 300, priceFrom: 200, description: 'Locs légères à l\'allure naturelle, installées avec soin.', image: '/styles/locs.jpg' }, - { id: 'cornrows', name: 'Nattes Collées', durationMinutes: 120, priceFrom: 90, description: 'Nattes collées classiques et nettes, droites vers l\'arrière ou avec un motif personnalisé.', image: '/styles/cornrows.jpg' }, -] +interface ServiceRow { + id: string + name: string + duration_minutes: number + price_from: number + description: string + image: string +} + +export async function fetchServices(): Promise { + const { data, error } = await supabase.from('services').select('*').order('name') + if (error) throw error + return ((data ?? []) as ServiceRow[]).map((row) => ({ + id: row.id, + name: row.name, + durationMinutes: row.duration_minutes, + priceFrom: row.price_from, + description: row.description, + image: resolveImageUrl(row.image), + })) +}