Lustre Salon — Hairstylist Website with Scheduling
11/4/2025

Overview
Lustre Salon is a modern hairstylist website that lets clients request appointments online and explore services and hours. The admin dashboard provides simple tools to manage bookings, services, and business settings.

Architecture
- Frontend: React + Vite + Tailwind; pages for Home, About, and Schedule
- Backend: Node.js + Express + MongoDB (Mongoose)
- Admin: Separate React + Vite app (appointments, settings)
- i18n: lightweight translation layer on frontend
[lustresalon] → web app + API (services, appointments, settings)
├─ src/pages/{HomePage,AboutPage,SchedulePage}
└─ server/src/routes/{appointments,services,settings,auth}
[lustresalonadmin] → admin SPA (Dashboard, Login, Settings)
Features
- Appointment requests with form validation and business‑hours guardrails
- Services listing from backend settings
- Admin filtering: upcoming vs archived, pagination, conflict detection
- Timezone‑aware scheduling (Europe/Skopje)

Advanced Implementation
Frontend schedule form with work‑day validation
// src/pages/SchedulePage.jsx (excerpt)
const minDate = useMemo(() => DateTime.now().setZone('Europe/Skopje').toFormat('yyyy-LL-dd'), [])
<input name="preferredDate" type="date" min={minDate} onChange={(e)=>{
const d = DateTime.fromISO(e.target.value, { zone: 'Europe/Skopje' })
const weekday = d.weekday // 1..7
const day = weekday % 7 // 0..6
if (!workDays.includes(day)) e.target.setCustomValidity(t('schedule.form.notWorkingDay'))
else e.target.setCustomValidity('')
}} />
Appointment creation with schema validation (Zod) and timezone normalization
// server/src/routes/appointments.js (excerpt)
const createSchema = z.object({ fullName: z.string().min(2), email: z.string().email(), phone: z.string().min(7), service: z.string().min(2), preferredDate: z.string().optional(), notes: z.string().max(1000).optional() })
router.post('/', async (req,res)=>{
const parsed = createSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error:'Invalid input', details: parsed.error.flatten() })
const pref = parsed.data.preferredDate ? DateTime.fromISO(parsed.data.preferredDate,{zone:'Europe/Skopje'}).startOf('day').toUTC().toJSDate() : undefined
const doc = await Appointment.create({ ...parsed.data, preferredDate: pref, notes: parsed.data.notes || '' })
res.status(201).json({ ok:true, appointment: doc })
})
Admin: conflict‑aware scheduling with 29‑minute buffer and status rules
// server/src/routes/appointments.js (excerpt)
// prevent rescheduling confirmed appts; block decline of confirmed; enforce 29‑min conflict window
if (update.status === 'confirmed' && !(update.scheduledAt || appt.scheduledAt)) return res.status(400).json({ error:'Set a time before confirming' })
if (appt.status === 'confirmed' && update.scheduledAt) return res.status(400).json({ error:'Cannot change time of a confirmed appointment' })
const windowStart = new Date(date.getTime() - 29*60*1000)
const windowEnd = new Date(date.getTime() + 29*60*1000)
const conflict = await Appointment.findOne({ _id:{ $ne: appt._id }, scheduledAt:{ $gte: windowStart, $lte: windowEnd }, status:{ $in:['pending','confirmed'] } })
if (conflict) return res.status(409).json({ error:'Conflicts within 29 minutes of another appointment' })

Booking Lifecycle
- Statuses: pending → confirmed / cancelled / declined
- Confirm requires a concrete scheduled time (guarded server‑side)
- Confirmed bookings cannot have their time changed and cannot be declined (cancellation allowed)
- Upcoming/Archived admin filters are computed from status and time windows
Services & Business Hours
- Work days and service catalog are fetched from the backend (
/api/settings,/api/services) and reflected in the schedule form - Frontend blocks non‑working days at input level; server normalizes preferred dates into UTC day starts
Admin Lists & Pagination
- Query parameters:
type=upcoming|archived,page,limit - Returns
items,page,totalPages,total; sorted appropriately per filter - 503 guard: endpoints return service unavailable if the DB isn’t ready
API Shapes (examples)
// POST /api/appointments
{
"fullName": "Alex Rivera",
"email": "alex@example.com",
"phone": "+1 555 111 2222",
"service": "Haircut",
"preferredDate": "2025-11-08",
"notes": "Layered cut"
}
// 201 response
{
"ok": true,
"appointment": { "_id": "...", "status": "pending", "preferredDate": "2025-11-08T00:00:00.000Z", "createdAt": "..." }
}
// PATCH /api/appointments/:id (admin)
{ "status": "confirmed", "scheduledAt": "2025-11-08T13:30:00.000Z" }
Authentication & Admin
- Admin routes require authentication (JWT middleware)
- Separate admin SPA with guarded routes (login, dashboard, settings)
Internationalization
- Frontend uses a lightweight i18n layer (
src/i18n/i18n.jsx) with keys for headings, field labels, validation, and messages
SEO & Branding
- Clean, minimal brand with warm neutrals; accent used sparingly on CTAs
- Prose typography for service descriptions; accessible form styles
- Meta tags and manifest shipped; sitemap/robots can be added as needed
What made it work
- Opinionated scheduling rules simplify admin decisions and avoid double‑booking
- Clear, elegant UI reduces friction for clients and administrators alike
- Separation of web and admin apps makes maintenance straightforward
Deliverables
- Marketing website (home, about, schedule)
- Appointment request flow with validation
- Admin dashboard for appointments, services, settings
- Multi-lingual ready (i18n)