feat(scheduling): dezky Scheduling — Calendly-style booking on Stalwart calendars
First-party booking system on top of Stalwart calendars (no third-party scheduling dependency). Hosts expose public booking pages; visitors pick a slot computed from the host's live Stalwart free/busy, and confirming writes the event to the host's calendar and sends a dezky-branded confirmation with an .ics. platform-api (services/platform-api/src/scheduling): - Schemas: Host, StalwartCredential (AES-256-GCM at rest), AvailabilitySchedule, EventType, Booking, SlotLock (unique (hostId,startUtc) + TTL). - StalwartCalendarModule: JMAP gateway (free/busy via Principal/getAvailability, event create/delete, scheduleAgent=client) + on-behalf app-password provisioning. CredentialCipher for at-rest encryption. - DST-correct slot engine (Luxon) with unit tests; two-layer double-booking guard (atomic SlotLock + live free/busy re-check). - Booking confirm/cancel/reschedule, branded email + .ics via JMAP submission, self-service manage tokens. /api/v1 public + tenant-gated admin routes, per-IP rate limiting. apps/booking: standalone public, whitelabel booking app (booking.dezky.eu) — path-based tenant resolution, per-tenant brand colour, booking + manage flows. apps/portal: admin scheduling page (hosts, event types, availability, bookings with edit/delete + admin cancel/reschedule) and proxy routes. infra: booking dev service in docker-compose; scheduling env vars.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
/* Booking app base styles + design tokens. The accent is whitelabel: each
|
||||
booking page sets --accent from the tenant's brandColor at runtime (see the
|
||||
booking page's :style binding); this file only provides the fallback. */
|
||||
:root {
|
||||
--accent: #1a1a1a;
|
||||
--accent-contrast: #ffffff;
|
||||
--bg: #f6f6f7;
|
||||
--surface: #ffffff;
|
||||
--text: #1a1a1a;
|
||||
--text-mute: #6b6b70;
|
||||
--border: #e7e7ea;
|
||||
--radius: 14px;
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.06);
|
||||
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
line-height: 1.5;
|
||||
}
|
||||
a { color: inherit; }
|
||||
button { font-family: inherit; }
|
||||
|
||||
/* Shared primitives used across pages */
|
||||
.bk-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
||||
height: 44px; padding: 0 20px; border-radius: 10px; border: 1px solid transparent;
|
||||
font-size: 15px; font-weight: 600; cursor: pointer; transition: filter .15s, opacity .15s;
|
||||
}
|
||||
.bk-btn--primary { background: var(--accent); color: var(--accent-contrast); }
|
||||
.bk-btn--primary:hover { filter: brightness(1.06); }
|
||||
.bk-btn--ghost { background: transparent; border-color: var(--border); color: var(--text); }
|
||||
.bk-btn--ghost:hover { background: #00000008; }
|
||||
.bk-btn:disabled { opacity: .5; cursor: not-allowed; }
|
||||
|
||||
.bk-field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.bk-label { font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-mute); }
|
||||
.bk-input {
|
||||
height: 44px; padding: 0 14px; border: 1px solid var(--border); border-radius: 10px;
|
||||
background: var(--surface); font-size: 15px; color: var(--text); width: 100%;
|
||||
}
|
||||
.bk-input:focus { outline: 2px solid var(--accent); outline-offset: 0; border-color: transparent; }
|
||||
textarea.bk-input { height: auto; padding: 12px 14px; resize: vertical; min-height: 84px; }
|
||||
Reference in New Issue
Block a user