From ec4d8db16e3907d5e0b600c31c3dc02fe43986f4 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Wed, 12 Feb 2025 00:03:24 -0500 Subject: [PATCH] feat: Add skeleton loaders for improved loading states in studies and participants pages - Implemented skeleton components for studies list, study details, and participants table - Enhanced loading experience by replacing simple "Loading..." placeholders with detailed skeleton loaders - Created reusable skeleton components in `src/components/ui/skeleton.tsx` - Updated studies and participants pages to use new skeleton loaders --- src/app/dashboard/studies/[id]/edit/page.tsx | 23 +++- src/app/dashboard/studies/[id]/page.tsx | 24 +++- .../[participantId]/edit/page.tsx | 23 +++- src/app/dashboard/studies/page.tsx | 37 +++-- src/components/studies/study-participants.tsx | 17 ++- src/components/ui/skeleton.tsx | 127 +++++++++++++++++- 6 files changed, 226 insertions(+), 25 deletions(-) diff --git a/src/app/dashboard/studies/[id]/edit/page.tsx b/src/app/dashboard/studies/[id]/edit/page.tsx index d47904c..430779f 100644 --- a/src/app/dashboard/studies/[id]/edit/page.tsx +++ b/src/app/dashboard/studies/[id]/edit/page.tsx @@ -7,6 +7,7 @@ import { PageHeader } from "~/components/layout/page-header"; import { PageContent } from "~/components/layout/page-content"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { use } from "react"; +import { CardSkeleton } from "~/components/ui/skeleton"; export default function EditStudyPage({ params }: { params: Promise<{ id: string }> }) { const router = useRouter(); @@ -29,7 +30,27 @@ export default function EditStudyPage({ params }: { params: Promise<{ id: string } if (isLoadingStudy) { - return
Loading...
; + return ( + <> + + + + + Study Details + + Please wait while we load the study information. + + + + + + + + + ); } if (!study) { diff --git a/src/app/dashboard/studies/[id]/page.tsx b/src/app/dashboard/studies/[id]/page.tsx index 855c11d..8025498 100644 --- a/src/app/dashboard/studies/[id]/page.tsx +++ b/src/app/dashboard/studies/[id]/page.tsx @@ -13,6 +13,7 @@ import { StudyParticipants } from "~/components/studies/study-participants"; import { StudyMembers } from "~/components/studies/study-members"; import { StudyMetadata } from "~/components/studies/study-metadata"; import { StudyActivity } from "~/components/studies/study-activity"; +import { StudyDetailsSkeleton } from "~/components/ui/skeleton"; export default function StudyPage({ params }: { params: Promise<{ id: string }> }) { const router = useRouter(); @@ -24,7 +25,28 @@ export default function StudyPage({ params }: { params: Promise<{ id: string }> const { data: study, isLoading: isLoadingStudy } = api.study.getById.useQuery({ id }); if (isLoadingStudy) { - return
Loading...
; + return ( + <> + + + + + Overview + Participants + Members + Metadata + Activity + + + + + + + + ); } if (!study) { diff --git a/src/app/dashboard/studies/[id]/participants/[participantId]/edit/page.tsx b/src/app/dashboard/studies/[id]/participants/[participantId]/edit/page.tsx index 29b13ed..3771daf 100644 --- a/src/app/dashboard/studies/[id]/participants/[participantId]/edit/page.tsx +++ b/src/app/dashboard/studies/[id]/participants/[participantId]/edit/page.tsx @@ -9,6 +9,7 @@ import { ParticipantForm, type ParticipantFormValues } from "~/components/partic import { use } from "react"; import { useToast } from "~/hooks/use-toast"; import { ROLES } from "~/lib/permissions/constants"; +import { CardSkeleton } from "~/components/ui/skeleton"; export default function EditParticipantPage({ params, @@ -50,7 +51,27 @@ export default function EditParticipantPage({ } if (isLoading) { - return
Loading...
; + return ( + <> + + + + + Participant Details + + Please wait while we load the participant information. + + + + + + + + + ); } if (!study || !participant) { diff --git a/src/app/dashboard/studies/page.tsx b/src/app/dashboard/studies/page.tsx index 9386859..02c53c4 100644 --- a/src/app/dashboard/studies/page.tsx +++ b/src/app/dashboard/studies/page.tsx @@ -7,15 +7,12 @@ import { PageContent } from "~/components/layout/page-content"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Plus as PlusIcon } from "lucide-react"; +import { StudyListSkeleton } from "~/components/ui/skeleton"; export default function StudiesPage() { const router = useRouter(); const { data: studies, isLoading } = api.study.getMyStudies.useQuery(); - if (isLoading) { - return
Loading...
; - } - return ( <> -
- {!studies || studies.length === 0 ? ( - - - No Studies - - You haven't created any studies yet. Click the button above to create your first study. - - - - ) : ( - studies.map((study) => ( + {isLoading ? ( + + ) : !studies || studies.length === 0 ? ( + + + No Studies + + You haven't created any studies yet. Click the button above to create your first study. + + + + ) : ( +
+ {studies.map((study) => ( - )) - )} -
+ ))} +
+ )}
); diff --git a/src/components/studies/study-participants.tsx b/src/components/studies/study-participants.tsx index 2035d42..027d1ca 100644 --- a/src/components/studies/study-participants.tsx +++ b/src/components/studies/study-participants.tsx @@ -18,6 +18,7 @@ import { ROLES } from "~/lib/permissions/constants"; import { Switch } from "~/components/ui/switch"; import { Label } from "~/components/ui/label"; import { useState } from "react"; +import { TableSkeleton } from "~/components/ui/skeleton"; interface StudyParticipantsProps { studyId: number; @@ -38,7 +39,21 @@ export function StudyParticipants({ studyId, role }: StudyParticipantsProps) { .includes(role.toLowerCase()); if (isLoading) { - return
Loading...
; + return ( + + +
+
+ Study Participants + Loading participants... +
+
+
+ + + +
+ ); } return ( diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index cd75149..a2d9852 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,4 +1,5 @@ import { cn } from "~/lib/utils" +import { Card, CardContent, CardHeader } from "~/components/ui/card" function Skeleton({ className, @@ -6,10 +7,132 @@ function Skeleton({ }: React.HTMLAttributes) { return (
) } -export { Skeleton } +function TableRowSkeleton() { + return ( +
+ + + + +
+ ) +} + +function TableSkeleton() { + return ( +
+
+ + + + +
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ ) +} + +function CardSkeleton() { + return ( +
+ + +
+ +
+
+ ) +} + +function StudyListSkeleton() { + return ( +
+ {[...Array(3)].map((_, i) => ( +
+ +
+ ))} +
+ ) +} + +function StudyDetailsSkeleton() { + return ( +
+ {/* Overview Card */} + + + + + + +
+
+ + +
+
+ + +
+
+
+
+ + {/* Stats Cards */} +
+ {[...Array(3)].map((_, i) => ( + + + + + + + + + + + ))} +
+ + {/* Activity Card */} + + + + + + +
+ {[...Array(3)].map((_, i) => ( +
+ +
+ + + +
+
+ ))} +
+
+
+
+ ) +} + +export { + Skeleton, + TableRowSkeleton, + TableSkeleton, + CardSkeleton, + StudyListSkeleton, + StudyDetailsSkeleton, +}