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:
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
38
src/data/rules.test.ts
Normal 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' })
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user