feat: add Shop page with category filter and product inquiries
Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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
28
src/pages/Shop.module.css
Normal 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
28
src/pages/Shop.test.tsx
Normal 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
38
src/pages/Shop.tsx
Normal 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
|
||||
Reference in New Issue
Block a user