feat: replace static rules array with fetchRules; RuleIcon takes iconKey

- Rewrite src/data/rules.ts: export async fetchRules() instead of static rules array
- Fetch from 'rules' table, map icon_key to iconKey in RuleSection interface
- Create src/data/rules.test.ts with 2 tests (fetch mapping, error handling)
- Rename RuleIcon prop from ruleId to iconKey for consistency with new data shape
- Update RuleIcon.test.tsx to use iconKey prop name

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-13 09:05:25 +02:00
parent 2aee242a01
commit 35b28f6a2a
4 changed files with 66 additions and 15 deletions

View File

@@ -3,13 +3,13 @@ import { describe, it, expect } from 'vitest'
import RuleIcon from './RuleIcon'
describe('RuleIcon', () => {
it('renders an svg for a known rule id', () => {
const { container } = render(<RuleIcon ruleId="deposit" />)
it('renders an svg for a known icon key', () => {
const { container } = render(<RuleIcon iconKey="deposit" />)
expect(container.querySelector('svg')).toBeInTheDocument()
})
it('falls back to a default icon for an unknown rule id without throwing', () => {
const { container } = render(<RuleIcon ruleId="not-a-real-rule" />)
it('falls back to a default icon for an unknown icon key without throwing', () => {
const { container } = render(<RuleIcon iconKey="not-a-real-key" />)
expect(container.querySelector('svg path')).toBeInTheDocument()
})
})

View File

@@ -1,5 +1,5 @@
interface RuleIconProps {
ruleId: string
iconKey: string
}
const ICON_PATHS: Record<string, string> = {
@@ -11,8 +11,8 @@ const ICON_PATHS: Record<string, string> = {
payment: 'M3 6h18v12H3zM3 10h18M7 15h4',
}
function RuleIcon({ ruleId }: RuleIconProps) {
const d = ICON_PATHS[ruleId] ?? ICON_PATHS.deposit
function RuleIcon({ iconKey }: RuleIconProps) {
const d = ICON_PATHS[iconKey] ?? ICON_PATHS.deposit
return (
<svg

38
src/data/rules.test.ts Normal file
View File

@@ -0,0 +1,38 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
const orderMock = vi.fn()
const selectMock = vi.fn(() => ({ order: orderMock }))
const fromMock = vi.fn<(table: string) => { select: typeof selectMock }>(() => ({ select: selectMock }))
vi.mock('../lib/supabase', () => ({
supabase: { from: (table: string) => fromMock(table) },
}))
const ROW = { id: 'r1', title: 'Acompte', body: 'Body text.', icon_key: 'deposit', sort_order: 0 }
import { fetchRules, type RuleSection } from './rules'
describe('fetchRules', () => {
beforeEach(() => {
orderMock.mockReset()
selectMock.mockClear()
fromMock.mockClear()
})
it('fetches rules ordered by sort_order and maps to the RuleSection shape', async () => {
orderMock.mockResolvedValue({ data: [ROW], error: null })
const rules = await fetchRules()
expect(fromMock).toHaveBeenCalledWith('rules')
expect(orderMock).toHaveBeenCalledWith('sort_order')
expect(rules).toEqual<RuleSection[]>([
{ id: 'r1', title: 'Acompte', body: 'Body text.', iconKey: 'deposit' },
])
})
it('throws when the fetch fails', async () => {
orderMock.mockResolvedValue({ data: null, error: { message: 'network error' } })
await expect(fetchRules()).rejects.toEqual({ message: 'network error' })
})
})

View File

@@ -1,14 +1,27 @@
import { supabase } from '../lib/supabase'
export interface RuleSection {
id: string
title: string
body: string
iconKey: string
}
export const rules: RuleSection[] = [
{ id: 'deposit', title: 'Acompte', body: "Un acompte de 30 $ est exigé afin de confirmer toute réservation. Celui-ci n'est pas remboursable, mais transférable à une date ultérieure sur entente, pour un maximum d'une fois. Toute annulation moins de 48 heures avant le rendez-vous entraînera la perte de l'acompte, qui ne sera ni remboursé ni transféré. En cas de non-présentation (« no-show »), l'acompte est automatiquement perdu." },
{ id: 'hair-prep', title: 'Préalables cheveux', body: "Prière de garder vos cheveux propres, démêlés et secs, prêts à être tressés. Le blowdry peut être facturé si cette règle n'est pas respectée. Pas de bains d'huiles la veille ou le jour du rendez-vous, s'il vous plaît. Merci de toujours préciser, photo à l'appui, vos caractéristiques spécifiques (cheveux courts, locks, etc.)." },
{ id: 'extensions', title: 'Mèches et extensions', body: "Les mèches sont à déposer 24 à 48h avant la prestation si ce n'est pas moi qui les fournis. Choisissez de préférence des mèches pré-étirées ; l'étirage des mèches vous sera facturé en fonction du nombre de paquets." },
{ id: 'lateness', title: 'Retard', body: "Les retards sans notification préalable ne sont pas autorisés. Un retard de 15 à 20 minutes entraîne des frais supplémentaires, et à partir de 30 minutes, le rendez-vous est automatiquement annulé." },
{ id: 'guests', title: 'Accompagnant', body: "Vous êtes tenue de vous présenter seule à votre rendez-vous. Merci de ne pas venir accompagnée sans l'avoir signalé au préalable." },
{ id: 'payment', title: 'Paiement', body: 'Le paiement peut être fait en espèces ou par virement Interac.' },
]
interface RuleRow {
id: string
title: string
body: string
icon_key: string
sort_order: number
}
export async function fetchRules(): Promise<RuleSection[]> {
const { data, error } = await supabase.from('rules').select('*').order('sort_order')
if (error) throw error
return ((data ?? []) as RuleRow[]).map((row) => ({
id: row.id,
title: row.title,
body: row.body,
iconKey: row.icon_key,
}))
}