Skip to content

Mvp overhaul#108

Merged
Taleef7 merged 16 commits intomainfrom
mvp-overhaul
Mar 4, 2026
Merged

Mvp overhaul#108
Taleef7 merged 16 commits intomainfrom
mvp-overhaul

Conversation

@Taleef7
Copy link
Copy Markdown
Owner

@Taleef7 Taleef7 commented Mar 3, 2026

Changes
This pull request introduces several improvements and refactors to the admin analytics dashboard and related backend logic. The main focus is enhancing platform health visibility, especially around package renewals, and improving code maintainability and UI consistency.

Admin Analytics Dashboard Enhancements:

  • Added a "Renewals Due" section to the analytics dashboard, showing students with packages expiring in the next 5 days, including session counts and expiry dates. This uses the new getExpiringPackages service and joins user profile data for display. [1] [2]
  • Changed the calculation of "active students" to count unique users with active requests, providing a more accurate metric. [1] [2]

UI and Styling Updates:

  • Updated dashboard headings, cards, and sections to use a consistent color palette and bolder typography for improved readability and visual hierarchy. [1] [2] [3]

Backend and Auth Refactor:

  • Refactored admin action authentication by importing and using the shared requireAdmin function from lib/auth/requireAdmin, removing the local duplicate implementation and unused sign-out code. [1] [2] [3]

Documentation and Environment Setup:

  • Added a comprehensive CLAUDE.md file detailing repository tooling, workflows, coding standards, DB migration discipline, and architectural conventions for onboarding and automation.
  • Updated .env.example to include new environment variables for bank details, Sentry, and cron secrets, clarifying required fields for deployment and local development.

Taleef7 and others added 16 commits March 3, 2026 16:33
- Generate lib/supabase/database.types.ts from all 19 migrations covering
  13 tables, 8 enums (user_role, request_status, payment_status,
  match_status, session_status, etc.) and 5 RPC functions
- Add Database generic to createBrowserClient<Database>() in client.ts
- Add Database generic to createServerClient<Database>() in server.ts;
  also add try/catch around setAll() so Server Components that read
  cookies do not throw on cookie-write attempts
- Add Database generic to createClient<Database>() in admin.ts
- Update tsconfig.json with @/ path alias for clean imports

This eliminates all manual inline type assertions (as SomeShape | null)
and provides compile-time detection of column name typos across the
entire codebase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rary

Design system (app/globals.css + app/layout.tsx):
- Import Outfit font via next/font/google as primary typeface
- Define CSS custom properties for full Bauhaus palette:
  background #F0F0F0, foreground #121212, primary-red #D02020,
  primary-blue #1040C0, primary-yellow #F0C020
- Add .bauhaus-card, .bauhaus-card-sm, .bauhaus-shadow, .bauhaus-shadow-lg
  utility classes (thick borders + hard offset shadows)
- Add accessible :focus-visible ring, subtle scrollbar, and
  prefers-reduced-motion support
- Wrap root layout in TooltipProvider; mount <Toaster> (Sonner) globally

UI component library (components/ui/):
- Install and configure shadcn/ui components: alert-dialog, badge,
  button, card, dialog, dropdown-menu, form, input, label, pagination,
  select, separator, skeleton, sonner, table, tabs, tooltip
- Add custom bauhaus.tsx with BauhausCard, BauhausButton, BauhausBadge
  variants (hard shadow, thick borders, no border-radius)
- components.json added for shadcn/ui CLI configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…afe sign-out (C1+C2+C5)

next.config.ts — security headers (C1):
- X-Frame-Options: DENY (clickjacking protection)
- X-Content-Type-Options: nosniff (MIME-sniffing protection)
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: camera=(), microphone=(), geolocation=()
- Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
- X-DNS-Prefetch-Control: on

middleware.ts — role-based enforcement + email verification (C2):
- After session refresh, check email_confirmed_at for email/password
  users; redirect unverified users to /auth/verify
- Fetch primary_role from user_profiles on protected routes:
  /admin/* requires primary_role = 'admin'
  /tutor/* requires primary_role = 'tutor' | 'admin'
- Redirects to /dashboard on role mismatch (no layout-only guard)

app/auth/actions.ts — CSRF-safe sign-out (C5):
- New Server Action signOut() replaces plain POST to /auth/sign-out
- Next.js Server Actions have built-in CSRF protection via the
  Origin header check; no explicit token needed
- app/auth/sign-out/route.ts retained but simplified

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(F1+F3+F4+F5)

vercel.json (F1):
- Cron job: /api/cron/expire-packages runs daily at 02:00 UTC
  to automatically expire packages past their end_date

.env.example (F4):
- Documents all required environment variables with descriptions:
  Supabase (URL, anon key, service role), WhatsApp number, bank
  details (NEXT_PUBLIC_BANK_*), CRON_SECRET, SENTRY_DSN placeholder
- Developers copy this to .env.local before running the project

package.json (F3+F5):
- Move @types/luxon to devDependencies (type-only package, not runtime)
- Add "typecheck": "tsc --noEmit" script for fast CI type checking
- Add "test": "vitest run" and "test:watch": "vitest" for unit tests
- Add vitest, @vitest/ui, and related packages

vitest.config.ts:
- Configure Vitest with @/ path alias and node test environment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ers, and RLS

20260225000004 — create_payment_proofs_bucket:
- Private storage bucket 'payment-proofs' with per-user path scoping
- RLS policies: owner can insert/select own proofs; service_role has
  full access; no public reads (signed URLs required)

20260225000005 — add_admin_notes_to_matches:
- Add admin_notes text column to matches table for internal annotations

20260226000001 — update_handle_new_user:
- Update handle_new_user() trigger to correctly populate user_profiles
  on sign-up, including for_student_name and timezone defaults

20260226000002 — subjects_grant_select:
- Grant SELECT on subjects table to authenticated role so students
  can load the subject dropdown in the request form

20260226000003 — requests_package_tier:
- Add package_tier column to requests table (8 | 12 | 20 sessions)
  so the package selection is recorded on the request itself

20260226000004 — tutor_profile_fields:
- Add experience_years, education, teaching_approach columns to
  tutor_profiles for richer profile display on student dashboard

20260228000001 — subjects_rls_policy:
- Add explicit RLS policy for subjects: all authenticated users can
  read active subjects; only service_role can mutate

seed.sql:
- Update seed data to match new schema columns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The original tutor_update_session() RPC incremented sessions_used
whenever p_status IN ('done', 'no_show_student') without checking the
session's previous status. This meant changing a session from 'done'
to 'no_show_student' would double-increment, silently burning credits.

Migration 20260303000001 replaces the RPC with a guarded version:
- SELECT current status before any mutation
- Only increment sessions_used when transitioning FROM a non-consuming
  status TO a consuming status ('done' | 'no_show_student')
- Only decrement (via new decrement_sessions_used helper) when
  transitioning FROM a consuming status TO a non-consuming status
- No increment/decrement for no_show_tutor or rescheduled regardless
  of previous status
- Existing ceiling guard (sessions_used < sessions_total) is preserved

Also adds decrement_sessions_used(p_request_id) RPC as a safety
valve for admin corrections without direct SQL access.

The TypeScript path in lib/services/sessions.ts already had this guard
via isConsuming() check; the SQL RPC now has parity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…imiting (C3+C4+A4)

lib/services/sessions.ts:
- Add tutorUpdateSessionStatus() with isConsuming() guard
- Add adminOverrideSessionStatus() for admin corrections
- Add rescheduleSession() for admin-triggered reschedules
- All mutations write to audit_logs

lib/services/payments.ts:
- Add expirePackages() — marks packages past end_date as expired and
  updates associated request status to 'ended'
- Add getExpiringPackages(days) — returns packages expiring within N
  days with student/subject info for admin renewal outreach
- markPaymentPaid() has full rollback logic for cascading status
  changes (payment → package → request)

lib/services/requests.ts:
- Add updateRequestStatus() with audit log
- Add createRequest() with Zod validation at the service boundary

lib/services/whatsapp.ts:
- Add buildWhatsAppLink() and message template helpers

lib/utils/request.ts, lib/utils/session.ts:
- Extend STATUS_LABELS, STATUS_COLOURS, SESSION_STATUS_LABELS
- Add SESSION_STATUS_COLOURS, LEVEL_LABELS, getLevelLabel helpers

lib/validators/payment.ts (C3):
- Add markPaidSchema (paymentId, packageId, requestId as z.string().uuid())
- Add rejectPaymentSchema (paymentId UUID, rejectionNote min/max length)
- Schemas used at the top of admin payment server actions

lib/config/pricing.ts (A2):
- Replace CONFIGURE_BEFORE_LAUNCH literals with NEXT_PUBLIC_BANK_*
  environment variable reads; falls back gracefully if unset

lib/rate-limit.ts (C4):
- In-memory sliding-window rate limiter with periodic cleanup
- checkRateLimit(key, limit, windowMs) → { success, remaining }
- For production scale, replace with @upstash/ratelimit + Redis

lib/auth/requireAdmin.ts:
- Reusable requireAdmin() guard for all admin server actions
- Throws if user is not authenticated or does not have admin role

lib/config/timezones.ts:
- IANA timezone list for the profile setup and request forms

app/api/leads/route.ts (C4):
- Apply rate limiting: 10 requests per minute per IP
- Return 429 with Retry-After header when limit exceeded

app/api/cron/expire-packages/route.ts (A4):
- Vercel Cron endpoint called daily at 02:00 UTC
- Validates CRON_SECRET header before executing
- Calls expirePackages() and logs count of expired packages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…us treatment (D3)

app/admin/page.tsx:
- Now an async Server Component fetching live counts in parallel:
  pending payments, pending tutor approvals, new requests,
  upcoming sessions, active subjects
- Cards show live badge counts with color-coded Bauhaus styling
- Subjects management card added

app/admin/analytics/page.tsx (D3):
- New "Renewals Due (Next 5 Days)" section calling getExpiringPackages(5)
- Lists student name, subject, sessions remaining, expiry date
- Pending payments, tutor approvals, unmarked sessions widgets

app/admin/layout.tsx:
- Convert sign-out to use signOut() Server Action (C5 fix)
- Mobile nav: add CSS gradient fade indicators on both edges to
  signal horizontal scrollability (10 nav items on small screens)
- Navigation links updated to include subjects

app/admin/requests/:
- Redesigned with Bauhaus card layout and status badges
- RequestFilters.tsx: status, level, subject filter dropdowns
- [id]/AssignTutorForm.tsx: tutor selector with subject/level match
- [id]/AdminRequestActions.tsx: new component for inline actions
- actions.ts: createMatch(), updateRequestStatus() with audit logs

app/admin/payments/:
- PaymentActions.tsx: mark paid / reject with rejection note modal
- PaymentQuickActions.tsx: new quick action bar for payment list
- ViewProofButton.tsx: triggers signed URL for proof preview
- actions.ts: Zod validation on markPaymentPaid/markPaymentRejected

app/admin/tutors/:
- Tutor detail shows full profile, subjects, availability
- TutorActions.tsx: approve/reject with audit log
- TutorFilters.tsx: filter by approved status

app/admin/sessions/:
- SessionActions.tsx: admin status override with reason
- Sessions list shows overdue sessions prominently

app/admin/matches/:
- Match detail: EditMatchForm (schedule pattern + Meet link),
  GenerateSessionsForm, WhatsApp template copy buttons
- MatchActions.tsx: reassign tutor, edit match

app/admin/subjects/:
- New subjects management CRUD UI for admin
  (add/activate/deactivate subjects in the request form)

app/admin/users/page.tsx:
- User list with role badges and primary role display

app/admin/error.tsx, app/admin/loading.tsx:
- Error boundary and skeleton loading state for admin routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ssword, tutor onboarding

app/auth/sign-up/page.tsx:
- Full Bauhaus-styled sign-up form with client-side Zod validation
- Show/hide password toggle; clear error messages
- Redirect to /auth/verify after successful sign-up

app/auth/sign-up/tutor/:
- Dedicated tutor sign-up flow that sets primary_role = 'tutor'
  and pre-populates the profile-setup step with tutor fields

app/auth/sign-in/page.tsx + SignInForm.tsx:
- Redesigned sign-in with Google OAuth button and email/password form
- Inline error display for invalid credentials
- 'Forgot password?' link

app/auth/forgot-password/:
- New forgot-password page: sends Supabase password reset email
- Success state with resend option

app/auth/reset-password/:
- New reset-password page: handles magic link callback,
  prompts for new password with confirmation

app/auth/verify/page.tsx:
- Enhanced: shows email address, resend instructions,
  links to Gmail/Outlook
- ResendButton.tsx: client component to resend verification email
  with 60s cooldown to prevent spam

app/auth/profile-setup/page.tsx:
- Collects display_name, whatsapp_number (normalises +92 prefix),
  timezone (IANA selector), and for_student_name (parent accounts)
- Validates WhatsApp format before saving; redirects to /dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… proof URLs (A1+A3+D4+D5+D8)

app/dashboard/page.tsx:
- OnboardingChecklist: progress bar + step list tracking
  profile → request → payment → match → first session;
  auto-hides when all steps are complete
- StatusBanner: request-status-aware banner explaining the
  current state and next action (payment_pending, ready_to_match,
  matched, active, paused, ended)
- Redirects admin→/admin, tutor→/tutor based on primary_role

app/dashboard/packages/[id]/page.tsx (A1+A3+D5):
- Replace getPublicUrl() (broken on private buckets) with
  getPaymentProofSignedUrl() server action (300s signed URL,
  verifies user owns the payment record)
- Rejected payments: show upload form again with rejection reason
  from payment.rejection_note; student can re-upload proof and
  enter a corrected reference number
- resubmitRejectedPayment() server action resets status → pending

app/dashboard/requests/:
- RequestForm.tsx: multi-step request form with subject selector,
  level/exam-board pickers, goals textarea, availability windows,
  timezone selector
- CancelRequestButton.tsx: cancel pending requests with confirmation
- actions.ts: createRequest(), cancelRequest() server actions

app/dashboard/sessions/page.tsx:
- Sessions list sorted by scheduled_start_utc; past sessions shown
  with status badge; RescheduleButton on upcoming sessions

app/dashboard/tutor/[id]/page.tsx (D4):
- Read-only tutor profile page accessible to matched students
- Shows: display name, bio, subjects by level, experience, education,
  teaching approach; hides WhatsApp and availability windows
- Verifies match relationship before granting access

app/dashboard/layout.tsx:
- Dashboard layout with nav links and sign-out Server Action
app/dashboard/error.tsx, app/dashboard/loading.tsx:
- Error boundary and skeleton loading state

components/dashboards/:
- OnboardingChecklist.tsx: progress bar + step checklist component
- StatusBanner.tsx: request status → friendly message + colour variant
- NextSessionCard.tsx: tutor name, subject, time in user's timezone,
  Meet link button; empty state when no upcoming session
- PackageSummary.tsx: sessions remaining, renewal alert at ≤3 left or ≤5 days
- RescheduleButton.tsx: disabled with tooltip when < 24h to start (B3)
- SessionCompleteForm.tsx: toast.success()/toast.error() on submit (G1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ror boundaries

app/tutor/page.tsx:
- Next session hero card: time in tutor's timezone (luxon), student
  name, subject, level, one-tap Meet link, Google Calendar add button
- Upcoming session count, completed session count quick stats
- Quick links to sessions list, profile edit

app/tutor/sessions/page.tsx:
- Sessions sorted chronologically; past sessions show status badge
- SessionCompleteForm inline: mark done / student no-show / tutor
  no-show with optional notes; toasts on success/error (G1)

app/tutor/profile/TutorProfileForm.tsx:
- WeeklyAvailabilityPicker integration for setting availability
- Subjects multi-select by level (O Level, A Level, both)
- Bio, experience_years, education, teaching_approach fields
- Saves to tutor_profiles via updateTutorProfile() server action

app/tutor/profile/actions.ts:
- updateTutorProfile() validates with Zod and writes audit log

app/tutor/layout.tsx:
- Nav with sign-out Server Action; mobile-friendly horizontal scroll
  with fade indicators

app/tutor/conduct/page.tsx:
- Tutor code of conduct: attendance, punctuality, communication,
  no-show consequences, Meet link policy

app/tutor/error.tsx, app/tutor/loading.tsx:
- Error boundary and skeleton loading state for tutor routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app/page.tsx:
- Bold uppercase hero headline with geometric color block accent
- Thick black borders and 8px hard-shadow (bauhaus-shadow-lg)
- How It Works section: 4-step process with numbered geometric squares
- Subjects & Levels section with badge grid
- Pricing cards for 8/12/20 session tiers with Bauhaus button style
- LeadForm and WhatsApp CTA integrated at bottom
- Fully responsive: stacks vertically on mobile, side-by-side on lg

components/LeadForm.tsx:
- Redesigned with Bauhaus input/button style
- Client-side Zod validation (name, email, phone, message)
- Toast feedback on success/error
- Rate-limited by /api/leads (10 req/min)

components/WhatsAppCTA.tsx, WhatsAppLink.tsx:
- WhatsApp number sourced from NEXT_PUBLIC_WHATSAPP_NUMBER env var
- WhatsAppLink builds wa.me URL with optional pre-filled message

components/CopyMessageButton.tsx:
- Copy-to-clipboard + open WhatsApp in new tab
- Clipboard icon → check icon animation on copy

components/AdminPagination.tsx:
- Reusable pagination component for all admin list pages
- Uses shadcn Pagination component; handles page=1 edge case

components/WeeklyAvailabilityPicker.tsx:
- Interactive day/time slot grid for tutor availability and
  student availability windows on request form
- 7 days × time slot columns; toggleable cells with shift+click range

app/error.tsx, app/not-found.tsx:
- Global error boundary and 404 page with Bauhaus styling
- 404 shows numeric big text block with return-home link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app/privacy/page.tsx:
- Full Privacy Policy covering: data collected (name, email, WhatsApp,
  payment receipts), how data is used, storage via Supabase (EU/US),
  data retention, user rights (access, deletion, correction),
  contact information for data requests
- Important for legal compliance when handling student/parent data
  and payment proof uploads

app/terms/page.tsx:
- Terms of Service covering: service description, user eligibility,
  payment terms (bank transfer, 48h verification), refund policy,
  no-show policy per docs/MVP.md, intellectual property,
  account termination conditions
- Pakistan-jurisdiction clause; links to privacy policy

app/help/page.tsx:
- FAQ section with 10 common questions across: Getting Started,
  Sessions, Payments, Technical categories
- WhatsApp contact button (pre-filled "I need help" message)
- Email support address
- Quick links to /policies, /privacy, /terms

All three pages use Bauhaus styling and are accessible from the
landing page footer and the student dashboard help icon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tests

vitest.config.ts (already committed) configures Vitest with node
environment and @/ path alias.

lib/services/__tests__/scheduling.test.ts (11 tests):
- Generates correct session count for each tier (8/12/20)
- Respects tier cap (no extra sessions beyond tierSessions)
- Respects start/end date range (no sessions outside the month)
- Handles 31-day months correctly
- Converts session times to UTC correctly for Asia/Karachi (PKT, +5:00)
- Handles DST boundary correctly for America/New_York (EDT/EST)
- Returns empty array for unrecognised IANA timezone
- Returns empty array for malformed time string (HH:MM expected)
- Returns empty array when no days match in date range
- Calculates duration_mins correctly in scheduled_end_utc
- All 11 tests pass

lib/__tests__/rate-limit.test.ts (4 tests):
- Allows requests within the configured limit (remaining decrements)
- Blocks requests once the limit is exceeded
- Resets counter after the window expires (tested with vi.useFakeTimers)
- Tracks different IP keys independently
- All 4 tests pass

playwright.config.ts:
- Playwright E2E configuration targeting http://localhost:3000
- Single browser (Chromium) for CI speed; full browser matrix optional
- webServer block to auto-start dev server during test runs

e2e/:
- Initial E2E test stubs for: sign-up flow, student request creation,
  admin payment approval, tutor session completion

Run: npm test (unit) | npm run test:e2e (E2E)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pleted sprint

CLAUDE.md:
- Add unit test commands: npm test, npm run test:watch
- Add typecheck command: npm run typecheck
- Update key file locations table with new infrastructure:
  database.types.ts, rate-limit.ts, OnboardingChecklist,
  StatusBanner, unit tests, cron jobs, vercel.json/.env.example
- Fix Atomic RPCs section: p_package_id → p_request_id (B2)

docs/GAP_ANALYSIS.md:
- Update summary table with status column (24/34 items done)
- Mark all resolved items with ✅ DONE and resolution details:
  A1-A4, B1-B3, C1-C5, D3-D8, E1-E2, F1+F3-F5, G1-G2, H1
- Mark deferred items: B4 (reminders), D1 (parent accounts),
  D2 (email), F2 (Sentry), G3 (skeletons), H3 (tables)
- Update Recommended Priority Order: all critical/important items
  struck through; only Sentry and nice-to-haves remain

docs/plan-CorvEd.md:
- Sprint 4 marked COMPLETE
- Section 3 (MVP v0.1 blockers): mark addressed items done
- Section 4 (MVP v0.2): mark completed items; testing section done
- Section 6 (refinement plan): update step statuses
- Section 9 (new): full inventory of files created and modified
  in this sprint with description of each change

advisor_update.md, plan-corvEd.prompt.md:
- Planning artifacts for this sprint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
scripts/bauhaus-replace.ps1:
- PowerShell utility script used during the Bauhaus design sprint to
  batch-remove dark mode Tailwind classes (dark:bg-*, dark:text-*,
  dark:border-*, dark:hover:*) from all .tsx files in app/ and
  components/; CorvEd uses a single light-mode Bauhaus palette so
  dark mode variants are intentionally removed
- Also standardises specific Bauhaus button and badge class strings
  across the codebase for consistency

scripts/fix3.ps1:
- Targeted follow-up script to correct residual class mismatches
  after bauhaus-replace.ps1 (e.g. hover:border-indigo-400 →
  hover:border-[#1040C0], animate-spin colour fix, button variant
  string standardisation)
- Both scripts are idempotent: re-running them on already-updated
  files produces no changes

supabase/.branches/_current_branch:
- Supabase CLI internal file tracking the active local branch ("main")
- Committed so the local Supabase environment matches the repo state
  when a new developer clones and runs `npx supabase start`

.gitignore additions:
- .claude/         — Claude Code local settings (machine-specific)
- .playwright-mcp/ — Playwright MCP server runtime state
- .serena/         — Serena semantic code nav tool state
- .vscode/         — VS Code MCP config (machine-specific, not project config)
- playwright-report/ — generated Playwright HTML reports
- test-results/    — generated Playwright test artifacts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Taleef7 Taleef7 added this to the Phase 0 milestone Mar 3, 2026
@Taleef7 Taleef7 self-assigned this Mar 3, 2026
Copilot AI review requested due to automatic review settings March 3, 2026 21:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR overhauls the MVP admin + user dashboards by adding renewal/health visibility, tightening auth/route protection, and standardizing the UI component system (Bauhaus styling + shadcn-style primitives), alongside new backend utilities (rate limiting, cron expiry, typed Supabase clients) and expanded E2E/unit tests.

Changes:

  • Adds platform health/admin tooling: package expiry cron + renewal lookups, new admin subjects management, pagination, and richer admin dashboard counts.
  • Strengthens auth/security: shared requireAdmin, role-based middleware + email verification redirect, API route rate limiting, and security headers.
  • Refactors UI: introduces reusable UI primitives (button/card/table/etc.), consistent styling, toasts/tooltips, plus loading/error/not-found pages and E2E coverage.

Reviewed changes

Copilot reviewed 143 out of 163 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
next.config.ts Adds global security headers for all routes
middleware.ts Adds role enforcement + email verification checks in middleware
lib/validators/request.ts Updates request validation for structured availability windows + package tier
lib/validators/payment.ts Adds zod schemas for payment-related server actions
lib/utils/session.ts Updates session status labels/colors + formatting utilities
lib/utils/request.ts Adds availability formatting helper and refines status styling constants
lib/utils.ts Adds cn() Tailwind utility (clsx + tailwind-merge)
lib/supabase/server.ts Adds typed server Supabase client + safer cookie writes
lib/supabase/client.ts Adds typed browser Supabase client
lib/supabase/admin.ts Adds typed service-role Supabase admin client
lib/services/whatsapp.ts Adds WhatsApp helpers + PK phone normalization
lib/services/requests.ts Adds request status transition helper + admin request fetch
lib/services/payments.ts Adds package expiry cron logic + expiring package query
lib/services/tests/scheduling.test.ts Adds vitest coverage for session generation logic
lib/rate-limit.ts Adds in-memory rate limiter utility
lib/config/timezones.ts Adds centralized IANA timezone options
lib/config/pricing.ts Pulls payment instruction bank details from env
lib/auth/requireAdmin.ts Introduces shared admin guard for server actions
lib/tests/rate-limit.test.ts Adds vitest coverage for rate limiter
e2e/utils.spec.ts Adds Playwright tests for utility formatting functions
e2e/responsive.spec.ts Adds basic mobile responsiveness checks
e2e/request-form.spec.ts Adds redirect checks for protected request/admin routes
e2e/protected-routes.spec.ts Adds protected-route redirect coverage
e2e/policies.spec.ts Adds policies page rendering assertions
e2e/not-found.spec.ts Adds 404 page assertions
e2e/landing.spec.ts Adds landing page smoke coverage
e2e/dashboard-features.spec.ts Adds dashboard route redirect coverage
e2e/auth.spec.ts Adds auth page smoke checks
e2e/accessibility.spec.ts Adds basic a11y checks (alt text, labels, focus)
components/ui/tooltip.tsx Adds Tooltip primitive wrapper
components/ui/tabs.tsx Adds Tabs primitive wrapper
components/ui/table.tsx Adds Table component wrappers
components/ui/sonner.tsx Adds global toast Toaster configuration
components/ui/skeleton.tsx Adds Skeleton component
components/ui/separator.tsx Adds Separator primitive wrapper
components/ui/pagination.tsx Adds Pagination components
components/ui/label.tsx Adds Label primitive wrapper
components/ui/input.tsx Adds Input component styling
components/ui/form.tsx Adds react-hook-form helpers + accessible field wiring
components/ui/dialog.tsx Adds Dialog primitive wrapper
components/ui/card.tsx Adds Card components
components/ui/button.tsx Adds Button component + variants
components/ui/badge.tsx Adds Badge component + variants
components/ui/alert-dialog.tsx Adds AlertDialog primitive wrapper
components/dashboards/StatusBanner.tsx Adds request status banner helper + component
components/dashboards/SessionCompleteForm.tsx Adds toasts + restyles tutor session completion form
components/dashboards/RescheduleButton.tsx Restyles WhatsApp reschedule CTA
components/dashboards/OnboardingChecklist.tsx Adds onboarding checklist component
components/dashboards/NextSessionCard.tsx Restyles next-session card + improves link a11y
components/WhatsAppLink.tsx Restyles WhatsApp link component
components/WhatsAppCTA.tsx Restyles WhatsApp CTA
components/WeeklyAvailabilityPicker.tsx Adds interactive weekly availability picker
components/CopyMessageButton.tsx Restyles copy/open WhatsApp buttons
components/AdminPagination.tsx Adds pagination component for admin lists
components.json Adds shadcn/ui config metadata
app/tutor/sessions/page.tsx Restyles tutor sessions list
app/tutor/profile/page.tsx Changes tutor profile reads to admin client (scoped by user) + restyles
app/tutor/profile/actions.ts Uses admin client for tutor profile mutations (scoped by user)
app/tutor/loading.tsx Adds tutor loading skeleton UI
app/tutor/layout.tsx Adds persistent tutor nav + server-action sign out
app/tutor/error.tsx Adds tutor route error UI
app/not-found.tsx Adds global 404 page
app/layout.tsx Adds global TooltipProvider + Toaster
app/globals.css Adds base styling, focus ring, scrollbar, motion preferences
app/error.tsx Adds global error page
app/dashboard/tutor/[id]/page.tsx Adds read-only student view of assigned tutor profile
app/dashboard/sessions/page.tsx Restyles student sessions list + improves link a11y
app/dashboard/requests/actions.ts Adds student request cancellation action
app/dashboard/requests/[id]/CancelRequestButton.tsx Adds cancel request UI
app/dashboard/packages/new/page.tsx Adds tier query param support + restyles package selection
app/dashboard/packages/actions.ts Adds signed URL generation + rejected payment resubmission
app/dashboard/loading.tsx Adds dashboard loading skeleton UI
app/dashboard/layout.tsx Adds persistent student nav + role redirects + server-action sign out
app/dashboard/error.tsx Adds dashboard route error UI
app/auth/verify/page.tsx Restyles verify page + adds geometric panel
app/auth/verify/ResendButton.tsx Adds resend verification CTA
app/auth/sign-out/route.ts Adds GET support and refactors sign-out handler
app/auth/sign-in/page.tsx Restyles sign-in page + geometric panel
app/auth/reset-password/page.tsx Adds reset password page
app/auth/profile-setup/page.tsx Restyles profile setup and swaps to Bauhaus UI components
app/auth/forgot-password/page.tsx Adds forgot password page
app/auth/actions.ts Adds shared sign-out server action
app/api/leads/route.ts Adds IP-based rate limiting to lead submission
app/api/cron/expire-packages/route.ts Adds cron endpoint to expire packages
app/admin/tutors/page.tsx Restyles tutors directory page
app/admin/tutors/actions.ts Refactors to shared requireAdmin
app/admin/tutors/[id]/page.tsx Restyles tutor detail page
app/admin/tutors/TutorFilters.tsx Restyles tutor filters
app/admin/tutors/TutorActions.tsx Restyles approve/revoke actions
app/admin/subjects/page.tsx Adds admin subjects management page
app/admin/subjects/actions.ts Adds subject add/toggle actions
app/admin/subjects/AddSubjectForm.tsx Adds subject creation form
app/admin/sessions/page.tsx Adds pagination + restyles admin sessions list
app/admin/requests/actions.ts Refactors to shared requireAdmin + adds new admin request actions
app/admin/requests/[id]/AdminRequestActions.tsx Adds admin status/cancel controls UI
app/admin/requests/RequestFilters.tsx Restyles request filters
app/admin/payments/actions.ts Refactors to shared requireAdmin + validates inputs with zod
app/admin/payments/ViewProofButton.tsx Adds modal proof viewer with signed URL fetch
app/admin/payments/PaymentQuickActions.tsx Adds compact WA + copy-message quick actions
app/admin/payments/PaymentActions.tsx Restyles payment action forms
app/admin/page.tsx Adds admin dashboard counts + refreshed card styling
app/admin/loading.tsx Adds admin loading skeleton UI
app/admin/layout.tsx Restyles admin layout + server-action sign out
app/admin/error.tsx Adds admin route error UI
app/admin/audit/page.tsx Restyles audit log
app/admin/actions.ts Refactors to shared requireAdmin and removes duplicate signOut
.env.example Documents new env vars (bank, cron, Sentry)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +6
'use server'

import { createAdminClient } from '@/lib/supabase/admin'
import { revalidatePath } from 'next/cache'
import { z } from 'zod'

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addSubject and toggleSubjectActive use the service-role admin client but do not call requireAdmin(). Server actions can be invoked directly (bypassing the layout redirect), so these mutations need an explicit admin guard (and ideally audit logging) to prevent unauthorized subject changes.

Copilot uses AI. Check for mistakes.
Comment thread components/ui/tooltip.tsx
Comment on lines +3 to +6
import * as React from "react"
import { Tooltip as TooltipPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Radix import shape here appears incorrect for the usage (TooltipPrimitive.Provider, .Root, etc.), and the Tailwind class origin-(--radix-tooltip-content-transform-origin) is invalid syntax (Tailwind expects bracket form). This will likely fail at build time; switch to the standard Radix Tooltip module import pattern and correct the Tailwind arbitrary value syntax.

Copilot uses AI. Check for mistakes.
Comment thread components/ui/tooltip.tsx
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Radix import shape here appears incorrect for the usage (TooltipPrimitive.Provider, .Root, etc.), and the Tailwind class origin-(--radix-tooltip-content-transform-origin) is invalid syntax (Tailwind expects bracket form). This will likely fail at build time; switch to the standard Radix Tooltip module import pattern and correct the Tailwind arbitrary value syntax.

Copilot uses AI. Check for mistakes.
Comment thread components/ui/tabs.tsx

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "radix-ui"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TabsPrimitive.Root/List/Trigger/Content usage requires a namespace-style Radix import; importing { Tabs as TabsPrimitive } is unlikely to provide those members and will break compilation/runtime. Use the standard Radix Tabs package import that matches the API surface you’re calling.

Suggested change
import { Tabs as TabsPrimitive } from "radix-ui"
import * as TabsPrimitive from "@radix-ui/react-tabs"

Copilot uses AI. Check for mistakes.
Comment thread components/ui/form.tsx
Comment on lines +4 to +5
import type { Label as LabelPrimitive } from "radix-ui"
import { Slot } from "radix-ui"
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LabelPrimitive is imported as a type-only symbol, but it’s referenced via typeof LabelPrimitive.Root, which requires a runtime value. This will fail type-checking; import the Radix Label primitive as a value (from the correct Radix package) or change the prop type so it doesn’t rely on typeof against a type-only import.

Suggested change
import type { Label as LabelPrimitive } from "radix-ui"
import { Slot } from "radix-ui"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"

Copilot uses AI. Check for mistakes.

return (
<form action={formAction} className="mt-2 space-y-2 rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-800/50">
<form action={formAction} className="mt-2 space-y-2 border border-[#D0D0D0] bg-[#F0F0F0] p-3 /50">
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The className includes a stray /50 token at the end (and double spaces), which isn’t a valid Tailwind utility and will be ignored (or potentially flagged by class linting). Remove the stray token and normalize the class list.

Suggested change
<form action={formAction} className="mt-2 space-y-2 border border-[#D0D0D0] bg-[#F0F0F0] p-3 /50">
<form action={formAction} className="mt-2 space-y-2 border border-[#D0D0D0] bg-[#F0F0F0] p-3">

Copilot uses AI. Check for mistakes.
Comment thread lib/services/requests.ts
.from("requests")
.select(
`
id, level, subject_id, status, goals, availability, timezone,
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request validator/schema uses availability_windows, but this admin fetch selects availability. If the underlying DB column is now availability_windows (or the UI expects that field), this will return the wrong shape/empty data. Align the selected column name with the updated schema and downstream consumers.

Suggested change
id, level, subject_id, status, goals, availability, timezone,
id, level, subject_id, status, goals, availability_windows, timezone,

Copilot uses AI. Check for mistakes.
Comment thread middleware.ts
Comment on lines +72 to +97
// Fetch primary_role from user_profiles for role-based route protection
const { data: profile } = await supabase
.from('user_profiles')
.select('primary_role')
.eq('user_id', user.id)
.single()

const primaryRole = profile?.primary_role

// Redirect non-admin users away from /admin/*
if (pathname.startsWith('/admin') && primaryRole !== 'admin') {
const url = request.nextUrl.clone()
url.pathname = '/dashboard'
return NextResponse.redirect(url)
}

// Redirect non-tutor users away from /tutor/*
// (admins are allowed to access tutor routes per existing layout logic)
if (
pathname.startsWith('/tutor') &&
primaryRole !== 'tutor' &&
primaryRole !== 'admin'
) {
const url = request.nextUrl.clone()
url.pathname = '/dashboard'
return NextResponse.redirect(url)
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds a database round-trip on every protected request (including non-admin/non-tutor pages). To reduce middleware latency/cost, consider only querying user_profiles when pathname is under /admin or /tutor, or move role info into a token claim / cached session metadata so middleware can decide without a DB call.

Suggested change
// Fetch primary_role from user_profiles for role-based route protection
const { data: profile } = await supabase
.from('user_profiles')
.select('primary_role')
.eq('user_id', user.id)
.single()
const primaryRole = profile?.primary_role
// Redirect non-admin users away from /admin/*
if (pathname.startsWith('/admin') && primaryRole !== 'admin') {
const url = request.nextUrl.clone()
url.pathname = '/dashboard'
return NextResponse.redirect(url)
}
// Redirect non-tutor users away from /tutor/*
// (admins are allowed to access tutor routes per existing layout logic)
if (
pathname.startsWith('/tutor') &&
primaryRole !== 'tutor' &&
primaryRole !== 'admin'
) {
const url = request.nextUrl.clone()
url.pathname = '/dashboard'
return NextResponse.redirect(url)
const isRoleProtectedPath =
pathname.startsWith('/admin') || pathname.startsWith('/tutor')
if (isRoleProtectedPath) {
// Fetch primary_role from user_profiles for role-based route protection
const { data: profile } = await supabase
.from('user_profiles')
.select('primary_role')
.eq('user_id', user.id)
.single()
const primaryRole = profile?.primary_role
// Redirect non-admin users away from /admin/*
if (pathname.startsWith('/admin') && primaryRole !== 'admin') {
const url = request.nextUrl.clone()
url.pathname = '/dashboard'
return NextResponse.redirect(url)
}
// Redirect non-tutor users away from /tutor/*
// (admins are allowed to access tutor routes per existing layout logic)
if (
pathname.startsWith('/tutor') &&
primaryRole !== 'tutor' &&
primaryRole !== 'admin'
) {
const url = request.nextUrl.clone()
url.pathname = '/dashboard'
return NextResponse.redirect(url)
}

Copilot uses AI. Check for mistakes.
Comment thread app/error.tsx
Comment on lines +21 to +23
<p className="mb-6 text-sm text-[#121212]/60">
{error.message || 'An unexpected error occurred. Please try again.'}
</p>
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rendering error.message directly to end users can leak internal details (stack-derived messages, DB errors, etc.). Consider showing a generic message in production and logging the detailed error server-side (or gating the detailed message behind NODE_ENV !== 'production').

Copilot uses AI. Check for mistakes.
Comment thread next.config.ts
Comment on lines +5 to +34
return [
{
// Apply security headers to all routes
source: "/(.*)",
headers: [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "X-DNS-Prefetch-Control",
value: "on",
},
],
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HSTS should generally only be sent when you’re certain the site is always served over HTTPS (typically production only). Consider conditionally adding this header based on environment/deployment to avoid unintended behavior during local/dev or non-HTTPS environments.

Suggested change
return [
{
// Apply security headers to all routes
source: "/(.*)",
headers: [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "X-DNS-Prefetch-Control",
value: "on",
},
],
const securityHeaders = [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Permissions-Policy",
value: "camera=(), microphone=(), geolocation=()",
},
{
key: "X-DNS-Prefetch-Control",
value: "on",
},
];
if (process.env.NODE_ENV === "production") {
securityHeaders.push({
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
});
}
return [
{
// Apply security headers to all routes
source: "/(.*)",
headers: securityHeaders,

Copilot uses AI. Check for mistakes.
@Taleef7 Taleef7 merged commit f5473c4 into main Mar 4, 2026
1 check failed
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.

2 participants