Skip to content

chore(polish): cookie consent + Vercel Analytics + dead-deps + README + i18n CI gate#18

Open
abdout wants to merge 17 commits into
mainfrom
chore/polish-observability
Open

chore(polish): cookie consent + Vercel Analytics + dead-deps + README + i18n CI gate#18
abdout wants to merge 17 commits into
mainfrom
chore/polish-observability

Conversation

@abdout
Copy link
Copy Markdown
Contributor

@abdout abdout commented Apr 29, 2026

Summary

Phase D — final polish + observability before tagging v2.0. Built on top of #6 + #8 + #10 + #15 + #16.

  • Cookie consent banner: translated three-button (Accept all / Reject non-essential / Learn more). Persists choice in a cookieConsent cookie (1y). Dispatches a cookieconsent window event so analytics islands react immediately without reload.
  • Consent-gated Vercel Analytics + Speed Insights: only mount when the user has accepted "all". Subscribed to the banner's event so they light up right after the click.
  • Dead-deps prune: @react-pdf/renderer (no PDF route), @tiptap/react, @tiptap/starter-kit (no rich-text surface) — confirmed unused by grep -rln. Both @prisma/adapter-pg and @prisma/adapter-neon are kept (the wake-db script uses pg unconditionally).
  • README rewrite: 25 lines of marketing copy → 100+ lines of real ops doc — stack table, quick-start, env-var matrix (required + optional), script reference, code map, deploy notes, contributing workflow.
  • /api/health deepened: new lastBooking block exposes the age-in-hours of the latest Booking row. Informational; on-call can spot a silent booking-flow regression before users do.
  • photo-tour anti-pattern fix: switched from React 18's useEffect(params.then(...)) + setState to React 19's use(params). No more flash of empty listing id on first paint.
  • i18n CI gate: lint job runs pnpm i18n:check. A regression in inline ternaries or raw toLocaleDateString() now blocks merge.
  • Pulled in upstream linter improvements: defensive ?. on report-issue's dictionary access, Arabic weekday support + RTL handling in <Calendar/>, dropdown class normalisation in vertical-search.tsx.

Verification

  • pnpm typecheck clean.
  • pnpm i18n:check PASS.
  • After merge: cookieless visit shows the banner. Clicking Accept fires Vercel Analytics. curl /api/health includes a services.lastBooking.latency integer (hours). /hosting/listings/editor/[id]/details/photo-tour no longer flashes empty.

Out of scope

  • Full Sentry replacement — Vercel Analytics + structured logger.error covers v2.0; revisit when error volume warrants.
  • Stripe Connect host payout — separate epic.
  • Transactional emails — needs domain configuration.
  • Legal copy review (the existing /privacy /terms /cookies /accessibility pages are dictionary-driven with real sections; full legal review is a lawyer task).

Ready to tag

After this merges, every item on the plan's "Ship gate" should pass:

  • / redirects to /ar ✅ (Phase B)
  • 0 inline ternaries, 0 raw toLocaleDateString ✅ (audit gates CI)
  • Report-issue widget on every locale-prefixed page ✅ (Phase A)
  • Publish CTA + real reviews ✅ (Phase C1)
  • Per-office Sudan bank details + Stripe webhook idempotency + refund calculator ✅ (Phase C2)
  • verifyOffice + real /admin/settings ✅ (Phase C3)
  • Cookie consent + Vercel Analytics + observability + clean README ✅ (this PR)

Refs #7, #14, #17

abdout and others added 17 commits April 26, 2026 14:25
…d-start

The pg adapter holds long-lived TCP connections that Neon drops when its
serverless compute scales to zero, causing the next query to fail with
"Server has closed the connection". The neon serverless adapter speaks
HTTPS+WS and wakes the compute on demand. Detect Neon URLs and switch
automatically when DATABASE_URL_ADAPTER is unset.

Pairs with a backfill of the missing Location.createdAt/updatedAt columns
applied via Neon MCP — schema.prisma listed them but production never
received the ALTER TABLE, so every Listing.findMany() failed once the
nested location include resolved.

Refs #4
…apes, restructure meta

- Replace pathname.startsWith("/ar") heuristic with the existing
  reportIssue.* dictionary slice via useDictionary(). A single locale
  source keeps the widget consistent with every other localized surface.
- Use literal Arabic glyphs in fallback handling instead of \u escape
  sequences (carry-over from the previous edit; Arabic is now sourced
  from the dictionary anyway).
- Pull browser/viewport/direction into a nested `meta` object on the
  server action's input shape so the body fields are always emitted in
  the same canonical order — needed by the kun report-agent parser.
- Self-bootstrap the `report` GitHub label on 422 with color #d93f0b so
  the action works in any repo without manual seeding.

Refs #5
…link

Mounts <ReportIssue variant="icon"/> as a fixed-position floating button
in the locale-root layout so every one of the 117 locale-prefixed routes
exposes the report widget. Previously only two footer components carried
it, leaving auth, hosting, dashboard, transport, and admin surfaces with
no path to file a bug.

Adds the explicit Next 16 viewport export (width/initialScale/themeColor)
that mobile Safari needs to render at the correct DPR and to honour
prefers-color-scheme for the address bar.

Moves the bilingual skip-link literal at layout.tsx:74 to
common.skipToContent in both dictionaries — the only remaining inline
ternary in the locale-root layout.

Refs #5
…consolidated proxy

- Flip `i18n.defaultLocale` from `en` to `ar` per the global rule. The proxy
  already cookie-pins NEXT_LOCALE, so visitors with a prior English preference
  are not redirected; only fresh, headerless visits land on /ar/*.
- Make `useDictionary()` lenient — return the bundled English JSON as a
  static fallback when no DictionaryProvider is mounted instead of throwing,
  so test fixtures, isolated demos, and the root error page render strings.
- Drop the duplicated `src/components/internationalization/middleware.ts`.
  Its `localizationMiddleware` export was never imported (proxy.ts is the
  live entry point in Next 16) and the parallel logic was a footgun.
- Update `[lang]/layout.tsx` so the default-lang and default-config fallbacks
  reference `i18n.defaultLocale` instead of a hardcoded "en", and the
  hreflang `x-default` mirrors the same. Metadata API auto-emits the
  alternate hreflang links.
- Add `src/lib/i18n/date-locale.ts` (`dateLocaleFor` + `intlLocaleFor`) and
  `src/components/ui/direction-aware-icon.tsx` so future code has one place
  to look for "date-fns locale per app locale" and "icon flip on RTL".

Refs #7
…n, transport metadata, and skipToContent

Adds the keys the upcoming refactor needs so per-page generateMetadata,
filter panels, paginators, and the skip-link can pull from the dictionary
instead of inline `lang === 'ar' ? ... : ...` ternaries:

- `common.skipToContent` (replaces the bilingual literal at layout.tsx:74)
- `pageMetadata.<page>` objects with `title` + `description` + (optional)
  `subtitle` for help / host / hosting / hostingListings / transportHost /
  transportOffices / transportCheckout / landing / login / join / reset /
  managersProperties / tenantsFavorites / tenantsResidences
- `rental.property.filters` — `title`, `clearAll`, `any`, `priceRange`
- `rental.property.pagination` — `previous`, `next`, `pageOf`
- `transport.search.metadataTitle` + `metadataDescription` + `pagination`
- `transport.routes.hoursFormat` + `currency`
- `transport.metadataTitle` + `metadataDescription`

Both en.json and ar.json updated in parity.

Refs #7
…String calls

Every visible string and date now flows through the dictionary or through
the locale-aware formatter helpers. Concrete swaps:

- generateMetadata across 17 pages now pulls from `dictionary.pageMetadata.*`
  instead of `lang === 'ar' ? "..." : "..."` ternaries.
- `transport/page.tsx`, `transport/search/page.tsx`, `listings/page.tsx`,
  `transport/booking/checkout/content.tsx`, etc. drop their per-key
  ternary fallbacks now that the dictionary has the keys upstream.
- Date display sites — bookings, transport-host, dashboard, tenants,
  managers, admin tables, ApplicationCard — call `formatDate(date, lang)`
  from `src/lib/i18n/formatters.ts` instead of `new Date(x).toLocaleDateString()`.
  Admin tables receive `lang` as a prop from their server-page caller.
- Locale narrowing in client components (`useParams<{lang}>().lang === 'ar' ? 'ar' : 'en'`)
  is replaced by `useLocale()` which returns the typed `Locale`.
- Data-shape ternaries (category.labelAr/label, tw.rangeAr/En, DEMO_DATA.ar/en)
  are restructured to be locale-keyed objects so call sites do
  `obj[lang]` instead of branching.
- The decorative `data-day` attribute in `ui/calendar.tsx` switches to a
  stable ISO date so the value is locale-independent.

Closes the i18n anti-pattern audit's `inline lang ternaries` and
`raw toLocaleDateString calls` budgets at zero.

Refs #7
…t + lefthook gate

- `ui/carousel.tsx`, `ui/calendar.tsx`, `listings/pagination.tsx`,
  `[lang]/hosting/calendar/page.tsx`, and the manager-applications
  back-button each pick up `rtl:rotate-180` so their chevrons and arrows
  point the right way under RTL. Decorative arrows whose meaning is
  rotation-independent (e.g. the ticket-page absolute-positioned arrow)
  are intentionally left as-is.
- `HeartButton` and the `<ReportIssue variant="icon" />` trigger now wrap
  their svg/lucide icon in a 44x44 hit area (`h-11 w-11 inline-flex`),
  matching iOS HIG and WCAG 2.5.5 (Target Size, AA).
- New `scripts/i18n-anti-pattern-check.sh` greps for inline lang ternaries,
  raw toLocaleDateString, raw toLocaleString, and likely-hardcoded English
  JSX. Each metric has an env-overridable budget; the budget can only
  shrink. Wired as `pnpm i18n:check` and added to lefthook pre-commit.
- `tests/e2e/seo.spec.ts` extended with three new assertions: html
  lang+dir parity per locale (en/ltr, ar/rtl) and presence of the
  report-issue mount on every locale-prefixed surface.

Refs #7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final pieces of Phase C1:

- Mobile listing detail now passes the latest 8 reviews + average rating +
  total count into <MobileReviews/>. The component drops its sample-data
  fallback and renders an empty state when there are no reviews. Review
  date is locale-aware via formatDate.
- /host/content.tsx no-op `handleCreateFromExisting` TODO is replaced
  with a route to the host overview (where the existing listings are
  pickable). Clone-from-existing is filed as a follow-up.
- New `docs/booking-vs-application.md` documents the dual-track rental
  flow (short-term Booking vs. long-term Application -> Lease) and the
  rule that they must not be mixed in a single UI flow.

Refs #9
…ency log

Schema additions for Phase C2:

- TransportOffice gains `bankName`, `bankAccount`, `bankHolder`, `momoNumber`,
  `momoProvider` — per-office Sudan payment instructions so the bank-transfer
  card on the transport checkout reads from the operator's own row instead
  of the hardcoded `1234567890` that was sending every guest's money to one
  global mkan account.
- New `WebhookEvent` model logs every Stripe (and future provider) webhook
  by `eventId @unique`. Inserts before processing so a duplicate Stripe
  delivery hits the unique constraint, the route returns 200, and side
  effects don't run twice.

Migration `20260428103000_payments_complete` is idempotent (`ADD COLUMN IF
NOT EXISTS`, `CREATE TABLE IF NOT EXISTS`) and was applied directly to
prod via Neon MCP.

Refs #14
…egration

`src/lib/refund.ts` is a pure function (no Date, no DB, no fetch) that takes
the listing's `cancellationPolicy`, the total paid, optional cleaning fee,
and the hours-before-check-in, and returns a Stripe-ready refund breakdown
(`refundAmount`, `refundAmountMinor`, human-readable `reason`, `isFullRefund`).

Policy semantics mirror Airbnb so guests don't have to learn a new
vocabulary:
- Flexible: full up to 24h before, cleaning fee kept within 24h.
- Moderate: full 5+ days before; 50% of nightly + cleaning between 5d–24h;
  none within 24h.
- Firm: full 30+ days before; none within 30 days.
- Strict: full 7+ days before; none within 7 days.
- NonRefundable: never.

`tests/lib/refund.test.ts` covers all five policies + null fallback +
post-check-in lockout + Stripe minor-units rounding (14 cases, all pass).

`cancelBooking` now computes the breakdown server-side and returns it in
the action response so the client cancellation dialog can confirm the
refund the guest will receive. The Stripe refund itself is still fired by
admin via processRefund (the Booking model doesn't carry a payment_intent
yet — tracked as a follow-up).

Refs #14
…n checkout

- Stripe webhook handler inserts into `WebhookEvent` by `eventId @unique`
  before running side effects. A duplicate delivery hits the unique
  constraint, the handler short-circuits with `{ ok: true }`, and the
  route returns 200 — no double-charged Payment rows, no double-paid
  TransportPayment rows. Other DB errors still surface so Stripe retries.
- The transport-booking checkout's bank-transfer card now reads
  `office.bankName`/`bankAccount`/`bankHolder` off the booking's nested
  `trip.route.office` (relation already included in `getBooking`), and
  shows a translated "operator hasn't published bank details yet"
  fallback when the office row is empty. The hardcoded
  `Bank of Khartoum / Mkan Transport Services / 1234567890` global
  account is gone.
- `paymentSucceeded` and `paymentFailed` toast messages move from inline
  `locale === 'ar' ? ... : ...` ternaries into the file-local
  `paymentMethodTranslations` table so the i18n audit stays clean.

Refs #14
…earch-gating

- New `verifyOffice(officeId)` and `unverifyOffice(officeId, reason?)` admin
  actions in `lib/actions/admin-actions.ts`. Verification flips
  `TransportOffice.isVerified` and audits the change.
- `searchTrips` (and the routes it builds via `buildTripWhere`) now requires
  `route.office.isVerified=true` AND `office.isActive=true`. Unverified
  operators stay invisible to public guests even if their `isActive` flag
  was flipped on. Admin can still browse them via /admin/transport.
- New singleton `PlatformSetting` model with platformFeePct,
  defaultCancellationPolicy, supportedCurrencies, payoutScheduleDays,
  emailFrom, supportEmail. The `getPlatformSettings` action lazily creates
  id=1 on first read so the rest of the app can rely on it always existing.
- Migration `20260428113000_admin_settings` is applied to prod via Neon MCP.

Refs #14
…sport/[id]

- Replace /admin/settings "Coming soon" Card with a real tabbed form bound
  to `getPlatformSettings` / `updatePlatformSettings`. Six sections: platform
  fee %, default cancellation policy, supported currencies, payout schedule,
  outbound email From, support email. Saves via server action with toast.
- New <VerifyOfficeButton/> client island on /admin/transport/[id] wraps
  verifyOffice / unverifyOffice in useTransition. Disabled state during
  the call; toast on success/error. Each click revalidates the public
  /transport/search and /transport/offices paths so the gate flips
  immediately.
- Calendar's data-day attribute stays on locale-independent ISO date so
  the i18n audit doesn't regress (this kept reverting between sessions).

Refs #14
- Remove `@react-pdf/renderer` (no PDF route exists), `@tiptap/react`,
  `@tiptap/starter-kit` (no rich-text editor surface) — verified by
  `grep -rln "@react-pdf|@tiptap" src/` returning empty.
- Replace the 25-line marketing README with a real one: stack table,
  quick-start, env-var matrix (required + optional), `pnpm` script
  reference, code map, deploy notes, contributing workflow.

Refs #17
- New `<CookieBanner/>` (translated, three-button: Accept all / Reject
  non-essential / Learn more) anchors at the bottom of the locale-root
  layout. Reads/writes the `cookieConsent` cookie (1y), persists choice
  for return visits.
- New `<ConsentAwareAnalytics/>` mounts `@vercel/analytics` and
  `@vercel/speed-insights` only when the user has accepted "all". Listens
  to the `cookieconsent` window event the banner dispatches so analytics
  light up immediately on Accept (no reload).
- Both mount in `[lang]/layout.tsx` alongside the existing report-issue
  floating button. Dictionary keys added under `cookieConsent.*` in
  both en.json and ar.json.

Refs #17
…18n CI gate

- `/api/health` adds a `lastBooking` service block with the age-in-hours
  of the most recent Booking row. Informational (doesn't fail the
  status), but on-call can spot a silent regression in the booking flow
  before users do.
- `hosting/listings/editor/[id]/details/photo-tour/page.tsx` switches
  from the React 18 anti-pattern (`useEffect(params.then(...))` + setState)
  to React 19's `use(params)`. Fixes a flash-of-empty listing id on first
  paint and silences the "params is a Promise" warning.
- CI: lint job runs `pnpm i18n:check` so a regression in inline ternaries
  or raw `toLocaleDateString()` blocks merge.
- Pull in defensive null-checks on `report-issue` dict access, the
  Calendar Arabic-weekday + RTL improvements, and the calendar dropdown
  styling normalization in `vertical-search.tsx` from the upstream
  worktree.

Refs #17
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mkan Ready Ready Preview, Comment Apr 29, 2026 4:27am

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