Skip to content

fix(explore): expert booking calendar — viewport-fit dialog, nav z-stack, per-day availability dots - #1229

Draft
teetangh wants to merge 11 commits into
devfrom
fix/explore-booking-responsive
Draft

fix(explore): expert booking calendar — viewport-fit dialog, nav z-stack, per-day availability dots#1229
teetangh wants to merge 11 commits into
devfrom
fix/explore-booking-responsive

Conversation

@teetangh

Copy link
Copy Markdown
Contributor

Problem

On the Explore Experts details page (/explore/experts/[consultantId]):

  1. Booking dialog buried under the navbar — the Book Now dialog used the shadcn default z-50 while the fixed navbar is z-[1000] and the announcement bar z-[1001], so the nav painted above the modal.
  2. Dialog/calendar cropped at the bottom — fixed-size calendar cells + unbounded slot list made the dialog grow past the viewport on short screens with no usable scroll affordance.
  3. Pricing sidebar stuck under the navsticky top-24 (96px) sat under the real chrome stack (80px nav + ~40px announcement bar ≈ 120px).
  4. No way to see which dates have slots — every calendar day rendered identically; availability was fetched only after clicking a day.

Fix

  • Dialog z-order: inline zIndex: 1002 on the Book Now DialogContent (merge-order-proof vs the z-50 base class). Matches the z-[1002] pattern already used by TrialBookingModal/SubscriptionPricingToggle.
  • Viewport-fit dialog: calendar cell size driven by a --cell: clamp(26px, 5.2dvh, 42px) CSS var so 6 rows + legend always fit; compact header/footer chrome; slot list capped (30dvh mobile / 350px desktop) with internal scroll; description + schedule note hidden on tight screens. The whole dialog now shrinks with the screen instead of overflowing it.
  • Per-day availability dots: month-wide rollup from the existing /api/slots/availability-with-allocation range endpoint (today → month end, lazy re-fetch on month flip, stale-response guard) rendered as emerald (open) / amber (partially booked) / rose (fully booked) / zinc (no slots) dots under each date, with a legend. Past and zero-slot days are disabled; fully-booked days stay clickable to show the rose slot list; prev-month arrow disabled on the current month. No backend changes.
  • Sidebar sticky offset: xl:sticky xl:top-[calc(var(--header-height,5rem)+1rem)] — header-aware and only sticky where the two-column layout exists.

Verification

  • tsc --noEmit + ESLint clean on touched files.
  • Visual checks on the Netlify preview at 375 / 768 / 1280 / 1536 px widths (screenshots in comments).

…ay availability dots

- Book Now dialog: inline zIndex 1002 so the fixed navbar (z-[1000]) and
  announcement bar (z-[1001]) can no longer paint above it; compact
  header/footer chrome; slot list capped (30dvh mobile / 350px desktop)
  with internal scroll so the dialog never grows past the viewport.
- Calendar: cell size driven by a --cell clamp(26px, 5.2dvh, 42px) var so
  all rows + legend fit on screen at any height; weekday header and grid
  center cells; description and schedule note hidden on tight screens.
- Calendar now shows which dates have slots: month-wide rollup from the
  existing availability range endpoint (today -> month end, lazy per
  month flip) rendered as emerald/amber/rose/zinc dots with a legend;
  past and zero-slot days disabled; prev-month arrow disabled on the
  current month.
- Sidebar pricing card sticks below the real header stack via
  xl:top-[calc(var(--header-height)+1rem)] instead of top-24.
@netlify

netlify Bot commented Aug 23, 2026

Copy link
Copy Markdown

Deploy Preview for familiarise ready!

Name Link
🔨 Latest commit 3dd95d1
🔍 Latest deploy log https://app.netlify.com/projects/familiarise/deploys/6a8b2fd1400e89000803d8d5
😎 Deploy Preview https://deploy-preview-1229--familiarise.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 38 (🔴 down 19 from production)
Accessibility: 90 (no change from production)
Best Practices: 83 (no change from production)
SEO: 82 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2530d09f-abb6-4a3c-966d-637448bfc109

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Too big: expected string to have <=250 characters at "tone_instructions"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Comment @coderabbitai help to get the list of available commands.

…olling

Dialog is now a fixed-height (85dvh) flex column with container-type:
size — header/footer stay fixed, the two panes flex, and only the slot
list scrolls internally. Calendar cells size off the dialog's real
height via cqh container units (--cell clamp), so the grid can never
push the dialog past the screen. Dropped the duplicate calendar legend;
the slot list legend doubles as the dot key.
8cqh-of-dialog cells ignored the chrome share of the pane, so the last
week row spilled outside the card on shorter screens. The card is now
the size container at md+ and cells derive from its real height
(clamp(22px, 16.6cqh - 30px, 48px)); card clips overflow; mobile stack
scrolls the body and caps the slot list at 40dvh.
@teetangh teetangh self-assigned this Aug 23, 2026
…h variant)

At short viewport heights the side-by-side dialog left the calendar
card ~217px — six week rows can never fit. New 'mdh' custom variant
(min-width 768px + min-height 700px) gates the two-column, no-scroll
layout; shorter screens use the stacked layout with body scroll.
Chrome slimmed (header/desc/footer/card padding) and cell formula
retuned to the card's real chrome.
… mdh layout

flex-basis:0 children of auto-height parents collapsed the stacked
fallback to ~97px, clipping the calendar. flex-1 now applies only at
mdh where the dialog has a fixed height; stacked mode sizes by content.
… full-screen on phones

Per the SaaS scheduling pattern (Cal.com/Calendly): the booking flow is
the task, so the sheet takes 92dvh and up to 1200px wide on desktop and
goes full-bleed below sm. The mdh two-column threshold drops to 620px
height — at the previously-cramped 662px viewports the side-by-side
no-scroll layout now fits with ~36px cells.
…size by content

min-h-0 let flex-shrink crush the card below its content when the body
grid's auto rows resolved small on mobile; the calendar clipped at ~4
rows. Panes/card/list now keep natural height outside mdh.
@sonarqubecloud

Copy link
Copy Markdown

teetangh added a commit that referenced this pull request Sep 5, 2026
…y everywhere, and day segments are half-open (#1343, #1342, #1326, #1348, #1415, #1416) (#1512)

## Summary

**The one rule this PR establishes:** `SlotOfAvailabilityWeekly.startDay` is the day the **consultant** published, in their own local calendar, and the UTC weekday is always derived from it through the row's own frozen `utcOffsetMinutes`. Four surfaces had each answered that question for themselves, and the disagreements are the six issues below.

| Issue | What was wrong | Fix |
| --- | --- | --- |
| **#1343** | The settings save path shifted the day forward or back to the UTC day the converted instant landed on, while onboarding stored the local day. An Asia/Kolkata row starting before 05:30 local walked back one weekday on every save — Monday 01:00 saved as Sunday, reloaded into Sunday's form row, saved again as Saturday. | `shiftDayOfWeek` and the `startDayOffset` block are gone. `utils/schedule/formatting.ts` now has one builder, `weeklySlotForSave`, that both save paths use; `formatWeeklySlot` is a thin adapter over it and keeps the #1125 all-or-nothing throw contract. |
| **#1342** | The calendar grid bucketed rows on the **viewer's** weekday and did not even select `utcOffsetMinutes`, so an overseas customer was shown an IST pre-05:30 row a day away from the day checkout would accept. | New `utils/schedule/weekly-projection.ts` owns `utcStartDayIndex`, `weeklyRowDurationMinutes` and `weeklyRowOccurrencesInRange` — the single generator the grid, `isMinuteWithinWeeklySlot`, `getNextOccurrenceWeekly` and `matchWeeklySlotToDay` all share. The grid endpoint carries the stored columns through instead of flattening them onto a 1970 reference date, and `processWeeklySlots` loses its `timezone` parameter because display zoning belongs downstream in `splitSlotsByDay`. |
| **#1326**, **#1348 item 1** | Onboarding and the per-row PATCH defaulted a consultant with no profile timezone to UTC 0 rather than to the launch offset, and nothing checked a caller who supplied an offset of their own. | New `lib/scheduling/weeklyUtcOffset.ts` is the only answer: derived from `User.timezone`, 330 when the profile carries no usable zone (ADR 17), `WeeklyOffsetConflictError` → 400 `UTC_OFFSET_CONFLICT` when a caller contradicts it, and one Sentry warning per write (never per row) when the consultant publishes from outside Asia/Kolkata. `PIN_TO_LAUNCH_OFFSET` is the one-line switch to hard-pin every row to IST. Onboarding also stamped the offset *after* `mergeAdjacentWeeklyRows`, so that fold's cross-offset guard was comparing `undefined` with `undefined`; it is stamped before the fold now. |
| **#1415** | `splitSlotsByDay` cut day segments at `endOfDay` (23:59:59.999). A 23:30–23:59:59.999 remainder is not a thirty-minute atom, so a block published up to local midnight silently lost its last bookable slot on every surface. | Segments are half-open: a segment ends at the next local day's midnight. |
| **#1416** | The expert page's display merge carried a 60-second tolerance where booking requires exact adjacency, so the availability card advertised a window whose seam no row publishes and checkout's per-atom union coverage then refused the booking. | `mergeConsecutiveSlotsForDisplay` requires exact adjacency, and its docblock now states the truth: the only difference from `mergeConsecutiveSlots` is which atoms are eligible (same-status versus available-only). |

Per **user decision 6 / #872**, all four weekly write paths additionally **dual-write** the five DST columns (`timezone`, `localStartMinutes`, `localEndMinutes`, `localStartDay`, `localEndDay`) from the same resolver, computed after the merge so they describe the row actually stored. Nothing reads them. `coalesceConsultantWeeklyRows` deletes and recreates rows, so it recomputes them for the merged row rather than losing them on the next coalesce. The `schema.prisma` doc-comment changes from "unwritten" to "written from 2026-09-05, read by nothing until the reader flip" — comment only, **no schema change, no `db push` needed**.

**No data repair.** The rows the old settings path wrote are pre-MVP mock data; the no-backfill rule applies and the pre-MVP reset wipes them.

### Rebands worth recording

- **#1415's headline is false.** `availability-window-merge.test.ts:240-245` already proves a two-hour block yields one two-hour window across rows. The real residual was the `endOfDay` bound described above, and that is what this PR fixes.
- **#1348 is a three-item bucket.** Item 1 (the zero-offset default) is fixed here, together with the regression test across the write paths that the issue asks for. Item 2 is explicitly scope-hygiene only and defers to #1206, which is now built. Item 3 asks for three tests, of which only the `mergeAdjacentWeeklyRows` cross-offset bullet is addressed (by the ordering fix above); see follow-ups.

## Files touched

**New:** `utils/schedule/weekly-projection.ts`, `lib/scheduling/weeklyUtcOffset.ts`, `__tests__/booking-algorithm/weekly-day-semantics.test.ts`.

**Changed:** `utils/schedule/formatting.ts`, `utils/timeSlotsProcessing.ts`, `utils/slotAllocation/slotTimeUtils.ts`, `utils/slotAllocation/SlotAllocationService.ts`, `utils/slotAllocation/mergeAdjacentWeeklyRows.ts`, `utils/onboarding-server.ts`, `app/api/slots/availability-with-allocation/[consultantId]/route.ts`, `app/api/slots/availability/weekly/route.ts`, `app/api/slots/availability/weekly/[id]/route.ts`, `app/api/user/consultants/[id]/route.ts`, `app/explore/experts/[consultantId]/utils/mergeSlots.ts`, `prisma/schema.prisma` (comment only), `__tests__/schedule/format-slots-for-api-throws.test.ts`.

**Docs:** `docs/booking/00-architecture-decisions.md` (ADR B4 rewritten, register row and status updated), `docs/booking/02-event-types-and-validation.md`, `docs/booking/03-slot-math-and-calculations.md` (new section "Projecting a weekly row onto real dates"), `docs/booking/19-dst-and-timezone-posture.md` (rules 1–4 corrected — the local columns are written now and the drift warning exists), `.claude/skills/booking/references/availability.md` §1/§2/§4. `docs/booking/05-troubleshooting-and-changelog.md` is deliberately untouched; PR-D writes one consolidated section.

## Verification

| Check | Command | Result |
| --- | --- | --- |
| Types | `rm tsconfig.tsbuildinfo && NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit` | Clean, zero diagnostics (run twice: before and after the Prettier pass). `npx prisma generate` deliberately not run — a sibling worktree owns the shared client. |
| Lint | `npx eslint` over all 15 changed/new TypeScript files | **0 errors, 2 warnings**, both pre-existing `no-explicit-any` at `SlotAllocationService.ts:3445` and `:3498` (verified identical on `origin/dev` at lines 3446/3499, only shifted by edits above them). Nothing new introduced. |
| Format | `npx prettier --check` over all 21 changed files | All matched files use Prettier code style. |
| Tests | `npx jest __tests__/booking-algorithm __tests__/booking __tests__/schedule` | **84 suites passed, 1294 tests passed**, exit 0. |
| A12.1 grid ⇔ validator walk | Throwaway script, not committed: every 30-minute atom of the week 2026-09-06 → 2026-09-13 for the IST Monday 01:00–05:00 row (`{MONDAY, 1170–1410, offset 330}`), asserting `weeklyRowOccurrencesInRange` coverage ⇔ `isMinuteWithinWeeklySlot`. | `ATOMS=336 GRID=8 VALIDATOR=8 MISMATCHES=0` — the eight atoms are the four hours the row publishes, and the grid and the validator agree on every one of the 336. |

The committed pin, `__tests__/booking-algorithm/weekly-day-semantics.test.ts`, is table-driven, node-environment and Prisma-free, and covers all six required areas: the two save paths agreeing on `startDay` (including re-save idempotency), the offset resolver's table plus a source assertion that the four write paths call `resolveWeeklyUtcOffsetMinutes` and no longer call `getTimezoneOffsetMinutes` directly, grid-equals-validator for the IST pre-dawn row seen from both an `Asia/Kolkata` and an `America/New_York` viewer, the midnight block keeping its 23:30 window, display-merge-equals-booking-merge over gaps of 0/1,000/60,000 ms, and the dual-written local columns. `slotTimeUtils.test.ts`, `availability-window-merge.test.ts`, `availability-grid-conditional-get.test.ts`, `availability-window-scan.test.ts` and `slot-session-fix-pins.test.ts` were run and are green, and were not edited.

## Not done / follow-ups

- **Server-side `validateTimeIncrements`** is out of scope. It is a private helper on a separate rejection surface, so the server write paths still do not run it and a minute-level seam remains creatable through the API even though neither merge function will now advertise one.
- **#1229** (draft, `fix/explore-booking-responsive`) overlaps this area textually only. Per the guardrail, `ConsultationPricingToggle.tsx`, `ExpertPricing.tsx`, `WeeklyAvailability.tsx` and `CustomAvailability.tsx` are untouched; the only explore file changed here is `utils/mergeSlots.ts`.
- **No data repair** for rows already written by the old settings path — pre-MVP mock data, no-backfill rule, the reset wipes them.
- **#1348 item 3** still wants two tests this PR does not add: preference-scoring tie-break determinism under equal scores, and confirmation that a manual allocation is not silently overwritten by a later auto pass. Neither is a behaviour change, and both are test-budget items that belong with the allocator work rather than here.
- **#872 reader flip** remains post-MVP. This PR only makes the dual-write correct and documents the reader-flip approach in `19-dst-and-timezone-posture.md`.

Closes #1343
Closes #1342
Closes #1326
Closes #1348
Closes #1415
Closes #1416
Part of #872
Part of #1433

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01EyngsXG829TRTBof4CSGT1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant