fix: address final review findings — Hero3D static class, InquiryModal a11y, date guards, shared email validation, README

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-03 20:40:16 +00:00
parent 251a6558fb
commit 9f3e037cdc
9 changed files with 69 additions and 88 deletions

View File

@@ -31,6 +31,7 @@ describe('Hero3D', () => {
})
const { default: Hero3D } = await import('./Hero3D')
render(<Hero3D />)
expect(screen.getByTestId('hero-3d').className).toMatch(/static/)
expect(screen.getByTestId('hero-3d')).toBeInTheDocument()
expect(screen.getByTestId('hero-3d-motif').className).toMatch(/static/)
})
})

View File

@@ -9,8 +9,8 @@ function Hero3D() {
if (prefersReducedMotion) {
return (
<div className={`${styles.wrapper} static`} data-testid="hero-3d">
<div className={styles.motif} />
<div className={styles.wrapper} data-testid="hero-3d">
<div className={styles.static} data-testid="hero-3d-motif" />
</div>
)
}

View File

@@ -32,4 +32,17 @@ describe('InquiryModal', () => {
await userEvent.click(screen.getByRole('button', { name: /close/i }))
expect(onClose).toHaveBeenCalled()
})
it('calls onClose when Escape is pressed', async () => {
const onClose = vi.fn()
render(<InquiryModal product={product} onClose={onClose} />)
await userEvent.keyboard('{Escape}')
expect(onClose).toHaveBeenCalled()
})
it('exposes an accessible dialog labelled by its heading', () => {
render(<InquiryModal product={product} onClose={vi.fn()} />)
const dialog = screen.getByRole('dialog', { name: /ask about silk edge control/i })
expect(dialog).toHaveAttribute('aria-modal', 'true')
})
})

View File

@@ -1,5 +1,6 @@
import { useState, type FormEvent } from 'react'
import { useEffect, useRef, useState, type FormEvent } from 'react'
import type { Product } from '../data/products'
import { isValidEmail } from '../lib/validation'
import styles from './InquiryModal.module.css'
interface InquiryModalProps {
@@ -12,9 +13,7 @@ interface Errors {
email?: string
}
function isValidEmail(email: string): boolean {
return /\S+@\S+\.\S+/.test(email)
}
const HEADING_ID = 'inquiry-modal-heading'
function InquiryModal({ product, onClose }: InquiryModalProps) {
const [name, setName] = useState('')
@@ -22,6 +21,19 @@ function InquiryModal({ product, onClose }: InquiryModalProps) {
const [notes, setNotes] = useState('')
const [errors, setErrors] = useState<Errors>({})
const [submitted, setSubmitted] = useState(false)
const nameInputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
nameInputRef.current?.focus()
}, [])
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [onClose])
function handleSubmit(e: FormEvent) {
e.preventDefault()
@@ -35,8 +47,18 @@ function InquiryModal({ product, onClose }: InquiryModalProps) {
}
return (
<div className={styles.overlay}>
<div className={styles.modal}>
<div
className={styles.overlay}
onClick={(e) => {
if (e.target === e.currentTarget) onClose()
}}
>
<div
className={styles.modal}
role="dialog"
aria-modal="true"
aria-labelledby={HEADING_ID}
>
{submitted ? (
<>
<p>Thanks — Mira will get back to you about {product.name} soon.</p>
@@ -44,10 +66,15 @@ function InquiryModal({ product, onClose }: InquiryModalProps) {
</>
) : (
<form onSubmit={handleSubmit}>
<h3>Ask about {product.name}</h3>
<h3 id={HEADING_ID}>Ask about {product.name}</h3>
<div className={styles.field}>
<label htmlFor="inquiry-name">Name</label>
<input id="inquiry-name" value={name} onChange={(e) => setName(e.target.value)} />
<input
id="inquiry-name"
ref={nameInputRef}
value={name}
onChange={(e) => setName(e.target.value)}
/>
{errors.name && <span className={styles.error}>{errors.name}</span>}
</div>
<div className={styles.field}>

View File

@@ -13,7 +13,11 @@ function seedFromDate(date: string): number {
}
export function getSlotsForDate(date: string): TimeSlot[] {
const day = new Date(`${date}T00:00:00`).getDay()
const parsed = new Date(`${date}T00:00:00`)
if (isNaN(parsed.getTime())) {
return []
}
const day = parsed.getDay()
if (day === 0) {
// Sundays: closed, no slots at all
return []

View File

@@ -1,5 +1,6 @@
import { useState, useCallback } from 'react'
import type { TimeSlot } from '../data/availability'
import { isValidEmail } from '../lib/validation'
export interface BookingContact {
name: string
@@ -30,10 +31,6 @@ function initialState(initialServiceId?: string): BookingState {
}
}
function isValidEmail(email: string): boolean {
return /\S+@\S+\.\S+/.test(email)
}
export default function useBooking(initialServiceId?: string) {
const [state, setState] = useState<BookingState>(() => initialState(initialServiceId))
const [errors, setErrors] = useState<BookingErrors>({})

3
src/lib/validation.ts Normal file
View File

@@ -0,0 +1,3 @@
export function isValidEmail(email: string): boolean {
return /\S+@\S+\.\S+/.test(email)
}

View File

@@ -53,6 +53,7 @@ function Book() {
id="booking-date"
type="date"
value={dateInput}
min={new Date().toISOString().slice(0, 10)}
onChange={(e) => {
setDateInput(e.target.value)
booking.selectDate(e.target.value)