From 672a96423f9e55d9a16feb7b6761454419a159e8 Mon Sep 17 00:00:00 2001 From: Taleef7 Date: Tue, 3 Mar 2026 16:33:33 -0500 Subject: [PATCH 01/20] infra: generate database types and type all Supabase clients (E1+E2) - 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() in client.ts - Add Database generic to createServerClient() 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() 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 --- lib/supabase/admin.ts | 3 +- lib/supabase/client.ts | 3 +- lib/supabase/database.types.ts | 753 +++++++++++++++++++++++++++++++++ lib/supabase/server.ts | 33 +- tsconfig.json | 3 +- 5 files changed, 779 insertions(+), 16 deletions(-) create mode 100644 lib/supabase/database.types.ts diff --git a/lib/supabase/admin.ts b/lib/supabase/admin.ts index 64700b6..1c8db3d 100644 --- a/lib/supabase/admin.ts +++ b/lib/supabase/admin.ts @@ -1,5 +1,6 @@ import 'server-only' import { createClient } from '@supabase/supabase-js' +import type { Database } from './database.types' export function createAdminClient() { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL @@ -11,5 +12,5 @@ export function createAdminClient() { ) } - return createClient(supabaseUrl, serviceRoleKey) + return createClient(supabaseUrl, serviceRoleKey) } diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts index 61c52bb..dab3fb2 100644 --- a/lib/supabase/client.ts +++ b/lib/supabase/client.ts @@ -1,4 +1,5 @@ import { createBrowserClient } from '@supabase/ssr' +import type { Database } from './database.types' export function createClient() { const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL @@ -10,5 +11,5 @@ export function createClient() { ) } - return createBrowserClient(supabaseUrl, supabaseAnonKey) + return createBrowserClient(supabaseUrl, supabaseAnonKey) } diff --git a/lib/supabase/database.types.ts b/lib/supabase/database.types.ts new file mode 100644 index 0000000..057e1a8 --- /dev/null +++ b/lib/supabase/database.types.ts @@ -0,0 +1,753 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json | undefined } + | Json[] + +export type Database = { + public: { + Tables: { + audit_logs: { + Row: { + id: string + actor_user_id: string | null + action: string + entity_type: string + entity_id: string | null + details: Json | null + created_at: string + } + Insert: { + id?: string + actor_user_id?: string | null + action: string + entity_type: string + entity_id?: string | null + details?: Json | null + created_at?: string + } + Update: { + id?: string + actor_user_id?: string | null + action?: string + entity_type?: string + entity_id?: string | null + details?: Json | null + created_at?: string + } + Relationships: [ + { + foreignKeyName: "audit_logs_actor_user_id_fkey" + columns: ["actor_user_id"] + isOneToOne: false + referencedRelation: "user_profiles" + referencedColumns: ["user_id"] + }, + ] + } + leads: { + Row: { + id: string + full_name: string + whatsapp_number: string + role: string + child_name: string | null + level: string + subject: string + exam_board: string + availability: string + city_timezone: string + goals: string | null + preferred_package: string | null + status: string + admin_notes: string | null + created_at: string + updated_at: string + } + Insert: { + id?: string + full_name: string + whatsapp_number: string + role: string + child_name?: string | null + level: string + subject: string + exam_board?: string + availability: string + city_timezone: string + goals?: string | null + preferred_package?: string | null + status?: string + admin_notes?: string | null + created_at?: string + updated_at?: string + } + Update: { + id?: string + full_name?: string + whatsapp_number?: string + role?: string + child_name?: string | null + level?: string + subject?: string + exam_board?: string + availability?: string + city_timezone?: string + goals?: string | null + preferred_package?: string | null + status?: string + admin_notes?: string | null + created_at?: string + updated_at?: string + } + Relationships: [] + } + matches: { + Row: { + id: string + request_id: string + tutor_user_id: string + status: Database["public"]["Enums"]["match_status_enum"] + meet_link: string | null + schedule_pattern: Json | null + assigned_by_user_id: string | null + assigned_at: string + created_at: string + updated_at: string + admin_notes: string | null + } + Insert: { + id?: string + request_id: string + tutor_user_id: string + status?: Database["public"]["Enums"]["match_status_enum"] + meet_link?: string | null + schedule_pattern?: Json | null + assigned_by_user_id?: string | null + assigned_at?: string + created_at?: string + updated_at?: string + admin_notes?: string | null + } + Update: { + id?: string + request_id?: string + tutor_user_id?: string + status?: Database["public"]["Enums"]["match_status_enum"] + meet_link?: string | null + schedule_pattern?: Json | null + assigned_by_user_id?: string | null + assigned_at?: string + created_at?: string + updated_at?: string + admin_notes?: string | null + } + Relationships: [ + { + foreignKeyName: "matches_assigned_by_user_id_fkey" + columns: ["assigned_by_user_id"] + isOneToOne: false + referencedRelation: "user_profiles" + referencedColumns: ["user_id"] + }, + { + foreignKeyName: "matches_request_id_fkey" + columns: ["request_id"] + isOneToOne: true + referencedRelation: "requests" + referencedColumns: ["id"] + }, + { + foreignKeyName: "matches_tutor_user_id_fkey" + columns: ["tutor_user_id"] + isOneToOne: false + referencedRelation: "tutor_profiles" + referencedColumns: ["tutor_user_id"] + }, + ] + } + packages: { + Row: { + id: string + request_id: string + tier_sessions: number + start_date: string + end_date: string + sessions_total: number + sessions_used: number + status: Database["public"]["Enums"]["package_status_enum"] + created_at: string + updated_at: string + } + Insert: { + id?: string + request_id: string + tier_sessions: number + start_date: string + end_date: string + sessions_total: number + sessions_used?: number + status?: Database["public"]["Enums"]["package_status_enum"] + created_at?: string + updated_at?: string + } + Update: { + id?: string + request_id?: string + tier_sessions?: number + start_date?: string + end_date?: string + sessions_total?: number + sessions_used?: number + status?: Database["public"]["Enums"]["package_status_enum"] + created_at?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "packages_request_id_fkey" + columns: ["request_id"] + isOneToOne: false + referencedRelation: "requests" + referencedColumns: ["id"] + }, + ] + } + payments: { + Row: { + id: string + package_id: string + payer_user_id: string + amount_pkr: number + method: string + reference: string | null + proof_path: string | null + status: Database["public"]["Enums"]["payment_status_enum"] + rejection_note: string | null + verified_by_user_id: string | null + verified_at: string | null + created_at: string + updated_at: string + } + Insert: { + id?: string + package_id: string + payer_user_id: string + amount_pkr: number + method?: string + reference?: string | null + proof_path?: string | null + status?: Database["public"]["Enums"]["payment_status_enum"] + rejection_note?: string | null + verified_by_user_id?: string | null + verified_at?: string | null + created_at?: string + updated_at?: string + } + Update: { + id?: string + package_id?: string + payer_user_id?: string + amount_pkr?: number + method?: string + reference?: string | null + proof_path?: string | null + status?: Database["public"]["Enums"]["payment_status_enum"] + rejection_note?: string | null + verified_by_user_id?: string | null + verified_at?: string | null + created_at?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "payments_package_id_fkey" + columns: ["package_id"] + isOneToOne: false + referencedRelation: "packages" + referencedColumns: ["id"] + }, + { + foreignKeyName: "payments_payer_user_id_fkey" + columns: ["payer_user_id"] + isOneToOne: false + referencedRelation: "user_profiles" + referencedColumns: ["user_id"] + }, + { + foreignKeyName: "payments_verified_by_user_id_fkey" + columns: ["verified_by_user_id"] + isOneToOne: false + referencedRelation: "user_profiles" + referencedColumns: ["user_id"] + }, + ] + } + requests: { + Row: { + id: string + created_by_user_id: string + requester_role: Database["public"]["Enums"]["role_enum"] + for_student_name: string | null + level: Database["public"]["Enums"]["level_enum"] + subject_id: number + exam_board: Database["public"]["Enums"]["exam_board_enum"] + goals: string | null + timezone: string + availability_windows: Json + preferred_start_date: string | null + status: Database["public"]["Enums"]["request_status_enum"] + preferred_package_tier: number | null + created_at: string + updated_at: string + } + Insert: { + id?: string + created_by_user_id: string + requester_role?: Database["public"]["Enums"]["role_enum"] + for_student_name?: string | null + level: Database["public"]["Enums"]["level_enum"] + subject_id: number + exam_board?: Database["public"]["Enums"]["exam_board_enum"] + goals?: string | null + timezone?: string + availability_windows?: Json + preferred_start_date?: string | null + status?: Database["public"]["Enums"]["request_status_enum"] + preferred_package_tier?: number | null + created_at?: string + updated_at?: string + } + Update: { + id?: string + created_by_user_id?: string + requester_role?: Database["public"]["Enums"]["role_enum"] + for_student_name?: string | null + level?: Database["public"]["Enums"]["level_enum"] + subject_id?: number + exam_board?: Database["public"]["Enums"]["exam_board_enum"] + goals?: string | null + timezone?: string + availability_windows?: Json + preferred_start_date?: string | null + status?: Database["public"]["Enums"]["request_status_enum"] + preferred_package_tier?: number | null + created_at?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "requests_created_by_user_id_fkey" + columns: ["created_by_user_id"] + isOneToOne: false + referencedRelation: "user_profiles" + referencedColumns: ["user_id"] + }, + { + foreignKeyName: "requests_subject_id_fkey" + columns: ["subject_id"] + isOneToOne: false + referencedRelation: "subjects" + referencedColumns: ["id"] + }, + ] + } + sessions: { + Row: { + id: string + match_id: string + scheduled_start_utc: string + scheduled_end_utc: string + status: Database["public"]["Enums"]["session_status_enum"] + tutor_notes: string | null + updated_by_user_id: string | null + updated_at: string + created_at: string + } + Insert: { + id?: string + match_id: string + scheduled_start_utc: string + scheduled_end_utc: string + status?: Database["public"]["Enums"]["session_status_enum"] + tutor_notes?: string | null + updated_by_user_id?: string | null + updated_at?: string + created_at?: string + } + Update: { + id?: string + match_id?: string + scheduled_start_utc?: string + scheduled_end_utc?: string + status?: Database["public"]["Enums"]["session_status_enum"] + tutor_notes?: string | null + updated_by_user_id?: string | null + updated_at?: string + created_at?: string + } + Relationships: [ + { + foreignKeyName: "sessions_match_id_fkey" + columns: ["match_id"] + isOneToOne: false + referencedRelation: "matches" + referencedColumns: ["id"] + }, + { + foreignKeyName: "sessions_updated_by_user_id_fkey" + columns: ["updated_by_user_id"] + isOneToOne: false + referencedRelation: "user_profiles" + referencedColumns: ["user_id"] + }, + ] + } + subjects: { + Row: { + id: number + code: string + name: string + active: boolean + sort_order: number + } + Insert: { + id?: number + code: string + name: string + active?: boolean + sort_order?: number + } + Update: { + id?: number + code?: string + name?: string + active?: boolean + sort_order?: number + } + Relationships: [] + } + tutor_availability: { + Row: { + tutor_user_id: string + windows: Json + updated_at: string + } + Insert: { + tutor_user_id: string + windows?: Json + updated_at?: string + } + Update: { + tutor_user_id?: string + windows?: Json + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "tutor_availability_tutor_user_id_fkey" + columns: ["tutor_user_id"] + isOneToOne: true + referencedRelation: "tutor_profiles" + referencedColumns: ["tutor_user_id"] + }, + ] + } + tutor_profiles: { + Row: { + tutor_user_id: string + approved: boolean + bio: string | null + timezone: string + experience_years: number | null + education: string | null + teaching_approach: string | null + created_at: string + updated_at: string + } + Insert: { + tutor_user_id: string + approved?: boolean + bio?: string | null + timezone?: string + experience_years?: number | null + education?: string | null + teaching_approach?: string | null + created_at?: string + updated_at?: string + } + Update: { + tutor_user_id?: string + approved?: boolean + bio?: string | null + timezone?: string + experience_years?: number | null + education?: string | null + teaching_approach?: string | null + created_at?: string + updated_at?: string + } + Relationships: [ + { + foreignKeyName: "tutor_profiles_tutor_user_id_fkey" + columns: ["tutor_user_id"] + isOneToOne: true + referencedRelation: "user_profiles" + referencedColumns: ["user_id"] + }, + ] + } + tutor_subjects: { + Row: { + tutor_user_id: string + subject_id: number + level: Database["public"]["Enums"]["level_enum"] + } + Insert: { + tutor_user_id: string + subject_id: number + level: Database["public"]["Enums"]["level_enum"] + } + Update: { + tutor_user_id?: string + subject_id?: number + level?: Database["public"]["Enums"]["level_enum"] + } + Relationships: [ + { + foreignKeyName: "tutor_subjects_subject_id_fkey" + columns: ["subject_id"] + isOneToOne: false + referencedRelation: "subjects" + referencedColumns: ["id"] + }, + { + foreignKeyName: "tutor_subjects_tutor_user_id_fkey" + columns: ["tutor_user_id"] + isOneToOne: false + referencedRelation: "tutor_profiles" + referencedColumns: ["tutor_user_id"] + }, + ] + } + user_profiles: { + Row: { + user_id: string + display_name: string + whatsapp_number: string | null + timezone: string + primary_role: Database["public"]["Enums"]["role_enum"] + created_at: string + updated_at: string + } + Insert: { + user_id: string + display_name: string + whatsapp_number?: string | null + timezone?: string + primary_role?: Database["public"]["Enums"]["role_enum"] + created_at?: string + updated_at?: string + } + Update: { + user_id?: string + display_name?: string + whatsapp_number?: string | null + timezone?: string + primary_role?: Database["public"]["Enums"]["role_enum"] + created_at?: string + updated_at?: string + } + Relationships: [] + } + user_roles: { + Row: { + user_id: string + role: Database["public"]["Enums"]["role_enum"] + created_at: string + } + Insert: { + user_id: string + role: Database["public"]["Enums"]["role_enum"] + created_at?: string + } + Update: { + user_id?: string + role?: Database["public"]["Enums"]["role_enum"] + created_at?: string + } + Relationships: [ + { + foreignKeyName: "user_roles_user_id_fkey" + columns: ["user_id"] + isOneToOne: false + referencedRelation: "user_profiles" + referencedColumns: ["user_id"] + }, + ] + } + } + Views: { + [_ in never]: never + } + Functions: { + has_role: { + Args: { + p_uid: string + p_role: Database["public"]["Enums"]["role_enum"] + } + Returns: boolean + } + is_admin: { + Args: { + p_uid: string + } + Returns: boolean + } + is_tutor: { + Args: { + p_uid: string + } + Returns: boolean + } + increment_sessions_used: { + Args: { + p_request_id: string + } + Returns: undefined + } + tutor_update_session: { + Args: { + p_session_id: string + p_status: Database["public"]["Enums"]["session_status_enum"] + p_notes: string + } + Returns: undefined + } + } + Enums: { + exam_board_enum: "cambridge" | "edexcel" | "other" | "unspecified" + level_enum: "o_levels" | "a_levels" + match_status_enum: "matched" | "active" | "paused" | "ended" + package_status_enum: "pending" | "active" | "expired" + payment_status_enum: "pending" | "paid" | "rejected" | "refunded" + request_status_enum: + | "new" + | "payment_pending" + | "ready_to_match" + | "matched" + | "active" + | "paused" + | "ended" + role_enum: "student" | "parent" | "tutor" | "admin" + session_status_enum: + | "scheduled" + | "done" + | "rescheduled" + | "no_show_student" + | "no_show_tutor" + } + CompositeTypes: { + [_ in never]: never + } + } +} + +type PublicSchema = Database[Extract] + +export type Tables< + PublicTableNameOrOptions extends + | keyof (PublicSchema["Tables"] & PublicSchema["Views"]) + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"]) + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & + Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { + Row: infer R + } + ? R + : never + : PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] & + PublicSchema["Views"]) + ? (PublicSchema["Tables"] & + PublicSchema["Views"])[PublicTableNameOrOptions] extends { + Row: infer R + } + ? R + : never + : never + +export type TablesInsert< + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Insert: infer I + } + ? I + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Insert: infer I + } + ? I + : never + : never + +export type TablesUpdate< + PublicTableNameOrOptions extends + | keyof PublicSchema["Tables"] + | { schema: keyof Database }, + TableName extends PublicTableNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"] + : never = never, +> = PublicTableNameOrOptions extends { schema: keyof Database } + ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { + Update: infer U + } + ? U + : never + : PublicTableNameOrOptions extends keyof PublicSchema["Tables"] + ? PublicSchema["Tables"][PublicTableNameOrOptions] extends { + Update: infer U + } + ? U + : never + : never + +export type Enums< + PublicEnumNameOrOptions extends + | keyof PublicSchema["Enums"] + | { schema: keyof Database }, + EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } + ? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"] + : never = never, +> = PublicEnumNameOrOptions extends { schema: keyof Database } + ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] + : PublicEnumNameOrOptions extends keyof PublicSchema["Enums"] + ? PublicSchema["Enums"][PublicEnumNameOrOptions] + : never + +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof PublicSchema["CompositeTypes"] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + : never = never, +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"] + ? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] + : never diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts index 5d7674b..c1270d0 100644 --- a/lib/supabase/server.ts +++ b/lib/supabase/server.ts @@ -1,27 +1,34 @@ -import { createServerClient } from '@supabase/ssr' -import { cookies } from 'next/headers' +import { createServerClient } from "@supabase/ssr"; +import { cookies } from "next/headers"; +import type { Database } from "./database.types"; export async function createClient() { - const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL - const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; if (!supabaseUrl || !supabaseAnonKey) { throw new Error( - 'Missing required environment variables: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY must be set' - ) + "Missing required environment variables: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY must be set", + ); } - const cookieStore = await cookies() - return createServerClient(supabaseUrl, supabaseAnonKey, { + const cookieStore = await cookies(); + return createServerClient(supabaseUrl, supabaseAnonKey, { cookies: { getAll() { - return cookieStore.getAll() + return cookieStore.getAll(); }, setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value, options }) => - cookieStore.set(name, value, options) - ) + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options), + ); + } catch { + // setAll is called from a Server Component where cookie writes are + // not allowed. Safe to ignore — session refresh is handled by the + // middleware / Route Handlers where cookies CAN be written. + } }, }, - }) + }); } diff --git a/tsconfig.json b/tsconfig.json index e83516c..20bc867 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,6 +37,7 @@ "**/*.mts" ], "exclude": [ - "node_modules" + "node_modules", + "e2e" ] } From 5ecbc0c8e778f26c45c65fbadd0f556ac8e4ce17 Mon Sep 17 00:00:00 2001 From: Taleef7 Date: Tue, 3 Mar 2026 16:34:17 -0500 Subject: [PATCH 02/20] feat: apply Bauhaus design system and install shadcn/ui component library 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 (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 --- app/globals.css | 50 +++++ app/layout.tsx | 9 +- components.json | 23 +++ components/ui/alert-dialog.tsx | 196 +++++++++++++++++++ components/ui/badge.tsx | 50 +++++ components/ui/bauhaus.tsx | 326 ++++++++++++++++++++++++++++++++ components/ui/button.tsx | 66 +++++++ components/ui/card.tsx | 92 +++++++++ components/ui/dialog.tsx | 158 ++++++++++++++++ components/ui/dropdown-menu.tsx | 257 +++++++++++++++++++++++++ components/ui/form.tsx | 167 ++++++++++++++++ components/ui/input.tsx | 21 ++ components/ui/label.tsx | 24 +++ components/ui/pagination.tsx | 127 +++++++++++++ components/ui/select.tsx | 190 +++++++++++++++++++ components/ui/separator.tsx | 28 +++ components/ui/skeleton.tsx | 13 ++ components/ui/sonner.tsx | 39 ++++ components/ui/table.tsx | 116 ++++++++++++ components/ui/tabs.tsx | 91 +++++++++ components/ui/tooltip.tsx | 57 ++++++ lib/utils.ts | 6 + 22 files changed, 2105 insertions(+), 1 deletion(-) create mode 100644 components.json create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/bauhaus.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 lib/utils.ts diff --git a/app/globals.css b/app/globals.css index 6e7ed65..3385da5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -16,7 +16,57 @@ --color-foreground: var(--foreground); } +/* ── Base ──────────────────────────────────────────────────────────────── */ +*, *::before, *::after { + box-sizing: border-box; +} + body { background: var(--background); color: var(--foreground); + font-family: 'Outfit', sans-serif; + -webkit-font-smoothing: antialiased; +} + +/* ── Focus ring: accessible + Bauhaus-styled ───────────────────────────── */ +:focus-visible { + outline: 3px solid var(--primary-blue); + outline-offset: 2px; +} + +/* ── Scrollbar (subtle) ────────────────────────────────────────────────── */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: var(--muted); } +::-webkit-scrollbar-thumb { background: #9ca3af; } +::-webkit-scrollbar-thumb:hover { background: var(--foreground); } + +/* ── Bauhaus utility aliases ────────────────────────────────────────────── */ +.bauhaus-card { + background: white; + border: 4px solid #121212; + box-shadow: 8px 8px 0 0 #121212; +} + +.bauhaus-card-sm { + background: white; + border: 2px solid #121212; + box-shadow: 4px 4px 0 0 #121212; +} + +.bauhaus-shadow { + box-shadow: 4px 4px 0 0 #121212; +} + +.bauhaus-shadow-lg { + box-shadow: 8px 8px 0 0 #121212; +} + +/* ── Motion: respect prefers-reduced-motion ─────────────────────────────── */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } } + diff --git a/app/layout.tsx b/app/layout.tsx index 220f3ee..c167458 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata } from "next"; import { Outfit } from "next/font/google"; +import { Toaster } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; import "./globals.css"; const outfit = Outfit({ @@ -21,7 +23,12 @@ export default function RootLayout({ }>) { return ( - {children} + + + {children} + + + ); } diff --git a/components.json b/components.json new file mode 100644 index 0000000..f87021e --- /dev/null +++ b/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..034b93f --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,196 @@ +"use client" + +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: React.ComponentProps & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogAction({ + className, + variant = "default", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + variant = "outline", + size = "default", + ...props +}: React.ComponentProps & + Pick, "variant" | "size">) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, +} diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..0b52efa --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,50 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center border-2 border-[#121212] px-2 py-0.5 text-[11px] font-bold uppercase tracking-wider w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: "bg-[#D02020] text-white border-[#121212]", + secondary: + "bg-[#1040C0] text-white border-[#121212]", + destructive: + "bg-[#D02020] text-white border-[#121212]", + outline: + "bg-white text-[#121212] border-[#121212]", + ghost: "bg-[#E0E0E0] text-[#121212] border-transparent", + link: "text-[#1040C0] underline-offset-4 border-transparent [a&]:hover:underline", + yellow: "bg-[#F0C020] text-[#121212] border-[#121212]", + success: "bg-[#121212] text-white border-[#121212]", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot.Root : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/bauhaus.tsx b/components/ui/bauhaus.tsx new file mode 100644 index 0000000..81b5e5a --- /dev/null +++ b/components/ui/bauhaus.tsx @@ -0,0 +1,326 @@ +/** + * Bauhaus UI Primitives + * ───────────────────── + * Shared atoms for the CorvEd Bauhaus design system. + * Design tokens: + * Canvas: #F0F0F0 (off-white) + * Ink: #121212 (stark black — all borders, text) + * Red: #D02020 (Bauhaus primary CTA / error) + * Blue: #1040C0 (Bauhaus accent / links) + * Yellow: #F0C020 (Bauhaus highlight / tutor) + */ + +import React from 'react' + +// ── Typography ─────────────────────────────────────────────────────────────── + +export function BauhausLabel({ + children, + htmlFor, + required, +}: { + children: React.ReactNode + htmlFor?: string + required?: boolean +}) { + return ( + + ) +} + +// ── Form Controls ───────────────────────────────────────────────────────────── + +export function BauhausInput( + props: React.InputHTMLAttributes & { hasError?: boolean }, +) { + const { hasError, className, ...rest } = props + return ( + + ) +} + +export function BauhausSelect( + props: React.SelectHTMLAttributes & { + children: React.ReactNode + hasError?: boolean + }, +) { + const { hasError, className, children, ...rest } = props + return ( + + ) +} + +export function BauhausFieldError({ message }: { message?: string }) { + if (!message) return null + return ( +

+ {message} +

+ ) +} + +export function BauhausServerError({ message }: { message: string | null }) { + if (!message) return null + return ( +
+
+ ) +} + +// ── Buttons ─────────────────────────────────────────────────────────────────── + +type ButtonVariant = 'red' | 'blue' | 'yellow' | 'outline' | 'ghost' + +interface BauhausButtonProps extends React.ButtonHTMLAttributes { + variant?: ButtonVariant + fullWidth?: boolean + children: React.ReactNode +} + +const BUTTON_VARIANTS: Record = { + red: 'bg-[#D02020] text-white border-2 border-[#121212] shadow-[4px_4px_0px_0px_#121212] hover:bg-[#D02020]/90 active:translate-x-[2px] active:translate-y-[2px] active:shadow-none', + blue: 'bg-[#1040C0] text-white border-2 border-[#121212] shadow-[4px_4px_0px_0px_#121212] hover:bg-[#1040C0]/90 active:translate-x-[2px] active:translate-y-[2px] active:shadow-none', + yellow: + 'bg-[#F0C020] text-[#121212] border-2 border-[#121212] shadow-[4px_4px_0px_0px_#121212] hover:bg-[#F0C020]/90 active:translate-x-[2px] active:translate-y-[2px] active:shadow-none', + outline: + 'bg-white text-[#121212] border-2 border-[#121212] shadow-[4px_4px_0px_0px_#121212] hover:bg-[#F0F0F0] active:translate-x-[2px] active:translate-y-[2px] active:shadow-none', + ghost: 'bg-transparent text-[#121212] border-2 border-transparent hover:border-[#121212] hover:bg-[#F0F0F0]', +} + +export function BauhausButton({ + variant = 'red', + fullWidth, + children, + className, + ...props +}: BauhausButtonProps) { + return ( + + ) +} + +// ── Cards ───────────────────────────────────────────────────────────────────── + +type CardAccent = 'red' | 'blue' | 'yellow' | 'none' + +const CARD_ACCENTS: Record = { + red: 'bg-[#D02020]', + blue: 'bg-[#1040C0]', + yellow: 'bg-[#F0C020]', + none: '', +} + +export function BauhausCard({ + children, + accent = 'none', + className, + hover = false, +}: { + children: React.ReactNode + accent?: CardAccent + className?: string + hover?: boolean +}) { + return ( +
+ {/* Geometric corner decoration */} + {accent !== 'none' && ( +
+ ) +} + +// ── Status Badges ────────────────────────────────────────────────────────────── + +export function BauhausBadge({ + children, + color = 'blue', +}: { + children: React.ReactNode + color?: 'red' | 'blue' | 'yellow' | 'black' +}) { + const colorMap = { + red: 'bg-[#D02020] text-white border-[#D02020]', + blue: 'bg-[#1040C0] text-white border-[#1040C0]', + yellow: 'bg-[#F0C020] text-[#121212] border-[#F0C020]', + black: 'bg-[#121212] text-white border-[#121212]', + } + return ( + + {children} + + ) +} + +// ── Divider ─────────────────────────────────────────────────────────────────── + +export function BauhausDivider({ label }: { label?: string }) { + if (!label) return
+ return ( +
+
+ {label} +
+
+ ) +} + +// ── Info panel (e.g. tips, notes) ───────────────────────────────────────────── + +export function BauhausInfoPanel({ + children, + accent = 'blue', +}: { + children: React.ReactNode + accent?: 'red' | 'blue' | 'yellow' +}) { + const bg = { red: 'bg-[#D02020]/10 border-[#D02020]', blue: 'bg-[#1040C0]/10 border-[#1040C0]', yellow: 'bg-[#F0C020]/40 border-[#F0C020]' } + return ( +
+ {children} +
+ ) +} + +// ── Google SVG logo (reused across auth forms) ──────────────────────────────── + +export function GoogleIcon() { + return ( + + ) +} + +// ── Bauhaus geometric Logo mark ─────────────────────────────────────────────── + +export function BauhausLogo({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) { + const dim = size === 'sm' ? 'h-5 w-5' : size === 'lg' ? 'h-8 w-8' : 'h-6 w-6' + const txt = size === 'sm' ? 'text-base' : size === 'lg' ? 'text-2xl' : 'text-lg' + return ( +
+ {/* Three geometric shapes: circle, square, triangle */} +