feat: Redesign Landing, Auth, and Dashboard Pages

Also fixed schema type exports and seed script errors.
This commit is contained in:
2026-02-01 22:28:19 -05:00
parent 816b2b9e31
commit dbfdd91dea
300 changed files with 17239 additions and 5952 deletions

478
src/app/(dashboard)/studies/[id]/analytics/page.tsx Normal file → Executable file
View File

@@ -1,25 +1,25 @@
"use client";
import { useParams } from "next/navigation";
import { Suspense, useEffect } from "react";
import { Suspense, useEffect, useState } from "react";
import {
Activity,
BarChart3,
Calendar,
Download,
Search,
Filter,
TrendingDown,
TrendingUp,
PlayCircle,
Calendar,
Clock,
ChevronRight,
User,
LayoutGrid
} from "lucide-react";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { PageHeader } from "~/components/ui/page-header";
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
import { useStudyContext } from "~/lib/study-context";
import { useSelectedStudyDetails } from "~/hooks/useSelectedStudyDetails";
import { api } from "~/trpc/react";
import { TrialAnalysisView } from "~/components/trials/views/TrialAnalysisView";
import {
Select,
SelectContent,
@@ -27,283 +27,180 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { PageHeader } from "~/components/ui/page-header";
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
import { useStudyContext } from "~/lib/study-context";
import { useSelectedStudyDetails } from "~/hooks/useSelectedStudyDetails";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { ScrollArea } from "~/components/ui/scroll-area";
import { formatDistanceToNow } from "date-fns";
// Mock chart component - replace with actual charting library
function MockChart({ title, data }: { title: string; data: number[] }) {
const maxValue = Math.max(...data);
// -- Sub-Components --
function AnalyticsContent({
selectedTrialId,
setSelectedTrialId,
trialsList,
isLoadingList
}: {
selectedTrialId: string | null;
setSelectedTrialId: (id: string | null) => void;
trialsList: any[];
isLoadingList: boolean;
}) {
// Fetch full details of selected trial
const {
data: selectedTrial,
isLoading: isLoadingTrial,
error: trialError
} = api.trials.get.useQuery(
{ id: selectedTrialId! },
{ enabled: !!selectedTrialId }
);
// Transform trial data
const trialData = selectedTrial ? {
...selectedTrial,
startedAt: selectedTrial.startedAt ? new Date(selectedTrial.startedAt) : null,
completedAt: selectedTrial.completedAt ? new Date(selectedTrial.completedAt) : null,
eventCount: (selectedTrial as any).eventCount,
mediaCount: (selectedTrial as any).mediaCount,
} : null;
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 className="h-[calc(100vh-140px)] flex flex-col">
{selectedTrialId ? (
isLoadingTrial ? (
<div className="flex-1 flex items-center justify-center bg-background/50 rounded-lg border border-dashed">
<div className="flex flex-col items-center gap-2 animate-pulse">
<div className="h-8 w-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
<span className="text-muted-foreground text-sm">Loading trial data...</span>
</div>
</div>
) : trialError ? (
<div className="flex-1 flex items-center justify-center p-8 bg-background/50 rounded-lg border border-dashed text-destructive">
<div className="max-w-md text-center">
<h3 className="font-semibold mb-2">Error Loading Trial</h3>
<p className="text-sm opacity-80">{trialError.message}</p>
<Button variant="outline" className="mt-4" onClick={() => setSelectedTrialId(null)}>
Return to Overview
</Button>
</div>
</div>
) : trialData ? (
<TrialAnalysisView trial={trialData} />
) : null
) : (
<div className="flex-1 bg-background/50 rounded-lg border shadow-sm overflow-hidden">
<StudyOverviewPlaceholder
trials={trialsList ?? []}
onSelect={(id) => setSelectedTrialId(id)}
/>
))}
</div>
</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,
},
];
function StudyOverviewPlaceholder({ trials, onSelect }: { trials: any[], onSelect: (id: string) => void }) {
const recentTrials = [...trials].sort((a, b) =>
new Date(b.startedAt || b.createdAt).getTime() - new Date(a.startedAt || a.createdAt).getTime()
).slice(0, 5);
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 className="h-full p-8 grid place-items-center bg-muted/5">
<div className="max-w-3xl w-full grid gap-8 md:grid-cols-2">
{/* Left: Illustration / Prompt */}
<div className="flex flex-col justify-center space-y-4">
<div className="bg-primary/10 w-16 h-16 rounded-2xl flex items-center justify-center mb-2">
<BarChart3 className="h-8 w-8 text-primary" />
</div>
<div>
<h2 className="text-2xl font-semibold tracking-tight">Analytics & Playback</h2>
<CardDescription className="text-base mt-2">
Select a session from the top right to review video recordings, event logs, and metrics.
</CardDescription>
</div>
<div className="flex gap-4 pt-4">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<PlayCircle className="h-4 w-4" />
Feature-rich playback
</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 className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
Synchronized timeline
</div>
))}
</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>
{/* Right: Recent Sessions */}
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>Generate custom reports</CardDescription>
<CardHeader className="pb-3">
<CardTitle className="text-base">Recent Sessions</CardTitle>
</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 className="p-0">
<ScrollArea className="h-[240px]">
<div className="px-4 pb-4 space-y-1">
{recentTrials.map(trial => (
<button
key={trial.id}
onClick={() => onSelect(trial.id)}
className="w-full flex items-center gap-3 p-3 rounded-md hover:bg-accent transition-colors text-left group"
>
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-mono font-medium text-primary">
{trial.sessionNumber}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">
{trial.participant?.participantCode ?? "Unknown"}
</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded-full border capitalize ${trial.status === 'completed' ? 'bg-green-500/10 text-green-500 border-green-500/20' :
trial.status === 'in_progress' ? 'bg-blue-500/10 text-blue-500 border-blue-500/20' :
'bg-slate-500/10 text-slate-500 border-slate-500/20'
}`}>
{trial.status.replace('_', ' ')}
</span>
</div>
<div className="text-xs text-muted-foreground flex items-center gap-2 mt-0.5">
<Calendar className="h-3 w-3" />
{new Date(trial.createdAt).toLocaleDateString()}
<span className="text-muted-foreground top-[1px] relative text-[10px]"></span>
{formatDistanceToNow(new Date(trial.createdAt), { addSuffix: true })}
</div>
</div>
<ChevronRight className="h-4 w-4 text-muted-foreground/30 group-hover:text-primary transition-colors" />
</button>
))}
{recentTrials.length === 0 && (
<div className="py-8 text-center text-sm text-muted-foreground">
No sessions found.
</div>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
</div>
);
)
}
// -- Main Page --
export default function StudyAnalyticsPage() {
const params = useParams();
const studyId: string = typeof params.id === "string" ? params.id : "";
const { setSelectedStudyId, selectedStudyId } = useStudyContext();
const { study } = useSelectedStudyDetails();
// State lifted up
const [selectedTrialId, setSelectedTrialId] = useState<string | null>(null);
// Fetch list of trials for the selector
const { data: trialsList, isLoading: isLoadingList } = api.trials.list.useQuery(
{ studyId, limit: 100 },
{ enabled: !!studyId }
);
// Set breadcrumbs
useBreadcrumbsEffect([
{ label: "Dashboard", href: "/dashboard" },
@@ -320,16 +217,53 @@ export default function StudyAnalyticsPage() {
}, [studyId, selectedStudyId, setSelectedStudyId]);
return (
<div className="space-y-6">
<PageHeader
title="Analytics"
description="Insights and data analysis for this study"
icon={BarChart3}
/>
<div className="h-[calc(100vh-64px)] flex flex-col p-6 gap-6">
<div className="flex-none">
<PageHeader
title="Analytics"
description="Analyze trial data and replay sessions"
icon={BarChart3}
actions={
<div className="flex items-center gap-2">
{/* Session Selector in Header */}
<div className="w-[300px]">
<Select
value={selectedTrialId ?? "overview"}
onValueChange={(val) => setSelectedTrialId(val === "overview" ? null : val)}
>
<SelectTrigger className="w-full h-9 text-xs">
<LayoutGrid className="h-3.5 w-3.5 mr-2 text-muted-foreground" />
<SelectValue placeholder="Select Session" />
</SelectTrigger>
<SelectContent className="max-h-[400px]" align="end">
<SelectItem value="overview" className="border-b mb-1 pb-1 font-medium text-xs">
Show Study Overview
</SelectItem>
{trialsList?.map((trial) => (
<SelectItem key={trial.id} value={trial.id} className="text-xs">
<span className="font-mono mr-2 text-muted-foreground">#{trial.sessionNumber}</span>
{trial.participant?.participantCode ?? "Unknown"} <span className="text-muted-foreground ml-1">({new Date(trial.createdAt).toLocaleDateString()})</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
}
/>
</div>
<Suspense fallback={<div>Loading analytics...</div>}>
<AnalyticsContent studyId={studyId} />
</Suspense>
<div className="flex-1 min-h-0 bg-transparent">
<Suspense fallback={<div>Loading analytics...</div>}>
<AnalyticsContent
selectedTrialId={selectedTrialId}
setSelectedTrialId={setSelectedTrialId}
trialsList={trialsList ?? []}
isLoadingList={isLoadingList}
studyId={studyId}
/>
</Suspense>
</div>
</div>
);
}