2025-10-31 23:11:33 +00:00

Crussell

Nail salon booking platform — Go 1.25 backend + SvelteKit 5 frontend + Docker. Built for a UK sole-trader nail artist.

Features

  • Three booking flows: self-service (customer), walk-in (admin), call-in (admin)
  • Slot reservations: temporary holds prevent double-booking (4 TTL types)
  • Guest accounts: disposable accounts for one-off bookings, GDPR-compliant anonymization
  • Service eligibility: age requirements + patch test validation
  • Deposit system: 3-strike rule for late cancellations, 24-hour threshold
  • Square payments: in-person Terminal + online Web Payments SDK (dev mock + prod stub, build-tagged)
  • Payment methods: Card (Square Terminal/online), cash, gift card — with saved cards, refunds, and webhook handling
  • Payment types: deposit, full, partial, balance, tip — admin can override service prices during payment
  • Scheduling: default hours, holiday overrides, time blockers
  • Loyalty & discounts: stamp-based loyalty, time-based and milestone campaigns
  • Portfolio gallery: S3/R2 storage with tag/category filtering
  • CardDAV sync: profile photos synced to SabreDAV contacts
  • Admin notifications: priority-sorted queue with bell icon, /notifications page, acknowledge flow
  • User notification preferences: per-channel toggles (email, SMS, browser) in account settings
  • Enriched edit requests: side-by-side original vs proposed booking snapshots (time, services, prices, durations, user details) for admin review
  • Admin reschedule modal: full reschedule UI on Today page with real-time available slot lookup, conflict detection, and one-click confirmation
  • Time blockers UI: admin can create/manage one-off and recurring time blockers directly from the Admin dashboard, with overlap detection against existing bookings
  • Today page enhancements: interactive daily calendar grid with visual time blockers, today stats summary, responsive layout improvements
  • Auto-select availability: all booking flows (self-service, admin create, edit request) automatically select the first available date when data loads
  • Shared time slot utilities: extracted common time slot generation, lunch protection, and formatting logic into a reusable module
  • Shared formatting utilities: formatDuration, formatDateTime, formatDate, formatTime, calculateAge in lib/utils/format.ts
  • Referral code registration: new users can enter a 12-character referral code during registration; relationship recorded in user_referrals table
  • Admin schedule page: Google Calendar-style week view at /admin/schedule with drag-scroll, booking details modal, and working hours overlays
  • BookingFlow welcome step: unauthenticated users see a welcome card (Step 0) encouraging login before guest checkout, with clear messaging about lost loyalty/discount benefits
  • Admin login redirect: admins are redirected to /today after login instead of the home page
  • Patch test duration on services: patch_test_duration_hours exposed on Service type; creating a service with duration > 0 auto-creates a patch test record
  • created_by_name on bookings: admin booking details now show who created the booking (admin name)
  • Exceptional scheduling fix: exceptional application queries expand start to Monday of the week, fixing single-day queries missing Monday-based week_start records
  • Portfolio image upload limits: 20MB file size limit with visual feedback (red borders, error text), upload button disabled for oversized files
  • Multi-format portfolio images: Backend stores AVIF, WebP, JPEG, and optional JXL variants for both full-size and thumbnail images. Frontend ImageVariant component renders <picture> elements with format-aware fallback. WASM-based client-side encoders (@jsquash/avif, @jsquash/webp, @jsquash/jpeg, @discourse/jxl) run in Web Workers for parallel encoding. Database schema extended with full_avif_url, full_webp_url, full_jpg_url, full_jxl_url, thumb_avif_url, thumb_webp_url, thumb_jpg_url columns. Image validation supports AVIF and JXL magic bytes. Legacy single-URL images remain backward-compatible.
  • MapLibre GL map components: Reusable Svelte map components (Map, MapMarker, MapControls, MapPopup, MapRoute, MapClusterLayer, MapArc) wrapping MapLibre GL JS. Features: light/dark theme auto-detection, controlled/uncontrolled viewport, zoom/compass/locate/fullscreen controls, draggable markers, popup/tooltip/label children. Used on contact page for salon location display.
  • NavBar accessibility improvements: Hamburger menu animated with 4-line to X transform, keyboard-accessible backdrop overlay (Enter/Space to close), ARIA labels, fly transitions on notification badge.
  • Loyalty stamp redesign: Fuchsia-themed stamp card with procedurally-generated flower-petal SVG stamps (unique shape per slot via sine-wave perturbation), star cutout mask, hover scale animation, gradient background. Admin users see simplified account view (no History/Referral/Danger Zone tabs).
  • Admin role restrictions: Admin accounts hide History tab, Referral tab, Danger Zone (delete account), and loyalty stamp card on the account page.
  • Svelte 5 SvelteDate: Replaced new Date() with new SvelteDate() in reactive contexts (admin schedule, account page booking filter) for proper Svelte 5 reactivity.
  • $derived for computed values: totalPages in notifications page now uses $derived instead of runtime calculation.
  • Svelte 5 each block keys: Added proper keys to {#each} blocks across multiple components (NavBar, PortfolioCarousel, TodayCalendar, BookingConfirmed, Demo, PayTip, Tip, Account, Schedule) to fix reordering bugs and improve render performance.
  • $app/paths resolve: All internal navigation hrefs now use resolve() from $app/paths for proper SPA routing.
  • Tailwind fuchsia ring safelist: Added ring-fuchsia-* pattern to Tailwind safelist for loyalty stamp theming.
  • Vite WASM support: vite.config.ts configured to include .wasm files as assets and exclude JSquash/Discourse encoder packages from dependency optimization.
  • NavBar enhancements: admin links reordered (Today/Schedule promoted), unread notification badge on mobile burger icon, backdrop overlay + slide transition for mobile menu
  • TodayCalendar improvements: fetches working/available hours for full week range instead of single day, closing time indicator on timeline, skips lunch suggestion for short days (5h or less)
  • Lunch protection refinement: shouldApplyLunchProtection() skips lunch protection for days with 5 or fewer working hours
  • formatDateISO utility: new formatDateISO(date) function in lib/utils/format.ts returning YYYY-MM-DD format for API calls, later extracted to shared utilities
  • JWT revocation with JTI: every JWT carries a unique jti claim (UUID v4), in-memory revoked JTI tracking with 5-minute cleanup ticker, POST /api/logout endpoint revokes current token, refresh handler revokes old JTI before issuing replacement
  • Portfolio image deletion fix: extractKey correctly extracts full S3 key path from URLs instead of just the filename, preventing orphaned files in storage
  • Notes validation: all Notes *string fields across booking structs validated with max=1000000 tag (13 fields across 4 files)
  • CharCounter component: reusable grapheme counter using Intl.Segmenter, shows counter only above 750K graphemes, color-coded (green <800K, yellow 800K-950K, red >950K), integrated into 6 booking/admin components
  • loginInProgress rate limiting: switched from map[string]bool to map[string]time.Time with 30-second staleness check, 20-entry cap (returns 429 when full), ticker goroutine cleans up stuck entries
  • Profile picture upload limit: 15MB client-side check before crop dialog in account page
  • Portfolio image upload backend limit: separate portfolioBodyLimit (20MB) applied to /images route, distinct from uploadBodyLimit (15MB) for profile pictures
  • GDPR compliance system: Full Article 15 Subject Access Request via /gdpr — async Go endpoint (GET /api/user/gdpr-export) with 12h in-memory cache and background generation, 16-section SQL export (export_all_user_data()) covering profile, bookings (with override pricing), payments, refunds, saved cards, social logins, loyalty redemptions, booking discounts, edit requests, affiliate payouts, verification codes, forgiven no-shows, patch tests, referrals, notification preferences. Frontend: skeleton loading, 2s polling, styled report cards/tables, PDF export (print CSS hides navbar + verification banner), raw JSON download. Conditional rendering for empty sections, VAT breakdowns (hidden until non-zero), deposits required (hidden when 0). Account deletion (DELETE /api/user/account) extended with external system scrubbing — S3 profile picture deletion, Square saved card deletion — before SQL-level anonymization. anonymize_user() SQL function extended with child table PII scrubbing (social logins, saved cards soft-delete with PCI clearance, verification code expiry, time blocker reservation scrubbing, edit request notes nulling, notification preference deletion). AnonymizeStaleGuestAccounts() extended with additional field scrubbing (profile_pic_url, referral_code, notes, data_retention_consent). 25 tests covering all backend additions.
  • Financial data retention & aggregation: Granular payment/refund records retained for MAX(created_at + 7 years, user_anonymized_at + 1 year) (HMRC + Limitation Act compliance). Expired records are aggregated into financial_aggregates (monthly totals by payment method/type, no PII) and deleted. Triggered lazily on GET /api/availability alongside existing cleanup functions. Idempotent — safe to run repeatedly. 8 tests covering retention edge cases, aggregation correctness, refund handling, and idempotency.

Project Structure

Crussell/
├─ backend/            # Go 1.25 + chi router API
│  ├─ handlers/        # API handlers (auth, bookings, payments, scheduling, etc.)
│  │  ├─ payments/     # Square payment handlers (terminal, online, refunds, tips)
│  │  └─ webhooks/     # Square webhook handler
│  ├─ internal/        # Internal packages
│  │  └─ square/       # Square client (dev mock + prod stub, build-tagged)
│  └─ testutils/       # Test helpers (fixtures, testdb, JWT)
├─ frontend/           # SvelteKit 5 SPA (static build)
│  └─ src/lib/components/
│     ├─ payments/     # PaymentModal (admin), UserPaymentModal (user)
│     ├─ booking/      # BookingFlow (5-step wizard), DatePicker, TimeSlotPicker, TimeSlotList, SelectedTimeSummary
│     ├─ admin/        # BookingCreateModal, RescheduleModal, TimeBlockers, WeeklySchedule, HolidayHours, etc.
│     ├─ today/        # TodayCalendar (interactive grid), CurrentAppointment, PendingApprovals, TodayStats
│     └─ account/      # EditRequestModal, UserBookingModal
├─ frontend/src/lib/utils/  # Shared utilities (timeSlots.ts, format.ts)
├─ sabredav/           # PHP + Composer for DAV
├─ nginx/              # Nginx reverse-proxy for HTTP & HTTPS
├─ init-scripts/       # PostgreSQL init SQL
├─ compose.yml         # Docker-Compose definition
├─ local-dev-2.sh      # Dev helper using tmux (seeds DB with test data)
└─ README.md

Prerequisites

Tool Version
Docker & Docker Compose >= 20.10
Go >= 1.22
Node >= 18 (npm)
tmux >= 3.0

Getting Started

Docker Compose

cp .env.example .env
# Edit .env — set POSTGRES_*, JWT_SECRET_KEY
docker compose up --build -d
Service URL
Frontend http://localhost
API http://localhost/api
SabreDAV http://localhost/dav

Local Development

chmod +x local-dev-2.sh
./local-dev-2.sh

Launches a tmux session (crussell-dev) with 4 panes: psql console, Go dev server, Svelte dev server, and Rustfs logs. Seeds the database with 20 users, 12 services, 43 bookings, guest accounts, time blockers, and exceptional hours.

Default login (all password: password):

  • Admin: admin@example.com
  • User: user@example.com

Building & Testing

# Backend
cd backend && go build -o bin/backend ./main.go

# Frontend
cd frontend && npm ci && npm run build

# Tests (584/587 passing, 3 skipped)
cd backend && go test -tags "test,dev" ./...

Full Documentation

For detailed user journeys, admin workflows, architecture, API reference, and database schema, see obsidian/Crussell/.

S
Description
Nail salon website with booking system and portfolio
Readme
48 MiB
Languages
Go 64.6%
JavaScript 16.5%
Svelte 14.8%
TypeScript 1.9%
PLpgSQL 1.2%
Other 0.9%