diff --git a/apps/frontend/src/providers/i18n-provider.tsx b/apps/frontend/src/providers/i18n-provider.tsx index 690e29b..314b64d 100644 --- a/apps/frontend/src/providers/i18n-provider.tsx +++ b/apps/frontend/src/providers/i18n-provider.tsx @@ -512,6 +512,8 @@ const dictionaries = { "booking.spotsAvailable": "volná místa", "booking.submitting": "Rezervuji...", "booking.submit": "Rezervovat", + "booking.joinWaitlist": "Na čekací listinu", + "booking.full": "Obsazeno", "booking.business": "Podnik", "booking.sidebar.body": "Rezervace proběhne online. Potvrzení a připomenutí přijdou e-mailem.", "booking.customer.title": "Kontaktní údaje", @@ -1088,6 +1090,8 @@ const dictionaries = { "booking.spotsAvailable": "spots available", "booking.submitting": "Booking...", "booking.submit": "Book now", + "booking.joinWaitlist": "Join waitlist", + "booking.full": "Full", "booking.business": "Business", "booking.sidebar.body": "Book online. Confirmation and reminders are sent by email.", "booking.customer.title": "Contact details", diff --git a/apps/frontend/src/routes/public-booking-route.tsx b/apps/frontend/src/routes/public-booking-route.tsx index ac09a96..89bbd97 100644 --- a/apps/frontend/src/routes/public-booking-route.tsx +++ b/apps/frontend/src/routes/public-booking-route.tsx @@ -69,6 +69,18 @@ export function PublicBookingRoute() { const services = createMemo(() => catalog.latest?.data?.services ?? []); const staff = createMemo(() => catalog.latest?.data?.staff ?? []); const classSlots = createMemo(() => (availability.latest?.data?.slots ?? []).filter((slot) => slot.mode === "class")); + const classDays = createMemo(() => { + const groups = new Map(); + for (const slot of classSlots()) { + const d = new Date(slot.startsAt); + const key = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`; + const day = groups.get(key); + if (day) day.push(slot); else groups.set(key, [slot]); + } + return [...groups.entries()] + .sort(([, a], [, b]) => a[0].startsAt.localeCompare(b[0].startsAt)) + .map(([key, slots]) => ({ key, slots, date: new Date(slots[0].startsAt) })); + }); const daySlots = createMemo(() => dayAvailability.latest?.data?.slots ?? []); const hasCatalog = createMemo(() => services().length > 0); @@ -505,55 +517,51 @@ export function PublicBookingRoute() { 0}>

{i18n.t("booking.classes")}

- - {(slot) => ( - -
-
-
- - {slot.mode} - - - {i18n.t("booking.only")} {slot.remainingCapacity} {i18n.t("booking.left")} - -
-

{slot.label ?? "Bookable slot"}

-
- - - - - {new Date(slot.startsAt).toLocaleDateString(i18n.locale(), { weekday: 'short', month: 'short', day: 'numeric' })} - - - - - - {new Date(slot.startsAt).toLocaleTimeString(i18n.locale(), { hour: '2-digit', minute: '2-digit' })} - {new Date(slot.endsAt).toLocaleTimeString(i18n.locale(), { hour: '2-digit', minute: '2-digit' })} - -
- -

- - - - {slot.remainingCapacity} {i18n.t("booking.spotsAvailable")} -

-
+
+ + {(day) => ( +
+

+ {day.date.toLocaleDateString(i18n.locale(), { weekday: 'long', day: 'numeric', month: 'long' })} +

+
+ + {(slot) => ( +
+
+ + {new Date(slot.startsAt).toLocaleTimeString(i18n.locale(), { hour: '2-digit', minute: '2-digit' })} + {" – "} + {new Date(slot.endsAt).toLocaleTimeString(i18n.locale(), { hour: '2-digit', minute: '2-digit' })} + +
+

{slot.label ?? "Bookable slot"}

+ +

+ {slot.remainingCapacity === 0 + ? i18n.t("booking.full") + : `${slot.remainingCapacity} ${i18n.t("booking.spotsAvailable")}`} +

+
+
+
+ +
+ )} +
-
- - )} -
+ )} + +
diff --git a/project.md b/project.md index 66c2577..29fb89a 100644 --- a/project.md +++ b/project.md @@ -116,6 +116,10 @@ - `PUT /v1/catalog/class-templates/{id}` end-to-end: `UpdateClassTemplate` in both repositories, catalog service method sharing `validateClassTemplateInput` with create, route, OpenAPI path, regenerated client - classes tab template rows gained a pencil edit action that loads the template into the form; the submit button switches to save-changes with a cancel affordance - remaining: live staging validation +- `[done/partial]` Milestone 18: Public class list polish + - class sessions on the public booking page are now grouped by day (localized weekday + date headers) with compact timetable-style rows instead of one tall card per session — consistent with the stepped appointment picker above + - capacity states read honestly: spots-left in success/warning tones, `Obsazeno`/`Full` when empty, and the action switches to `Na čekací listinu`/`Join waitlist` (previously a full session still said "Book now" while silently producing a waitlisted booking) + - remaining: live staging validation ## Overview Bookra is a Czech-first booking SaaS for small local service businesses. It targets salons, massage therapists, small clinics, repair shops, and small studios/clubs that need a simple public booking flow, internal schedule management, reminders, and basic business reporting without becoming a full ERP.