diff --git a/src/components/ProductCard.module.css b/src/components/ProductCard.module.css
new file mode 100644
index 0000000..3b73999
--- /dev/null
+++ b/src/components/ProductCard.module.css
@@ -0,0 +1,33 @@
+.card {
+ background: var(--color-cream-alt);
+ border-radius: 4px;
+ padding: var(--space-md);
+ transition: transform 0.25s ease;
+}
+
+.card:hover {
+ transform: translateY(-6px) rotateX(2deg);
+}
+
+.badge {
+ color: #a0453a;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.button {
+ background: var(--color-charcoal);
+ color: var(--color-cream);
+ border: none;
+ padding: 0.6rem 1.2rem;
+ font-size: 0.75rem;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ cursor: pointer;
+}
+
+.button:disabled {
+ background: #b7ada3;
+ cursor: not-allowed;
+}
diff --git a/src/components/ProductCard.test.tsx b/src/components/ProductCard.test.tsx
new file mode 100644
index 0000000..b976639
--- /dev/null
+++ b/src/components/ProductCard.test.tsx
@@ -0,0 +1,28 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, it, expect, vi } from 'vitest'
+import ProductCard from './ProductCard'
+import type { Product } from '../data/products'
+
+const product: Product = {
+ id: 'edge-control', name: 'Silk Edge Control', category: 'hair-care',
+ price: 12, image: '/x.jpg', description: 'Hold with shine.', inStock: true,
+}
+
+describe('ProductCard', () => {
+ it('renders product details and calls onInquire when clicked', async () => {
+ const onInquire = vi.fn()
+ render()
+ expect(screen.getByText('Silk Edge Control')).toBeInTheDocument()
+ expect(screen.getByText('$12')).toBeInTheDocument()
+ await userEvent.click(screen.getByRole('button', { name: /ask about this/i }))
+ expect(onInquire).toHaveBeenCalledWith(product)
+ })
+
+ it('disables the inquire button when out of stock', () => {
+ const onInquire = vi.fn()
+ render()
+ expect(screen.getByRole('button', { name: /ask about this/i })).toBeDisabled()
+ expect(screen.getByText(/unavailable/i)).toBeInTheDocument()
+ })
+})
diff --git a/src/components/ProductCard.tsx b/src/components/ProductCard.tsx
new file mode 100644
index 0000000..a1c058f
--- /dev/null
+++ b/src/components/ProductCard.tsx
@@ -0,0 +1,28 @@
+import type { Product } from '../data/products'
+import styles from './ProductCard.module.css'
+
+interface ProductCardProps {
+ product: Product
+ onInquire: (product: Product) => void
+}
+
+function ProductCard({ product, onInquire }: ProductCardProps) {
+ return (
+
+
{product.name}
+
${product.price}
+
{product.description}
+ {!product.inStock &&
Currently unavailable
}
+
+
+ )
+}
+
+export default ProductCard
diff --git a/src/components/ServiceCard.module.css b/src/components/ServiceCard.module.css
new file mode 100644
index 0000000..1e10c6c
--- /dev/null
+++ b/src/components/ServiceCard.module.css
@@ -0,0 +1,15 @@
+.card {
+ background: var(--color-cream-alt);
+ border-radius: 4px;
+ padding: var(--space-md);
+ min-width: 160px;
+ text-align: center;
+ border: none;
+ cursor: pointer;
+ transition: transform 0.25s ease;
+ font-family: var(--font-serif);
+}
+
+.card:hover {
+ transform: translateY(-6px) rotateX(2deg);
+}
diff --git a/src/components/ServiceCard.test.tsx b/src/components/ServiceCard.test.tsx
new file mode 100644
index 0000000..9e2d8c4
--- /dev/null
+++ b/src/components/ServiceCard.test.tsx
@@ -0,0 +1,26 @@
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, it, expect, vi } from 'vitest'
+import ServiceCard from './ServiceCard'
+import type { Service } from '../data/services'
+
+const service: Service = {
+ id: 'knotless', name: 'Knotless Braids', durationMinutes: 240,
+ priceFrom: 150, description: 'Tension-free braids.', image: '/x.jpg',
+}
+
+describe('ServiceCard', () => {
+ it('renders service details', () => {
+ render()
+ expect(screen.getByText('Knotless Braids')).toBeInTheDocument()
+ expect(screen.getByText(/from \$150/i)).toBeInTheDocument()
+ expect(screen.getByText('4h')).toBeInTheDocument()
+ })
+
+ it('calls onSelect with the service id when clicked', async () => {
+ const onSelect = vi.fn()
+ render()
+ await userEvent.click(screen.getByRole('button'))
+ expect(onSelect).toHaveBeenCalledWith('knotless')
+ })
+})
diff --git a/src/components/ServiceCard.tsx b/src/components/ServiceCard.tsx
new file mode 100644
index 0000000..8ffeb90
--- /dev/null
+++ b/src/components/ServiceCard.tsx
@@ -0,0 +1,25 @@
+import type { Service } from '../data/services'
+import styles from './ServiceCard.module.css'
+
+interface ServiceCardProps {
+ service: Service
+ onSelect?: (serviceId: string) => void
+}
+
+function formatDuration(minutes: number): string {
+ const hours = Math.floor(minutes / 60)
+ const rest = minutes % 60
+ return rest === 0 ? `${hours}h` : `${hours}h ${rest}m`
+}
+
+function ServiceCard({ service, onSelect }: ServiceCardProps) {
+ return (
+
+ )
+}
+
+export default ServiceCard