From 9f3e037cdc3e2dbb754a1ea26a092a845fbbb32f Mon Sep 17 00:00:00 2001 From: belviskhoremk Date: Mon, 3 Aug 2026 20:40:16 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20address=20final=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20Hero3D=20static=20class,=20InquiryModal=20a11y,=20d?= =?UTF-8?q?ate=20guards,=20shared=20email=20validation,=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH --- README.md | 79 +++------------------------- src/components/Hero3D.test.tsx | 3 +- src/components/Hero3D.tsx | 4 +- src/components/InquiryModal.test.tsx | 13 +++++ src/components/InquiryModal.tsx | 43 ++++++++++++--- src/data/availability.ts | 6 ++- src/hooks/useBooking.ts | 5 +- src/lib/validation.ts | 3 ++ src/pages/Book.tsx | 1 + 9 files changed, 69 insertions(+), 88 deletions(-) create mode 100644 src/lib/validation.ts diff --git a/README.md b/README.md index c300135..ed93d6f 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,10 @@ -# React + TypeScript + Vite +# Braids by Mira -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Public marketing site for Braids by Mira — landing page, product showcase, booking flow, and salon policies. -Currently, two official plugins are available: +## Scripts -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) - -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) - -``` +- `npm run dev` — start the local dev server +- `npm test` (or `npx vitest run`) — run the test suite +- `npm run build` — type-check and build for production +- `npm run lint` — run ESLint diff --git a/src/components/Hero3D.test.tsx b/src/components/Hero3D.test.tsx index e273ba1..3fc6357 100644 --- a/src/components/Hero3D.test.tsx +++ b/src/components/Hero3D.test.tsx @@ -31,6 +31,7 @@ describe('Hero3D', () => { }) const { default: Hero3D } = await import('./Hero3D') render() - expect(screen.getByTestId('hero-3d').className).toMatch(/static/) + expect(screen.getByTestId('hero-3d')).toBeInTheDocument() + expect(screen.getByTestId('hero-3d-motif').className).toMatch(/static/) }) }) diff --git a/src/components/Hero3D.tsx b/src/components/Hero3D.tsx index 68b7c22..e3ec881 100644 --- a/src/components/Hero3D.tsx +++ b/src/components/Hero3D.tsx @@ -9,8 +9,8 @@ function Hero3D() { if (prefersReducedMotion) { return ( -
-
+
+
) } diff --git a/src/components/InquiryModal.test.tsx b/src/components/InquiryModal.test.tsx index 955641b..85d7103 100644 --- a/src/components/InquiryModal.test.tsx +++ b/src/components/InquiryModal.test.tsx @@ -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() + await userEvent.keyboard('{Escape}') + expect(onClose).toHaveBeenCalled() + }) + + it('exposes an accessible dialog labelled by its heading', () => { + render() + const dialog = screen.getByRole('dialog', { name: /ask about silk edge control/i }) + expect(dialog).toHaveAttribute('aria-modal', 'true') + }) }) diff --git a/src/components/InquiryModal.tsx b/src/components/InquiryModal.tsx index 9f072fb..c0681c7 100644 --- a/src/components/InquiryModal.tsx +++ b/src/components/InquiryModal.tsx @@ -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({}) const [submitted, setSubmitted] = useState(false) + const nameInputRef = useRef(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 ( -
-
+
{ + if (e.target === e.currentTarget) onClose() + }} + > +
{submitted ? ( <>

Thanks — Mira will get back to you about {product.name} soon.

@@ -44,10 +66,15 @@ function InquiryModal({ product, onClose }: InquiryModalProps) { ) : (
-

Ask about {product.name}

+

Ask about {product.name}

- setName(e.target.value)} /> + setName(e.target.value)} + /> {errors.name && {errors.name}}
diff --git a/src/data/availability.ts b/src/data/availability.ts index f598cb0..792e08a 100644 --- a/src/data/availability.ts +++ b/src/data/availability.ts @@ -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 [] diff --git a/src/hooks/useBooking.ts b/src/hooks/useBooking.ts index efafab2..4c95d51 100644 --- a/src/hooks/useBooking.ts +++ b/src/hooks/useBooking.ts @@ -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(() => initialState(initialServiceId)) const [errors, setErrors] = useState({}) diff --git a/src/lib/validation.ts b/src/lib/validation.ts new file mode 100644 index 0000000..cf231bc --- /dev/null +++ b/src/lib/validation.ts @@ -0,0 +1,3 @@ +export function isValidEmail(email: string): boolean { + return /\S+@\S+\.\S+/.test(email) +} diff --git a/src/pages/Book.tsx b/src/pages/Book.tsx index 2ad0a35..8312d0a 100644 --- a/src/pages/Book.tsx +++ b/src/pages/Book.tsx @@ -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)