feat: add per-rule icons and staggered scroll-in to Rules page
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
15
src/components/RuleIcon.test.tsx
Normal file
15
src/components/RuleIcon.test.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { render } from '@testing-library/react'
|
||||
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" />)
|
||||
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" />)
|
||||
expect(container.querySelector('svg path')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
33
src/components/RuleIcon.tsx
Normal file
33
src/components/RuleIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
interface RuleIconProps {
|
||||
ruleId: string
|
||||
}
|
||||
|
||||
const ICON_PATHS: Record<string, string> = {
|
||||
deposit: 'M4 7h16M4 7a2 2 0 00-2 2v8a2 2 0 002 2h16a2 2 0 002-2V9a2 2 0 00-2-2M4 7l2-4h12l2 4M9 13h1v3H9zM14 13h1v3h-1z',
|
||||
cancellation: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
guests: 'M17 20h5v-2a4 4 0 00-3-3.87M9 20H4v-2a4 4 0 013-3.87m5-8.13a4 4 0 110 8 4 4 0 010-8zm6 4a4 4 0 10-3.2-6.4M5.2 5.6A4 4 0 108.4 12',
|
||||
'hair-prep': 'M12 3c-2 3-5 5-5 9a5 5 0 0010 0c0-4-3-6-5-9zM12 12v9',
|
||||
'health-safety': 'M12 2l8 4v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6l8-4z',
|
||||
}
|
||||
|
||||
function RuleIcon({ ruleId }: RuleIconProps) {
|
||||
const d = ICON_PATHS[ruleId] ?? ICON_PATHS.deposit
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d={d} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default RuleIcon
|
||||
@@ -5,7 +5,11 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: var(--space-lg);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-md);
|
||||
margin-bottom: var(--space-md);
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
@@ -13,6 +17,12 @@
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin: 0 0 0.35rem;
|
||||
}
|
||||
|
||||
.section p {
|
||||
color: var(--color-charcoal);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.cta {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import ScrollReveal from '../components/ScrollReveal'
|
||||
import RuleIcon from '../components/RuleIcon'
|
||||
import { rules } from '../data/rules'
|
||||
import styles from './Rules.module.css'
|
||||
|
||||
@@ -7,11 +9,16 @@ function Rules() {
|
||||
<div className={styles.page}>
|
||||
<h1>Salon Rules & Policies</h1>
|
||||
<p>Please read through before booking your appointment.</p>
|
||||
{rules.map((rule) => (
|
||||
<div key={rule.id} className={styles.section}>
|
||||
<h2>{rule.title}</h2>
|
||||
<p>{rule.body}</p>
|
||||
</div>
|
||||
{rules.map((rule, index) => (
|
||||
<ScrollReveal key={rule.id} delay={index * 0.08}>
|
||||
<div className={styles.section}>
|
||||
<RuleIcon ruleId={rule.id} />
|
||||
<div>
|
||||
<h2>{rule.title}</h2>
|
||||
<p>{rule.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
<div className={styles.cta}>
|
||||
<p>By booking, you agree to these terms.</p>
|
||||
|
||||
Reference in New Issue
Block a user