mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-11 22:54:45 -05:00
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
This commit is contained in:
@@ -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 <div>Loading...</div>;
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Edit Study"
|
||||
description="Loading study details..."
|
||||
/>
|
||||
<PageContent className="max-w-2xl">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Study Details</CardTitle>
|
||||
<CardDescription>
|
||||
Please wait while we load the study information.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardSkeleton />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!study) {
|
||||
|
||||
@@ -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 <div>Loading...</div>;
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Loading..."
|
||||
description="Please wait while we load the study details"
|
||||
/>
|
||||
<PageContent>
|
||||
<Tabs defaultValue="overview" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="participants">Participants</TabsTrigger>
|
||||
<TabsTrigger value="members">Members</TabsTrigger>
|
||||
<TabsTrigger value="metadata">Metadata</TabsTrigger>
|
||||
<TabsTrigger value="activity">Activity</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="overview">
|
||||
<StudyDetailsSkeleton />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!study) {
|
||||
|
||||
@@ -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 <div>Loading...</div>;
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
title="Edit Participant"
|
||||
description="Loading participant information..."
|
||||
/>
|
||||
<PageContent>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Participant Details</CardTitle>
|
||||
<CardDescription>
|
||||
Please wait while we load the participant information.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardSkeleton />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!study || !participant) {
|
||||
|
||||
@@ -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 <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
@@ -31,8 +28,9 @@ export default function StudiesPage() {
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<PageContent>
|
||||
<div className="grid gap-6">
|
||||
{!studies || studies.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<StudyListSkeleton />
|
||||
) : !studies || studies.length === 0 ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>No Studies</CardTitle>
|
||||
@@ -42,7 +40,8 @@ export default function StudiesPage() {
|
||||
</CardHeader>
|
||||
</Card>
|
||||
) : (
|
||||
studies.map((study) => (
|
||||
<div className="grid gap-6">
|
||||
{studies.map((study) => (
|
||||
<Card
|
||||
key={study.id}
|
||||
className="hover:bg-muted/50 cursor-pointer transition-colors"
|
||||
@@ -60,9 +59,9 @@ export default function StudiesPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</PageContent>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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 <div>Loading...</div>;
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Study Participants</CardTitle>
|
||||
<CardDescription>Loading participants...</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TableSkeleton />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-primary/10", className)}
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
function TableRowSkeleton() {
|
||||
return (
|
||||
<div className="flex space-x-4 p-4">
|
||||
<Skeleton className="h-5 w-[20%]" />
|
||||
<Skeleton className="h-5 w-[15%]" />
|
||||
<Skeleton className="h-5 w-[30%]" />
|
||||
<Skeleton className="h-5 w-[35%]" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableSkeleton() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex space-x-4 p-4 border-b">
|
||||
<Skeleton className="h-4 w-[20%]" />
|
||||
<Skeleton className="h-4 w-[15%]" />
|
||||
<Skeleton className="h-4 w-[30%]" />
|
||||
<Skeleton className="h-4 w-[35%]" />
|
||||
</div>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<TableRowSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CardSkeleton() {
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<Skeleton className="h-7 w-[40%]" />
|
||||
<Skeleton className="h-4 w-[60%]" />
|
||||
<div className="pt-4">
|
||||
<Skeleton className="h-4 w-[25%]" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StudyListSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="rounded-lg border bg-card">
|
||||
<CardSkeleton />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StudyDetailsSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Overview Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-7 w-[150px] mb-2" />
|
||||
<Skeleton className="h-4 w-[250px]" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<Skeleton className="h-4 w-[100px] mb-1" />
|
||||
<Skeleton className="h-5 w-[150px]" />
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton className="h-4 w-[100px] mb-1" />
|
||||
<Skeleton className="h-5 w-[200px]" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Skeleton className="h-4 w-[120px]" />
|
||||
<Skeleton className="h-4 w-4" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-7 w-[60px] mb-1" />
|
||||
<Skeleton className="h-4 w-[100px]" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Activity Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-7 w-[150px] mb-2" />
|
||||
<Skeleton className="h-4 w-[200px]" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="flex gap-4">
|
||||
<Skeleton className="h-8 w-8 rounded-full" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-[150px]" />
|
||||
<Skeleton className="h-4 w-[250px]" />
|
||||
<Skeleton className="h-3 w-[100px]" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Skeleton,
|
||||
TableRowSkeleton,
|
||||
TableSkeleton,
|
||||
CardSkeleton,
|
||||
StudyListSkeleton,
|
||||
StudyDetailsSkeleton,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user