From c5a2b9dfae04f16b92fb19c61681c51d03b8b3ac Mon Sep 17 00:00:00 2001 From: belviskhoremk Date: Thu, 13 Aug 2026 09:01:18 +0200 Subject: [PATCH] feat: add fetchSiteContent data function --- src/data/siteContent.test.ts | 59 ++++++++++++++++++++++++++++++++++++ src/data/siteContent.ts | 34 +++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/data/siteContent.test.ts create mode 100644 src/data/siteContent.ts diff --git a/src/data/siteContent.test.ts b/src/data/siteContent.test.ts new file mode 100644 index 0000000..a748887 --- /dev/null +++ b/src/data/siteContent.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const singleMock = vi.fn() +const eqMock = vi.fn(() => ({ single: singleMock })) +const selectMock = vi.fn(() => ({ eq: eqMock })) +const fromMock = vi.fn<(table: string) => { select: typeof selectMock }>(() => ({ select: selectMock })) +const resolveImageUrlMock = vi.fn((path: string) => `https://cdn.example/${path}`) + +vi.mock('../lib/supabase', () => ({ + supabase: { from: (table: string) => fromMock(table) }, +})) +vi.mock('../lib/resolveImageUrl', () => ({ + resolveImageUrl: (path: string) => resolveImageUrlMock(path), +})) + +const ROW = { + id: 'default', + hero_headline: 'Vos cheveux, votre couronne', + hero_tagline: 'Coiffures protectrices par Mira', + about_bio: 'Bio text.', + about_portrait_image: 'about/mira-portrait.jpg', + contact_email: 'hello@braidsbymira.com', + contact_hours: 'Mar–Sam, 9h à 18h', +} + +import { fetchSiteContent, type SiteContent } from './siteContent' + +describe('fetchSiteContent', () => { + beforeEach(() => { + singleMock.mockReset() + eqMock.mockClear() + selectMock.mockClear() + fromMock.mockClear() + resolveImageUrlMock.mockClear() + }) + + it('fetches the default row and maps snake_case columns to the SiteContent shape', async () => { + singleMock.mockResolvedValue({ data: ROW, error: null }) + + const content = await fetchSiteContent() + + expect(fromMock).toHaveBeenCalledWith('site_content') + expect(eqMock).toHaveBeenCalledWith('id', 'default') + expect(resolveImageUrlMock).toHaveBeenCalledWith('about/mira-portrait.jpg') + expect(content).toEqual({ + heroHeadline: 'Vos cheveux, votre couronne', + heroTagline: 'Coiffures protectrices par Mira', + aboutBio: 'Bio text.', + aboutPortraitImage: 'https://cdn.example/about/mira-portrait.jpg', + contactEmail: 'hello@braidsbymira.com', + contactHours: 'Mar–Sam, 9h à 18h', + }) + }) + + it('throws when the fetch fails', async () => { + singleMock.mockResolvedValue({ data: null, error: { message: 'network error' } }) + await expect(fetchSiteContent()).rejects.toEqual({ message: 'network error' }) + }) +}) diff --git a/src/data/siteContent.ts b/src/data/siteContent.ts new file mode 100644 index 0000000..3d81a93 --- /dev/null +++ b/src/data/siteContent.ts @@ -0,0 +1,34 @@ +import { supabase } from '../lib/supabase' +import { resolveImageUrl } from '../lib/resolveImageUrl' + +export interface SiteContent { + heroHeadline: string + heroTagline: string + aboutBio: string + aboutPortraitImage: string + contactEmail: string + contactHours: string +} + +interface SiteContentRow { + hero_headline: string + hero_tagline: string + about_bio: string + about_portrait_image: string + contact_email: string + contact_hours: string +} + +export async function fetchSiteContent(): Promise { + const { data, error } = await supabase.from('site_content').select('*').eq('id', 'default').single() + if (error) throw error + const row = data as SiteContentRow + return { + heroHeadline: row.hero_headline, + heroTagline: row.hero_tagline, + aboutBio: row.about_bio, + aboutPortraitImage: resolveImageUrl(row.about_portrait_image), + contactEmail: row.contact_email, + contactHours: row.contact_hours, + } +}