feat: add Shop page with category filter and product inquiries

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
belviskhoremk
2026-08-03 17:33:55 +00:00
parent cdc74488cd
commit 698f79daf6
5 changed files with 98 additions and 3 deletions

View File

@@ -10,9 +10,9 @@ describe('App routing', () => {
expect(screen.getByText(/hello@braidsbymira.com/)).toBeInTheDocument()
})
it('renders the shop placeholder at /shop', () => {
it('renders the Shop page at /shop', () => {
render(<App initialEntries={['/shop']} />)
expect(screen.getByTestId('page-shop')).toBeInTheDocument()
expect(screen.getByRole('heading', { name: /shop/i })).toBeInTheDocument()
})
it('renders the book placeholder at /book', () => {

View File

@@ -1,5 +1,6 @@
import { BrowserRouter, MemoryRouter, Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Shop from './pages/Shop'
interface AppProps {
initialEntries?: string[]
@@ -10,7 +11,7 @@ function AppRoutes() {
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<div data-testid="page-home" />} />
<Route path="/shop" element={<div data-testid="page-shop" />} />
<Route path="/shop" element={<Shop />} />
<Route path="/book" element={<div data-testid="page-book" />} />
<Route path="/rules" element={<div data-testid="page-rules" />} />
</Route>

28
src/pages/Shop.module.css Normal file
View File

@@ -0,0 +1,28 @@
.page {
padding: var(--space-xl) var(--space-lg);
}
.filters {
display: flex;
gap: var(--space-sm);
margin-bottom: var(--space-lg);
}
.filters button {
background: none;
border: 1px solid var(--color-gold);
padding: 0.5rem 1rem;
cursor: pointer;
text-transform: capitalize;
}
.filters button.active {
background: var(--color-charcoal);
color: var(--color-cream);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: var(--space-lg);
}

28
src/pages/Shop.test.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, it, expect } from 'vitest'
import Shop from './Shop'
import { products } from '../data/products'
describe('Shop page', () => {
it('renders all products by default', () => {
render(<Shop />)
for (const p of products) {
expect(screen.getByText(p.name)).toBeInTheDocument()
}
})
it('filters products by category', async () => {
render(<Shop />)
await userEvent.click(screen.getByRole('button', { name: /^tools$/i }))
expect(screen.getByText('Steel Rat-Tail Comb')).toBeInTheDocument()
expect(screen.queryByText('Silk Edge Control')).not.toBeInTheDocument()
})
it('opens the inquiry modal when a product is inquired about', async () => {
render(<Shop />)
const buttons = screen.getAllByRole('button', { name: /ask about this/i })
await userEvent.click(buttons[0])
expect(screen.getByRole('button', { name: /send inquiry/i })).toBeInTheDocument()
})
})

38
src/pages/Shop.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { useState } from 'react'
import ProductCard from '../components/ProductCard'
import InquiryModal from '../components/InquiryModal'
import { getProductsByCategory, type Product } from '../data/products'
import styles from './Shop.module.css'
const CATEGORIES: (Product['category'] | 'all')[] = ['all', 'hair-care', 'extensions', 'tools', 'accessories']
function Shop() {
const [category, setCategory] = useState<Product['category'] | 'all'>('all')
const [inquiring, setInquiring] = useState<Product | null>(null)
return (
<div className={styles.page}>
<h1>Shop</h1>
<div className={styles.filters}>
{CATEGORIES.map((c) => (
<button
key={c}
type="button"
className={c === category ? 'active' : ''}
onClick={() => setCategory(c)}
>
{c.replace('-', ' ')}
</button>
))}
</div>
<div className={styles.grid}>
{getProductsByCategory(category).map((product) => (
<ProductCard key={product.id} product={product} onInquire={setInquiring} />
))}
</div>
{inquiring && <InquiryModal product={inquiring} onClose={() => setInquiring(null)} />}
</div>
)
}
export default Shop