Enhance development standards and UI components

- Updated .rules to enforce stricter UI/UX standards, including exclusive use of Lucide icons and consistent patterns for entity view pages.
- Added new UI components for entity views, including headers, sections, and quick actions to improve layout and reusability.
- Refactored existing pages (experiments, participants, studies, trials) to utilize the new entity view components, enhancing consistency across the dashboard.
- Improved accessibility and user experience by implementing loading states and error boundaries in async operations.
- Updated package dependencies to ensure compatibility and performance improvements.

Features:
- Comprehensive guidelines for component reusability and visual consistency.
- Enhanced user interface with new entity view components for better organization and navigation.

Breaking Changes: None - existing functionality remains intact.
This commit is contained in:
2025-08-05 02:36:44 -04:00
parent 7cdc1a2340
commit 544207e9a2
16 changed files with 3643 additions and 1531 deletions

View File

@@ -1,27 +1,41 @@
"use client";
import { formatDistanceToNow } from "date-fns";
import {
ArrowLeft,
BarChart3,
Building,
Calendar,
CheckCircle,
Clock,
Edit,
FileText,
FlaskConical,
Plus,
Settings,
Shield,
Users,
XCircle,
} from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { useEffect, useState } from "react";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
EntityView,
EntityViewHeader,
EntityViewSection,
EntityViewSidebar,
EmptyState,
InfoGrid,
QuickActions,
StatsGrid,
} from "~/components/ui/entity-view";
import { Separator } from "~/components/ui/separator";
import { api } from "~/trpc/server";
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
import { useSession } from "next-auth/react";
import { api } from "~/trpc/react";
interface StudyDetailPageProps {
params: Promise<{
@@ -32,326 +46,281 @@ interface StudyDetailPageProps {
const statusConfig = {
draft: {
label: "Draft",
className: "bg-gray-100 text-gray-800",
icon: "📝",
variant: "secondary" as const,
icon: "FileText" as const,
},
active: {
label: "Active",
className: "bg-green-100 text-green-800",
icon: "🟢",
variant: "default" as const,
icon: "CheckCircle" as const,
},
completed: {
label: "Completed",
className: "bg-blue-100 text-blue-800",
icon: "✅",
variant: "outline" as const,
icon: "CheckCircle" as const,
},
archived: {
label: "Archived",
className: "bg-orange-100 text-orange-800",
icon: "📦",
variant: "destructive" as const,
icon: "XCircle" as const,
},
};
export default async function StudyDetailPage({
params,
}: StudyDetailPageProps) {
try {
const resolvedParams = await params;
const study = await api.studies.get({ id: resolvedParams.id });
const members = await api.studies.getMembers({
studyId: resolvedParams.id,
});
export default function StudyDetailPage({ params }: StudyDetailPageProps) {
const { data: session } = useSession();
const [study, setStudy] = useState<any>(null);
const [members, setMembers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [resolvedParams, setResolvedParams] = useState<{ id: string } | null>(
null,
);
if (!study) {
notFound();
useEffect(() => {
async function resolveParams() {
const resolved = await params;
setResolvedParams(resolved);
}
resolveParams();
}, [params]);
const statusInfo = statusConfig[study.status];
const { data: studyData } = api.studies.get.useQuery(
{ id: resolvedParams?.id ?? "" },
{ enabled: !!resolvedParams?.id },
);
return (
<div className="p-8">
{/* Header */}
<div className="mb-8">
<div className="mb-4 flex items-center space-x-2 text-sm text-slate-600">
<Link href="/studies" className="hover:text-slate-900">
Studies
</Link>
<span>/</span>
<span className="text-slate-900">{study.name}</span>
</div>
const { data: membersData } = api.studies.getMembers.useQuery(
{ studyId: resolvedParams?.id ?? "" },
{ enabled: !!resolvedParams?.id },
);
<div className="flex items-start justify-between">
<div className="min-w-0 flex-1">
<div className="mb-2 flex items-center space-x-3">
<h1 className="truncate text-3xl font-bold text-slate-900">
{study.name}
</h1>
<Badge className={statusInfo.className} variant="secondary">
<span className="mr-1">{statusInfo.icon}</span>
{statusInfo.label}
</Badge>
</div>
<p className="text-lg text-slate-600">{study.description}</p>
</div>
useEffect(() => {
if (studyData) {
setStudy(studyData);
}
if (membersData) {
setMembers(membersData);
}
if (studyData !== undefined) {
setLoading(false);
}
}, [studyData, membersData]);
<div className="ml-4 flex items-center space-x-2">
<Button asChild variant="outline">
<Link href={`/studies/${study.id}/edit`}>
<Settings className="mr-2 h-4 w-4" />
Edit Study
</Link>
</Button>
<Button asChild>
<Link href={`/studies/${study.id}/experiments/new`}>
<Plus className="mr-2 h-4 w-4" />
New Experiment
</Link>
</Button>
</div>
</div>
</div>
// Set breadcrumbs
useBreadcrumbsEffect([
{ label: "Dashboard", href: "/dashboard" },
{ label: "Studies", href: "/studies" },
{ label: study?.name || "Study" },
]);
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{/* Main Content */}
<div className="space-y-8 lg:col-span-2">
{/* Study Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Building className="h-5 w-5" />
<span>Study Information</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label className="text-sm font-medium text-slate-700">
Institution
</label>
<p className="text-slate-900">{study.institution}</p>
</div>
{study.irbProtocol && (
<div>
<label className="text-sm font-medium text-slate-700">
IRB Protocol
</label>
<p className="text-slate-900">{study.irbProtocol}</p>
</div>
)}
<div>
<label className="text-sm font-medium text-slate-700">
Created
</label>
<p className="text-slate-900">
{formatDistanceToNow(study.createdAt, {
addSuffix: true,
})}
</p>
</div>
<div>
<label className="text-sm font-medium text-slate-700">
Last Updated
</label>
<p className="text-slate-900">
{formatDistanceToNow(study.updatedAt, {
addSuffix: true,
})}
</p>
</div>
</div>
</CardContent>
</Card>
{/* Experiments */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center space-x-2">
<FlaskConical className="h-5 w-5" />
<span>Experiments</span>
</CardTitle>
<Button asChild variant="outline" size="sm">
<Link href={`/studies/${study.id}/experiments/new`}>
<Plus className="mr-2 h-4 w-4" />
Add Experiment
</Link>
</Button>
</div>
<CardDescription>
Design and manage experimental protocols for this study
</CardDescription>
</CardHeader>
<CardContent>
{/* Placeholder for experiments list */}
<div className="py-8 text-center">
<FlaskConical className="mx-auto mb-4 h-12 w-12 text-slate-400" />
<h3 className="mb-2 text-lg font-semibold text-slate-900">
No Experiments Yet
</h3>
<p className="mb-4 text-slate-600">
Create your first experiment to start designing research
protocols
</p>
<Button asChild>
<Link href={`/studies/${study.id}/experiments/new`}>
Create First Experiment
</Link>
</Button>
</div>
</CardContent>
</Card>
{/* Recent Activity */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<BarChart3 className="h-5 w-5" />
<span>Recent Activity</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="py-8 text-center">
<Calendar className="mx-auto mb-4 h-12 w-12 text-slate-400" />
<h3 className="mb-2 text-lg font-semibold text-slate-900">
No Recent Activity
</h3>
<p className="text-slate-600">
Activity will appear here once you start working on this
study
</p>
</div>
</CardContent>
</Card>
</div>
{/* Sidebar */}
<div className="space-y-6">
{/* Team Members */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center space-x-2">
<Users className="h-5 w-5" />
<span>Team</span>
</CardTitle>
<Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
Invite
</Button>
</div>
<CardDescription>
{members.length} team member{members.length !== 1 ? "s" : ""}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{members.map((member) => (
<div
key={member.user.id}
className="flex items-center space-x-3"
>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100">
<span className="text-sm font-medium text-blue-600">
{(member.user.name ?? member.user.email)
.charAt(0)
.toUpperCase()}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-slate-900">
{member.user.name ?? member.user.email}
</p>
<p className="text-xs text-slate-500 capitalize">
{member.role}
</p>
</div>
{member.role === "owner" && (
<Shield className="h-4 w-4 text-amber-600" />
)}
</div>
))}
</div>
</CardContent>
</Card>
{/* Quick Stats */}
<Card>
<CardHeader>
<CardTitle>Quick Stats</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-sm text-slate-600">Experiments:</span>
<span className="font-medium">0</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-slate-600">
Total Trials:
</span>
<span className="font-medium">0</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-slate-600">
Participants:
</span>
<span className="font-medium">0</span>
</div>
<Separator />
<div className="flex justify-between">
<span className="text-sm text-slate-600">
Completion Rate:
</span>
<span className="font-medium text-green-600"></span>
</div>
</div>
</CardContent>
</Card>
{/* Quick Actions */}
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<Button
asChild
variant="outline"
className="w-full justify-start"
>
<Link href={`/studies/${study.id}/participants`}>
<Users className="mr-2 h-4 w-4" />
Manage Participants
</Link>
</Button>
<Button
asChild
variant="outline"
className="w-full justify-start"
>
<Link href={`/studies/${study.id}/trials`}>
<Calendar className="mr-2 h-4 w-4" />
Schedule Trials
</Link>
</Button>
<Button
asChild
variant="outline"
className="w-full justify-start"
>
<Link href={`/studies/${study.id}/analytics`}>
<BarChart3 className="mr-2 h-4 w-4" />
View Analytics
</Link>
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
);
} catch (error) {
console.error("Error loading study:", error);
notFound();
if (!session?.user) {
return notFound();
}
if (loading || !study) {
return <div>Loading...</div>;
}
const statusInfo = statusConfig[study.status];
// TODO: Get actual stats from API
const mockStats = {
experiments: 0,
totalTrials: 0,
participants: 0,
completionRate: "—",
};
return (
<EntityView>
{/* Header */}
<EntityViewHeader
title={study.name}
subtitle={study.description}
icon="Building"
status={{
label: statusInfo.label,
variant: statusInfo.variant,
icon: statusInfo.icon,
}}
actions={
<>
<Button asChild variant="outline">
<Link href={`/studies/${study.id}/edit`}>
<Settings className="mr-2 h-4 w-4" />
Edit Study
</Link>
</Button>
<Button asChild>
<Link href={`/experiments/new?studyId=${study.id}`}>
<Plus className="mr-2 h-4 w-4" />
New Experiment
</Link>
</Button>
</>
}
/>
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{/* Main Content */}
<div className="space-y-8 lg:col-span-2">
{/* Study Information */}
<EntityViewSection title="Study Information" icon="Building">
<InfoGrid
items={[
{
label: "Institution",
value: study.institution,
},
{
label: "IRB Protocol",
value: study.irbProtocol || "Not specified",
},
{
label: "Created",
value: formatDistanceToNow(study.createdAt, {
addSuffix: true,
}),
},
{
label: "Last Updated",
value: formatDistanceToNow(study.updatedAt, {
addSuffix: true,
}),
},
]}
/>
</EntityViewSection>
{/* Experiments */}
<EntityViewSection
title="Experiments"
icon="FlaskConical"
description="Design and manage experimental protocols for this study"
actions={
<Button asChild variant="outline" size="sm">
<Link href={`/experiments/new?studyId=${study.id}`}>
<Plus className="mr-2 h-4 w-4" />
Add Experiment
</Link>
</Button>
}
>
<EmptyState
icon="FlaskConical"
title="No Experiments Yet"
description="Create your first experiment to start designing research protocols"
action={
<Button asChild>
<Link href={`/experiments/new?studyId=${study.id}`}>
Create First Experiment
</Link>
</Button>
}
/>
</EntityViewSection>
{/* Recent Activity */}
<EntityViewSection title="Recent Activity" icon="BarChart3">
<EmptyState
icon="Calendar"
title="No Recent Activity"
description="Activity will appear here once you start working on this study"
/>
</EntityViewSection>
</div>
{/* Sidebar */}
<EntityViewSidebar>
{/* Team Members */}
<EntityViewSection
title="Team"
icon="Users"
description={`${members.length} team member${members.length !== 1 ? "s" : ""}`}
actions={
<Button variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
Invite
</Button>
}
>
<div className="space-y-3">
{members.map((member) => (
<div
key={member.user.id}
className="flex items-center space-x-3"
>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100">
<span className="text-sm font-medium text-blue-600">
{(member.user.name ?? member.user.email)
.charAt(0)
.toUpperCase()}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">
{member.user.name ?? member.user.email}
</p>
<p className="text-muted-foreground text-xs capitalize">
{member.role}
</p>
</div>
{member.role === "owner" && (
<Shield className="h-4 w-4 text-amber-600" />
)}
</div>
))}
</div>
</EntityViewSection>
{/* Quick Stats */}
<EntityViewSection title="Quick Stats" icon="BarChart3">
<StatsGrid
stats={[
{
label: "Experiments",
value: mockStats.experiments,
},
{
label: "Total Trials",
value: mockStats.totalTrials,
},
{
label: "Participants",
value: mockStats.participants,
},
{
label: "Completion Rate",
value: mockStats.completionRate,
color: "success",
},
]}
/>
</EntityViewSection>
{/* Quick Actions */}
<EntityViewSection title="Quick Actions" icon="Settings">
<QuickActions
actions={[
{
label: "Manage Participants",
icon: "Users",
href: `/participants?studyId=${study.id}`,
},
{
label: "Schedule Trials",
icon: "Calendar",
href: `/trials?studyId=${study.id}`,
},
{
label: "View Analytics",
icon: "BarChart3",
href: `/analytics?studyId=${study.id}`,
},
]}
/>
</EntityViewSection>
</EntityViewSidebar>
</div>
</EntityView>
);
}