Lustre Salon — Hairstylist Website with Scheduling

11/4/2025

Lustre Salon — Hairstylist Website with Scheduling

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.

Homepage

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)

Schedule page

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' })

Admin — Appointments

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)

Links