feat: add mock data for products, services, availability, and rules
This commit is contained in:
29
src/data/availability.test.ts
Normal file
29
src/data/availability.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getSlotsForDate, getNextAvailableDate } from './availability'
|
||||
|
||||
describe('availability data', () => {
|
||||
it('returns a list of time slots for a given date', () => {
|
||||
const slots = getSlotsForDate('2026-08-10')
|
||||
expect(slots.length).toBeGreaterThan(0)
|
||||
for (const s of slots) {
|
||||
expect(s.date).toBe('2026-08-10')
|
||||
expect(typeof s.time).toBe('string')
|
||||
expect(typeof s.available).toBe('boolean')
|
||||
}
|
||||
})
|
||||
|
||||
it('is deterministic for the same date', () => {
|
||||
expect(getSlotsForDate('2026-08-10')).toEqual(getSlotsForDate('2026-08-10'))
|
||||
})
|
||||
|
||||
it('finds the next available date within 14 days when none is available immediately', () => {
|
||||
// Sundays (day 0) are fully booked in the mock generator
|
||||
const next = getNextAvailableDate('2026-08-09') // a Sunday
|
||||
expect(next).not.toBeNull()
|
||||
expect(next).not.toBe('2026-08-09')
|
||||
})
|
||||
|
||||
it('returns null if no available date is found within the search window', () => {
|
||||
expect(getNextAvailableDate('1999-01-01', 0)).toBeNull()
|
||||
})
|
||||
})
|
||||
40
src/data/availability.ts
Normal file
40
src/data/availability.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface TimeSlot {
|
||||
date: string // ISO date, e.g. "2026-08-10"
|
||||
time: string // e.g. "10:00 AM"
|
||||
available: boolean
|
||||
}
|
||||
|
||||
const ALL_TIMES = ['9:00 AM', '11:00 AM', '1:00 PM', '3:00 PM', '5:00 PM']
|
||||
|
||||
function seedFromDate(date: string): number {
|
||||
let seed = 0
|
||||
for (let i = 0; i < date.length; i++) seed += date.charCodeAt(i)
|
||||
return seed
|
||||
}
|
||||
|
||||
export function getSlotsForDate(date: string): TimeSlot[] {
|
||||
const day = new Date(`${date}T00:00:00`).getDay()
|
||||
if (day === 0) {
|
||||
// Sundays: closed, no slots at all
|
||||
return []
|
||||
}
|
||||
const seed = seedFromDate(date)
|
||||
return ALL_TIMES.map((time, i) => ({
|
||||
date,
|
||||
time,
|
||||
available: (seed + i) % 3 !== 0,
|
||||
}))
|
||||
}
|
||||
|
||||
export function getNextAvailableDate(fromDate: string, searchWindowDays = 14): string | null {
|
||||
const start = new Date(`${fromDate}T00:00:00`)
|
||||
for (let i = 1; i <= searchWindowDays; i++) {
|
||||
const candidate = new Date(start)
|
||||
candidate.setDate(start.getDate() + i)
|
||||
const iso = candidate.toISOString().slice(0, 10)
|
||||
if (getSlotsForDate(iso).some((s) => s.available)) {
|
||||
return iso
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
24
src/data/products.test.ts
Normal file
24
src/data/products.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { products, getProductsByCategory } from './products'
|
||||
|
||||
describe('products data', () => {
|
||||
it('has at least 6 products with required fields', () => {
|
||||
expect(products.length).toBeGreaterThanOrEqual(6)
|
||||
for (const p of products) {
|
||||
expect(p.id).toBeTruthy()
|
||||
expect(p.name).toBeTruthy()
|
||||
expect(p.price).toBeGreaterThan(0)
|
||||
expect(typeof p.inStock).toBe('boolean')
|
||||
}
|
||||
})
|
||||
|
||||
it('filters by category', () => {
|
||||
const hairCare = getProductsByCategory('hair-care')
|
||||
expect(hairCare.length).toBeGreaterThan(0)
|
||||
expect(hairCare.every((p) => p.category === 'hair-care')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns all products for "all"', () => {
|
||||
expect(getProductsByCategory('all')).toHaveLength(products.length)
|
||||
})
|
||||
})
|
||||
27
src/data/products.ts
Normal file
27
src/data/products.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export interface Product {
|
||||
id: string
|
||||
name: string
|
||||
category: 'hair-care' | 'extensions' | 'tools' | 'accessories'
|
||||
price: number
|
||||
image: string
|
||||
description: string
|
||||
inStock: boolean
|
||||
}
|
||||
|
||||
export const products: Product[] = [
|
||||
{ id: 'edge-control', name: 'Silk Edge Control', category: 'hair-care', price: 12, image: '/products/edge-control.jpg', description: 'Long-lasting hold with a natural shine, no flaking.', inStock: true },
|
||||
{ id: 'braid-oil', name: 'Nourishing Braid Oil', category: 'hair-care', price: 16, image: '/products/braid-oil.jpg', description: 'Lightweight scalp oil that soothes and adds shine between washes.', inStock: true },
|
||||
{ id: 'leave-in', name: 'Detangling Leave-In', category: 'hair-care', price: 14, image: '/products/leave-in.jpg', description: 'Slip-rich leave-in conditioner for gentle detangling.', inStock: true },
|
||||
{ id: 'kanekalon-black', name: 'Kanekalon Braiding Hair — Jet Black', category: 'extensions', price: 8, image: '/products/kanekalon-black.jpg', description: 'Premium synthetic braiding hair, soft texture, low shedding.', inStock: true },
|
||||
{ id: 'kanekalon-brown', name: 'Kanekalon Braiding Hair — Chestnut', category: 'extensions', price: 8, image: '/products/kanekalon-brown.jpg', description: 'Warm brown tone, blends seamlessly for natural styles.', inStock: false },
|
||||
{ id: 'locs-extension', name: 'Faux Locs Extensions', category: 'extensions', price: 22, image: '/products/locs-extension.jpg', description: 'Pre-looped faux locs for a natural, lightweight install.', inStock: true },
|
||||
{ id: 'rat-tail-comb', name: 'Steel Rat-Tail Comb', category: 'tools', price: 6, image: '/products/rat-tail-comb.jpg', description: 'Precision parting comb with a fine metal tail.', inStock: true },
|
||||
{ id: 'edge-brush', name: 'Mini Edge Brush', category: 'tools', price: 5, image: '/products/edge-brush.jpg', description: 'Dual-sided brush for laying edges and smoothing baby hairs.', inStock: true },
|
||||
{ id: 'gold-cuffs', name: 'Gold Hair Cuffs (Set of 6)', category: 'accessories', price: 10, image: '/products/gold-cuffs.jpg', description: 'Adjustable cuffs to accessorize braids and locs.', inStock: true },
|
||||
{ id: 'satin-bonnet', name: 'Satin Sleep Bonnet', category: 'accessories', price: 9, image: '/products/satin-bonnet.jpg', description: 'Protects braids and locs overnight, reduces frizz.', inStock: true },
|
||||
]
|
||||
|
||||
export function getProductsByCategory(category: Product['category'] | 'all'): Product[] {
|
||||
if (category === 'all') return products
|
||||
return products.filter((p) => p.category === category)
|
||||
}
|
||||
13
src/data/rules.ts
Normal file
13
src/data/rules.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export interface RuleSection {
|
||||
id: string
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export const rules: RuleSection[] = [
|
||||
{ id: 'deposit', title: 'Deposit', body: 'A non-refundable deposit of $30 is required to secure your appointment and will be deducted from your final total. Appointments are not confirmed until the deposit is received.' },
|
||||
{ id: 'cancellation', title: 'Cancellation & Lateness', body: 'Please give at least 48 hours notice to reschedule. Arrivals more than 15 minutes late may need to be rescheduled and will forfeit their deposit, as it affects every client booked after you.' },
|
||||
{ id: 'guests', title: 'Guests & Kids', body: 'To keep the space calm and on schedule, please come to your appointment alone unless the guest is also being serviced. Children who are not being serviced should not accompany you.' },
|
||||
{ id: 'hair-prep', title: 'Hair Prep', body: 'Arrive with hair freshly washed, blow-dried or stretched, and fully detangled unless a wash service has been booked. Extra prep time may be added to your total if hair arrives unprepped.' },
|
||||
{ id: 'health-safety', title: 'Health & Safety', body: 'Please reschedule if you are unwell. Let Mira know in advance of any scalp sensitivities, allergies, or conditions that may affect your service.' },
|
||||
]
|
||||
15
src/data/services.ts
Normal file
15
src/data/services.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface Service {
|
||||
id: string
|
||||
name: string
|
||||
durationMinutes: number
|
||||
priceFrom: number
|
||||
description: string
|
||||
image: string
|
||||
}
|
||||
|
||||
export const services: Service[] = [
|
||||
{ id: 'knotless', name: 'Knotless Braids', durationMinutes: 240, priceFrom: 150, description: 'Tension-free braids with a natural, seamless root.', image: '/styles/knotless.jpg' },
|
||||
{ id: 'boho', name: 'Boho Braids', durationMinutes: 270, priceFrom: 180, description: 'Knotless braids with curly pieces woven in for a soft, romantic look.', image: '/styles/boho.jpg' },
|
||||
{ id: 'locs', name: 'Faux Locs', durationMinutes: 300, priceFrom: 200, description: 'Lightweight, natural-looking locs installed with care.', image: '/styles/locs.jpg' },
|
||||
{ id: 'cornrows', name: 'Cornrows', durationMinutes: 120, priceFrom: 90, description: 'Classic sleek cornrows, straight-back or with a custom design.', image: '/styles/cornrows.jpg' },
|
||||
]
|
||||
Reference in New Issue
Block a user