Update docs, continue route consolidation

This commit is contained in:
2025-09-23 23:52:49 -04:00
parent c2bfeb8db2
commit e0679f726e
7 changed files with 1479 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import Link from "next/link";
import { AlertCircle, Home, ArrowLeft } from "lucide-react";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
export default function DashboardNotFound() {
return (
<div className="flex min-h-[60vh] items-center justify-center p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-red-50">
<AlertCircle className="h-8 w-8 text-red-500" />
</div>
<CardTitle className="text-2xl">Page Not Found</CardTitle>
<CardDescription>
The page you&apos;re looking for doesn&apos;t exist or has been
moved.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-muted-foreground space-y-2 text-center text-sm">
<p>This might have happened because:</p>
<ul className="space-y-1 text-left">
<li> The URL was typed incorrectly</li>
<li> The page was moved or deleted</li>
<li> You don&apos;t have permission to view this page</li>
</ul>
</div>
<div className="flex flex-col gap-2 pt-4">
<Button asChild className="w-full">
<Link href="/dashboard">
<Home className="mr-2 h-4 w-4" />
Go to Dashboard
</Link>
</Button>
<Button asChild variant="outline" className="w-full">
<Link href="/studies">
<ArrowLeft className="mr-2 h-4 w-4" />
Browse Studies
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,329 @@
"use client";
import { useParams } from "next/navigation";
import { Suspense, useEffect } from "react";
import {
Activity,
BarChart3,
Calendar,
Download,
Filter,
TrendingDown,
TrendingUp,
} from "lucide-react";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { ManagementPageLayout } from "~/components/ui/page-layout";
import { useStudyContext } from "~/lib/study-context";
import { useSelectedStudyDetails } from "~/hooks/useSelectedStudyDetails";
// Mock chart component - replace with actual charting library
function MockChart({ title, data }: { title: string; data: number[] }) {
const maxValue = Math.max(...data);
return (
<div className="space-y-2">
<h4 className="text-sm font-medium">{title}</h4>
<div className="flex h-32 items-end space-x-1">
{data.map((value, index) => (
<div
key={index}
className="bg-primary min-h-[4px] flex-1 rounded-t"
style={{ height: `${(value / maxValue) * 100}%` }}
/>
))}
</div>
</div>
);
}
function AnalyticsOverview() {
const metrics = [
{
title: "Total Trials This Month",
value: "142",
change: "+12%",
trend: "up",
description: "vs last month",
icon: Activity,
},
{
title: "Avg Trial Duration",
value: "24.5m",
change: "-3%",
trend: "down",
description: "vs last month",
icon: Calendar,
},
{
title: "Completion Rate",
value: "94.2%",
change: "+2.1%",
trend: "up",
description: "vs last month",
icon: TrendingUp,
},
{
title: "Participant Retention",
value: "87.3%",
change: "+5.4%",
trend: "up",
description: "vs last month",
icon: BarChart3,
},
];
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{metrics.map((metric) => (
<Card key={metric.title}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{metric.title}
</CardTitle>
<metric.icon className="text-muted-foreground h-4 w-4" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{metric.value}</div>
<div className="text-muted-foreground flex items-center space-x-2 text-xs">
<span
className={`flex items-center ${
metric.trend === "up" ? "text-green-600" : "text-red-600"
}`}
>
{metric.trend === "up" ? (
<TrendingUp className="mr-1 h-3 w-3" />
) : (
<TrendingDown className="mr-1 h-3 w-3" />
)}
{metric.change}
</span>
<span>{metric.description}</span>
</div>
</CardContent>
</Card>
))}
</div>
);
}
function ChartsSection() {
const trialData = [12, 19, 15, 27, 32, 28, 35, 42, 38, 41, 37, 44];
const participantData = [8, 12, 10, 15, 18, 16, 20, 24, 22, 26, 23, 28];
const completionData = [85, 88, 92, 89, 94, 91, 95, 92, 96, 94, 97, 94];
return (
<div className="grid gap-4 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>Trial Volume</CardTitle>
<CardDescription>Monthly trial execution trends</CardDescription>
</CardHeader>
<CardContent>
<MockChart title="Trials per Month" data={trialData} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Participant Enrollment</CardTitle>
<CardDescription>New participants over time</CardDescription>
</CardHeader>
<CardContent>
<MockChart title="New Participants" data={participantData} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Completion Rates</CardTitle>
<CardDescription>Trial completion percentage</CardDescription>
</CardHeader>
<CardContent>
<MockChart title="Completion %" data={completionData} />
</CardContent>
</Card>
</div>
);
}
function RecentInsights() {
const insights = [
{
title: "Peak Performance Hours",
description:
"Participants show 23% better performance during 10-11 AM trials",
type: "trend",
severity: "info",
},
{
title: "Attention Span Decline",
description:
"Average attention span has decreased by 8% over the last month",
type: "alert",
severity: "warning",
},
{
title: "High Completion Rate",
description: "Memory retention study achieved 98% completion rate",
type: "success",
severity: "success",
},
{
title: "Equipment Utilization",
description: "Robot interaction trials are at 85% capacity utilization",
type: "info",
severity: "info",
},
];
const getSeverityColor = (severity: string) => {
switch (severity) {
case "success":
return "bg-green-50 text-green-700 border-green-200";
case "warning":
return "bg-yellow-50 text-yellow-700 border-yellow-200";
case "info":
return "bg-blue-50 text-blue-700 border-blue-200";
default:
return "bg-gray-50 text-gray-700 border-gray-200";
}
};
return (
<Card>
<CardHeader>
<CardTitle>Recent Insights</CardTitle>
<CardDescription>
AI-generated insights from your research data
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{insights.map((insight, index) => (
<div
key={index}
className={`rounded-lg border p-4 ${getSeverityColor(insight.severity)}`}
>
<h4 className="mb-1 font-medium">{insight.title}</h4>
<p className="text-sm">{insight.description}</p>
</div>
))}
</div>
</CardContent>
</Card>
);
}
function AnalyticsContent({ studyId: _studyId }: { studyId: string }) {
return (
<div className="space-y-6">
{/* Header with time range controls */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Select defaultValue="30d">
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Time range" />
</SelectTrigger>
<SelectContent>
<SelectItem value="7d">Last 7 days</SelectItem>
<SelectItem value="30d">Last 30 days</SelectItem>
<SelectItem value="90d">Last 90 days</SelectItem>
<SelectItem value="1y">Last year</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm">
<Filter className="mr-2 h-4 w-4" />
Filter
</Button>
<Button variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
Export
</Button>
</div>
</div>
{/* Overview Metrics */}
<AnalyticsOverview />
{/* Charts */}
<ChartsSection />
{/* Insights */}
<div className="grid gap-4 lg:grid-cols-3">
<div className="lg:col-span-2">
<RecentInsights />
</div>
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>Generate custom reports</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<Button variant="outline" className="w-full justify-start">
<BarChart3 className="mr-2 h-4 w-4" />
Trial Performance Report
</Button>
<Button variant="outline" className="w-full justify-start">
<Activity className="mr-2 h-4 w-4" />
Participant Engagement
</Button>
<Button variant="outline" className="w-full justify-start">
<TrendingUp className="mr-2 h-4 w-4" />
Trend Analysis
</Button>
<Button variant="outline" className="w-full justify-start">
<Download className="mr-2 h-4 w-4" />
Custom Export
</Button>
</CardContent>
</Card>
</div>
</div>
);
}
export default function StudyAnalyticsPage() {
const params = useParams();
const studyId: string = typeof params.id === "string" ? params.id : "";
const { setSelectedStudyId, selectedStudyId } = useStudyContext();
const { study } = useSelectedStudyDetails();
// Set the active study if it doesn't match the current route
useEffect(() => {
if (studyId && selectedStudyId !== studyId) {
setSelectedStudyId(studyId);
}
}, [studyId, selectedStudyId, setSelectedStudyId]);
return (
<ManagementPageLayout
title="Analytics"
description="Insights and data analysis for this study"
breadcrumb={[
{ label: "Dashboard", href: "/dashboard" },
{ label: "Studies", href: "/studies" },
{ label: study?.name ?? "Study", href: `/studies/${studyId}` },
{ label: "Analytics" },
]}
>
<Suspense fallback={<div>Loading analytics...</div>}>
<AnalyticsContent studyId={studyId} />
</Suspense>
</ManagementPageLayout>
);
}

View File

@@ -0,0 +1,3 @@
import DashboardLayout from "../(dashboard)/layout";
export default DashboardLayout;

352
src/app/dashboard/page.tsx Normal file
View File

@@ -0,0 +1,352 @@
"use client";
import * as React from "react";
import Link from "next/link";
import {
Building,
FlaskConical,
TestTube,
Users,
Calendar,
Clock,
AlertCircle,
CheckCircle2,
} from "lucide-react";
import { formatDistanceToNow } from "date-fns";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { Badge } from "~/components/ui/badge";
import { Progress } from "~/components/ui/progress";
import { api } from "~/trpc/react";
// Dashboard Overview Cards
function OverviewCards() {
const { data: stats, isLoading } = api.dashboard.getStats.useQuery();
const cards = [
{
title: "Active Studies",
value: stats?.totalStudies ?? 0,
description: "Research studies you have access to",
icon: Building,
color: "text-blue-600",
bg: "bg-blue-50",
},
{
title: "Experiments",
value: stats?.totalExperiments ?? 0,
description: "Experiment protocols designed",
icon: FlaskConical,
color: "text-green-600",
bg: "bg-green-50",
},
{
title: "Participants",
value: stats?.totalParticipants ?? 0,
description: "Enrolled participants",
icon: Users,
color: "text-purple-600",
bg: "bg-purple-50",
},
{
title: "Trials",
value: stats?.totalTrials ?? 0,
description: "Total trials conducted",
icon: TestTube,
color: "text-orange-600",
bg: "bg-orange-50",
},
];
if (isLoading) {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="bg-muted h-4 w-20 animate-pulse rounded" />
<div className="bg-muted h-8 w-8 animate-pulse rounded" />
</CardHeader>
<CardContent>
<div className="bg-muted h-8 w-12 animate-pulse rounded" />
<div className="bg-muted mt-2 h-3 w-24 animate-pulse rounded" />
</CardContent>
</Card>
))}
</div>
);
}
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{cards.map((card) => (
<Card key={card.title}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{card.title}</CardTitle>
<div className={`rounded-md p-2 ${card.bg}`}>
<card.icon className={`h-4 w-4 ${card.color}`} />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{card.value}</div>
<p className="text-muted-foreground text-xs">{card.description}</p>
</CardContent>
</Card>
))}
</div>
);
}
// Recent Activity Component
function RecentActivity() {
const { data: activities = [], isLoading } =
api.dashboard.getRecentActivity.useQuery({
limit: 8,
});
const getStatusIcon = (status: string) => {
switch (status) {
case "success":
return <CheckCircle2 className="h-4 w-4 text-green-600" />;
case "pending":
return <Clock className="h-4 w-4 text-yellow-600" />;
case "error":
return <AlertCircle className="h-4 w-4 text-red-600" />;
default:
return <AlertCircle className="h-4 w-4 text-blue-600" />;
}
};
return (
<Card className="col-span-4">
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>
Latest updates from your research platform
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="flex items-center space-x-4">
<div className="bg-muted h-4 w-4 animate-pulse rounded-full" />
<div className="flex-1 space-y-2">
<div className="bg-muted h-4 w-3/4 animate-pulse rounded" />
<div className="bg-muted h-3 w-1/2 animate-pulse rounded" />
</div>
<div className="bg-muted h-3 w-16 animate-pulse rounded" />
</div>
))}
</div>
) : activities.length === 0 ? (
<div className="py-8 text-center">
<AlertCircle className="text-muted-foreground mx-auto h-8 w-8" />
<p className="text-muted-foreground mt-2 text-sm">
No recent activity
</p>
</div>
) : (
<div className="space-y-4">
{activities.map((activity) => (
<div key={activity.id} className="flex items-center space-x-4">
{getStatusIcon(activity.status)}
<div className="flex-1 space-y-1">
<p className="text-sm leading-none font-medium">
{activity.title}
</p>
<p className="text-muted-foreground text-sm">
{activity.description}
</p>
</div>
<div className="text-muted-foreground text-sm">
{formatDistanceToNow(activity.time, { addSuffix: true })}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
// Quick Actions Component
function QuickActions() {
const actions = [
{
title: "Create Study",
description: "Start a new research study",
href: "/studies/new",
icon: Building,
color: "bg-blue-500 hover:bg-blue-600",
},
{
title: "Browse Studies",
description: "View and manage your studies",
href: "/studies",
icon: Building,
color: "bg-green-500 hover:bg-green-600",
},
{
title: "Create Experiment",
description: "Design new experiment protocol",
href: "/experiments/new",
icon: FlaskConical,
color: "bg-purple-500 hover:bg-purple-600",
},
{
title: "Browse Experiments",
description: "View experiment templates",
href: "/experiments",
icon: FlaskConical,
color: "bg-orange-500 hover:bg-orange-600",
},
];
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{actions.map((action) => (
<Card
key={action.title}
className="group cursor-pointer transition-all hover:shadow-md"
>
<CardContent className="p-6">
<Button asChild className={`w-full ${action.color} text-white`}>
<Link href={action.href}>
<action.icon className="mr-2 h-4 w-4" />
{action.title}
</Link>
</Button>
<p className="text-muted-foreground mt-2 text-sm">
{action.description}
</p>
</CardContent>
</Card>
))}
</div>
);
}
// Study Progress Component
function StudyProgress() {
const { data: studies = [], isLoading } =
api.dashboard.getStudyProgress.useQuery({
limit: 5,
});
return (
<Card className="col-span-3">
<CardHeader>
<CardTitle>Study Progress</CardTitle>
<CardDescription>
Current status of active research studies
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-6">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="bg-muted h-4 w-32 animate-pulse rounded" />
<div className="bg-muted h-3 w-24 animate-pulse rounded" />
</div>
<div className="bg-muted h-5 w-16 animate-pulse rounded" />
</div>
<div className="bg-muted h-2 w-full animate-pulse rounded" />
<div className="bg-muted h-3 w-16 animate-pulse rounded" />
</div>
))}
</div>
) : studies.length === 0 ? (
<div className="py-8 text-center">
<Building className="text-muted-foreground mx-auto h-8 w-8" />
<p className="text-muted-foreground mt-2 text-sm">
No active studies found
</p>
<p className="text-muted-foreground text-xs">
Create a study to get started
</p>
</div>
) : (
<div className="space-y-6">
{studies.map((study) => (
<div key={study.id} className="space-y-2">
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm leading-none font-medium">
{study.name}
</p>
<p className="text-muted-foreground text-sm">
{study.participants}/{study.totalParticipants} completed
trials
</p>
</div>
<Badge
variant={
study.status === "active" ? "default" : "secondary"
}
>
{study.status}
</Badge>
</div>
<Progress value={study.progress} className="h-2" />
<p className="text-muted-foreground text-xs">
{study.progress}% complete
</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
export default function DashboardPage() {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<p className="text-muted-foreground">
Welcome to your HRI Studio research platform
</p>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="text-xs">
<Calendar className="mr-1 h-3 w-3" />
{new Date().toLocaleDateString()}
</Badge>
</div>
</div>
{/* Overview Cards */}
<OverviewCards />
{/* Main Content Grid */}
<div className="grid gap-4 lg:grid-cols-7">
<StudyProgress />
<div className="col-span-4 space-y-4">
<RecentActivity />
</div>
</div>
{/* Quick Actions */}
<div className="space-y-4">
<h2 className="text-xl font-semibold">Quick Actions</h2>
<QuickActions />
</div>
</div>
);
}