diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc28a8..8c0f973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,15 @@ This doc captures the main issues encountered while setting up/running the app l - **20th session:** Synced Students dojo filter to URL and reset pagination on filter change (with global navigation loader). - **20th session:** Centralized profile display-name derivation for OAuth and improved profile auto-create/update behavior (role + full_name). +- **21st session:** Added shimmer-based `Skeleton` UI primitive and global CSS animation for consistent skeleton loading states. +- **21st session:** Replaced the dashboard root loading screen with a skeleton layout matching the home bento + list surfaces. +- **21st session:** Added route-level skeleton `loading.tsx` for Entries, Events, Events Browser, and Students. + +- **22nd session:** Improved coach “Register athletes” dialog sizing/scrolling to work better on mobile and small viewports. +- **22nd session:** Made coach register filters/action bar responsive (full-width controls, horizontal scroll where needed) and allowed table horizontal scroll. +- **22nd session:** Tuned dashboard mobile header stacking (z-index/borders) and reduced mobile drawer width for better content context. +- **22nd session:** Minor copy polish on dashboard Students metric (“Total Athletes”). + ## 1) Supabase migration error: `must be owner of table users` **Symptom** @@ -4118,3 +4127,90 @@ This session focused on making student management “dojo-aware” end-to-end (d - `src/lib/auth/profile.ts` - `src/lib/auth/require-role.ts` - `src/app/dashboard/events-browser/actions/index.ts` + +--- + +# Session 21 — Skeleton Loading States + Shimmer UI (2026-02-26) + +This session focused on improving perceived performance by using skeleton placeholders (instead of blank/spinners) across key dashboard routes. + +## 1) Added a `Skeleton` primitive + shimmer animation + +**Change** +- Added a reusable `Skeleton` component backed by a `.shimmer` utility class. +- Added `.shimmer` styles + keyframes in global CSS for both light and dark theme. + +**Where** +- `src/components/ui/skeleton.tsx` +- `src/app/globals.css` + +--- + +## 2) Dashboard root loading now matches dashboard layout + +**Change** +- Replaced the previous dashboard loading component with a structured skeleton: page header, bento-style cards, and list rows. + +**Where** +- `src/app/dashboard/loading.tsx` + +--- + +## 3) Added route-level loading skeletons for major dashboard pages + +**Change** +- Added dedicated route-level `loading.tsx` skeletons so each section shows an immediate, layout-accurate placeholder while server components stream. + +**Where** +- `src/app/dashboard/entries/loading.tsx` +- `src/app/dashboard/events/loading.tsx` +- `src/app/dashboard/events-browser/loading.tsx` +- `src/app/dashboard/students/loading.tsx` + +--- + +# Session 22 — Coach Register UX + Mobile Nav Polish (2026-02-26) + +This session focused on tightening the coach registration UX on small screens and polishing the dashboard’s mobile navigation/header layering. + +## 1) Coach “Register athletes” dialog is viewport-friendly + +**Change** +- Updated the register dialog to use viewport-aware width and max-height, with scrolling enabled so the form stays usable on smaller devices. + +**Where** +- `src/components/coach/coach-dashboard.tsx` + +--- + +## 2) Coach register filters/action bar/table are responsive + +**Change** +- Filters bar now stacks nicely on mobile and uses full-width inputs/selects. +- Action controls (event day + participation + add) handle narrow screens via horizontal scrolling and tighter button copy. +- Table container now supports horizontal scrolling with a sensible minimum table width. + +**Where** +- `src/components/coach/coach-student-register.tsx` + +--- + +## 3) Mobile nav + header layering polish + +**Change** +- Reduced mobile drawer width so it doesn’t fully cover the screen on small devices. +- Increased mobile header z-index and refined border/background styles to prevent overlay/clipping issues. + +**Where** +- `src/components/dashboard/mobile-nav.tsx` +- `src/components/dashboard/responsive-dashboard-frame.tsx` + +--- + +## 4) Small dashboard copy improvement + +**Change** +- Renamed Students metric helper text from “Total registered” → “Total Athletes”. + +**Where** +- `src/app/dashboard/page.tsx` diff --git a/src/app/dashboard/entries/loading.tsx b/src/app/dashboard/entries/loading.tsx new file mode 100644 index 0000000..8215584 --- /dev/null +++ b/src/app/dashboard/entries/loading.tsx @@ -0,0 +1,74 @@ +import { Skeleton } from '@/components/ui/skeleton' + +export default function EntriesLoading() { + return ( +
+ {/* Header section */} +
+
+
+
+ +
+ +
+ +
+
+ + {/* Stats Overview */} +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+ + {/* Entries List Area */} +
+
+
+ + +
+
+ + + + +
+
+ + {/* Table Skeleton */} +
+
+ + +
+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+ +
+ +
+ + +
+
+
+
+ + +
+
+ ))} +
+
+
+
+ ) +} diff --git a/src/app/dashboard/events-browser/loading.tsx b/src/app/dashboard/events-browser/loading.tsx new file mode 100644 index 0000000..9571c98 --- /dev/null +++ b/src/app/dashboard/events-browser/loading.tsx @@ -0,0 +1,69 @@ +import { Skeleton } from '@/components/ui/skeleton' + +export default function EventBrowserLoading() { + return ( +
+
+ + +
+ +
+ {/* Approved Events */} +
+
+ + +
+
+
+ {Array.from({ length: 2 }).map((_, i) => ( +
+
+ +
+
+ + +
+ +
+
+ +
+ ))} +
+
+
+ + {/* Active Events */} +
+
+ + +
+
+
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+ +
+
+ + +
+ +
+
+ +
+ ))} +
+
+
+ +
+
+ ) +} diff --git a/src/app/dashboard/events/loading.tsx b/src/app/dashboard/events/loading.tsx new file mode 100644 index 0000000..e9411a4 --- /dev/null +++ b/src/app/dashboard/events/loading.tsx @@ -0,0 +1,53 @@ +import { Skeleton } from '@/components/ui/skeleton' + +export default function EventsLoading() { + return ( +
+
+
+ + +
+ +
+ +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+ +
+
+ + +
+ +
+
+ +
+ ))} +
+ +
+
+ + +
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+ +
+ + +
+
+
+ ))} +
+
+
+ ) +} diff --git a/src/app/dashboard/loading.tsx b/src/app/dashboard/loading.tsx index c464a22..48444e6 100644 --- a/src/app/dashboard/loading.tsx +++ b/src/app/dashboard/loading.tsx @@ -1,9 +1,45 @@ -import { DashboardLoading } from '@/components/dashboard/dashboard-loading' +import { Skeleton } from '@/components/ui/skeleton' -export default function Loading() { - return ( -
- +export default function DashboardLoading() { + return ( +
+
+ + +
+ + {/* Bento Grid Skeleton */} +
+ + + + +
+ +
+
+ + +
+ +
+ {Array.from({ length: 4 }).map((_, i) => ( +
+
+ +
+
+ + +
+ +
+
+ +
+ ))}
- ) +
+
+ ) } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 1640e3f..b2cbae2 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -293,7 +293,7 @@ export default async function DashboardPage() { Students
{studentsCount ?? 0}
-
Total registered
+
Total Athletes
diff --git a/src/app/dashboard/students/loading.tsx b/src/app/dashboard/students/loading.tsx new file mode 100644 index 0000000..d43b1eb --- /dev/null +++ b/src/app/dashboard/students/loading.tsx @@ -0,0 +1,52 @@ +import { Skeleton } from '@/components/ui/skeleton' + +export default function StudentsLoading() { + return ( +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ +
+ {/* Header row */} +
+ + + + +
+ {/* Rows */} + {Array.from({ length: 8 }).map((_, i) => ( +
+ + + + +
+ ))} +
+
+ +
+ + + +
+
+
+
+ ) +} diff --git a/src/app/globals.css b/src/app/globals.css index 6eb9479..f96c6dc 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -11,42 +11,36 @@ html { :root { /* Green / White / Black Theme - Light Mode */ - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; + --card: hsl(0, 0%, 100%); + --card-foreground: hsl(222.2, 84%, 4.9%); - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; + --popover: hsl(0, 0%, 100%); + --popover-foreground: hsl(222.2, 84%, 4.9%); /* EntryDesk Emerald Primary */ --primary: oklch(0.508 0.118 165.612); /* emerald-700 */ --primary-foreground: oklch(1 0 0); - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; + --secondary: hsl(210, 40%, 96.1%); + --secondary-foreground: hsl(222.2, 47.4%, 11.2%); - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted: hsl(210, 40%, 96.1%); + --muted-foreground: hsl(215.4, 16.3%, 46.9%); - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; + --accent: hsl(210, 40%, 96.1%); + --accent-foreground: hsl(222.2, 47.4%, 11.2%); - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; + --destructive: hsl(0, 84.2%, 60.2%); + --destructive-foreground: hsl(210, 40%, 98%); - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 222.2 84% 4.9%; + --border: hsl(214.3, 31.8%, 91.4%); + --input: hsl(214.3, 31.8%, 91.4%); + --ring: hsl(222.2, 84%, 4.9%); --radius: 1rem; /* Light Mode Background: Slate 50 (#f8fafc) */ - --background: oklch(0.985 0.002 247.839); - /* Using OKLCH approximation of #f8fafc or just white-ish */ - /* Actually, let's use the Hex directly in the theme mapping, but variables here usually take HSL or OKLCH in shadcn */ - /* However, Tailwind v4 can handle raw values. I'll stick to a clean separate mapping if I can. */ - /* Let's use HSL for these standard ones to be safe or oklch if preferred. */ - /* Slate 50 is approx 210 40% 98% in HSL */ --background: oklch(0.984 0.003 247.858); --foreground: oklch(0.129 0.042 264.695); @@ -88,21 +82,21 @@ html { /* emerald-500 */ --primary-foreground: oklch(0 0 0); - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; + --secondary: hsl(217.2, 32.6%, 17.5%); + --secondary-foreground: hsl(210, 40%, 98%); - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 65.1%; + --muted: hsl(217.2, 32.6%, 17.5%); + --muted-foreground: hsl(215, 20.2%, 65.1%); - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; + --accent: hsl(217.2, 32.6%, 17.5%); + --accent-foreground: hsl(210, 40%, 98%); - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; + --destructive: hsl(0, 62.8%, 30.6%); + --destructive-foreground: hsl(210, 40%, 98%); - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 212.7 26.8% 83.9%; + --border: hsl(217.2, 32.6%, 17.5%); + --input: hsl(217.2, 32.6%, 17.5%); + --ring: hsl(212.7, 26.8%, 83.9%); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); @@ -301,6 +295,45 @@ body { box-shadow: 0 14px 32px -24px rgb(0 0 0 / 0.55); } + .shimmer { + position: relative; + overflow: hidden; + background-color: rgb(0 0 0 / 0.06); + } + .dark .shimmer { + background-color: rgb(255 255 255 / 0.05); + } + .shimmer::after { + content: ""; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + transform: translateX(-100%); + background-image: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.6) 50%, + transparent 100% + ); + animation: shimmer 1.5s infinite; + z-index: 10; + } + .dark .shimmer::after { + background-image: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.05) 50%, + transparent 100% + ); + } + @keyframes shimmer { + 100% { + transform: translateX(100%); + } + } + .dashboard-shell .dashboard-list>*+* { border-top: 1px solid color-mix(in oklab, var(--color-border) 70%, transparent); } diff --git a/src/components/coach/coach-dashboard.tsx b/src/components/coach/coach-dashboard.tsx index 85fce33..617451f 100644 --- a/src/components/coach/coach-dashboard.tsx +++ b/src/components/coach/coach-dashboard.tsx @@ -85,7 +85,7 @@ export function CoachDashboard({ event, stats, entries, students, eventDays, doj {!isPastEvent && ( - + Register athletes diff --git a/src/components/coach/coach-student-register.tsx b/src/components/coach/coach-student-register.tsx index d490283..1c8ac88 100644 --- a/src/components/coach/coach-student-register.tsx +++ b/src/components/coach/coach-student-register.tsx @@ -141,18 +141,19 @@ export function CoachStudentRegister({ students, existingStudentIds, eventId, ev /> {/* Filters Bar */} -
- - - setSearchQuery(e.target.value)} - /> +
+
+ + setSearchQuery(e.target.value)} + /> +
- - +
+
+ {/* Event Day Selector (Conditional) */} + {eventDays.length > 0 && ( + + )} + + - )} - - - + +
-
- +
+
diff --git a/src/components/dashboard/mobile-nav.tsx b/src/components/dashboard/mobile-nav.tsx index b88d1dd..1b6d0b4 100644 --- a/src/components/dashboard/mobile-nav.tsx +++ b/src/components/dashboard/mobile-nav.tsx @@ -50,7 +50,7 @@ export function MobileNav({ role, profile, userEmail }: MobileNavProps) { {/* ✅ Required for Radix accessibility */} diff --git a/src/components/dashboard/responsive-dashboard-frame.tsx b/src/components/dashboard/responsive-dashboard-frame.tsx index 9773a8f..a8f6a5b 100644 --- a/src/components/dashboard/responsive-dashboard-frame.tsx +++ b/src/components/dashboard/responsive-dashboard-frame.tsx @@ -152,7 +152,7 @@ export function ResponsiveDashboardFrame({
-
+
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..1ee0684 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton }