mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-23 19:27:51 -04:00
ui: complete dashboard redesign
- New modern dashboard layout with gradient background - Quick action cards with teal glow effects - Live trials banner with pulsing indicator - Stats grid with colored icons - Study list with bot icons - Quick links sidebar - Recent trials section with status badges - Proper null safety and type checking
This commit is contained in:
@@ -2,31 +2,21 @@
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { format } from "date-fns";
|
import { useSession } from "~/lib/auth-client";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import {
|
import {
|
||||||
Activity,
|
|
||||||
ArrowRight,
|
|
||||||
Calendar,
|
|
||||||
CheckCircle,
|
|
||||||
CheckCircle2,
|
|
||||||
Clock,
|
|
||||||
FlaskConical,
|
|
||||||
HelpCircle,
|
|
||||||
LayoutDashboard,
|
|
||||||
MoreHorizontal,
|
|
||||||
Play,
|
Play,
|
||||||
PlayCircle,
|
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Activity,
|
||||||
Settings,
|
Clock,
|
||||||
|
CheckCircle2,
|
||||||
Users,
|
Users,
|
||||||
Radio,
|
FlaskConical,
|
||||||
Gamepad2,
|
ChevronRight,
|
||||||
AlertTriangle,
|
|
||||||
Bot,
|
Bot,
|
||||||
User,
|
Radio,
|
||||||
MessageSquare,
|
BarChart3,
|
||||||
|
Settings,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
@@ -37,448 +27,307 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "~/components/ui/card";
|
} from "~/components/ui/card";
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "~/components/ui/dropdown-menu";
|
|
||||||
import { Progress } from "~/components/ui/progress";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "~/components/ui/select";
|
|
||||||
import { Badge } from "~/components/ui/badge";
|
import { Badge } from "~/components/ui/badge";
|
||||||
import { ScrollArea } from "~/components/ui/scroll-area";
|
import { ScrollArea } from "~/components/ui/scroll-area";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { useTour } from "~/components/onboarding/TourProvider";
|
|
||||||
import { useSession } from "~/lib/auth-client";
|
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { startTour } = useTour();
|
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const [studyFilter, setStudyFilter] = React.useState<string | null>(null);
|
const userName = session?.user?.name ?? "Researcher";
|
||||||
|
|
||||||
// --- Data Fetching ---
|
const { data: userStudies } = api.studies.list.useQuery({
|
||||||
const { data: userStudiesData } = api.studies.list.useQuery({
|
|
||||||
memberOnly: true,
|
memberOnly: true,
|
||||||
limit: 100,
|
limit: 10,
|
||||||
});
|
});
|
||||||
const userStudies = userStudiesData?.studies ?? [];
|
|
||||||
|
|
||||||
const { data: stats } = api.dashboard.getStats.useQuery({
|
const { data: recentTrials } = api.trials.list.useQuery({
|
||||||
studyId: studyFilter ?? undefined,
|
limit: 5,
|
||||||
|
status: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: liveTrials } = api.dashboard.getLiveTrials.useQuery(
|
const { data: liveTrials } = api.dashboard.getLiveTrials.useQuery(
|
||||||
{ studyId: studyFilter ?? undefined },
|
{},
|
||||||
{ refetchInterval: 5000 },
|
{ refetchInterval: 5000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: recentActivity } = api.dashboard.getRecentActivity.useQuery({
|
const { data: stats } = api.dashboard.getStats.useQuery({});
|
||||||
limit: 15,
|
|
||||||
studyId: studyFilter ?? undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: studyProgress } = api.dashboard.getStudyProgress.useQuery({
|
const greeting = (() => {
|
||||||
limit: 5,
|
|
||||||
studyId: studyFilter ?? undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const userName = session?.user?.name ?? "Researcher";
|
|
||||||
|
|
||||||
const getWelcomeMessage = () => {
|
|
||||||
const hour = new Date().getHours();
|
const hour = new Date().getHours();
|
||||||
let greeting = "Good evening";
|
if (hour < 12) return "Good morning";
|
||||||
if (hour < 12) greeting = "Good morning";
|
if (hour < 18) return "Good afternoon";
|
||||||
else if (hour < 18) greeting = "Good afternoon";
|
return "Good evening";
|
||||||
|
})();
|
||||||
return `${greeting}, ${userName.split(" ")[0]}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-in fade-in space-y-8 duration-500">
|
<div className="min-h-screen bg-gradient-to-br from-background via-background to-primary/5 p-6 md:p-8">
|
||||||
{/* Header Section */}
|
{/* Header */}
|
||||||
<div
|
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
id="dashboard-header"
|
|
||||||
className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"
|
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-foreground text-3xl font-bold tracking-tight">
|
<h1 className="text-4xl font-bold tracking-tight">
|
||||||
{getWelcomeMessage()}
|
{greeting}, {userName.split(" ")[0]}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground mt-1">
|
||||||
Here's what's happening with your research today.
|
Ready to run your next session?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button variant="outline" asChild>
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => startTour("dashboard")}
|
|
||||||
title="Start Tour"
|
|
||||||
>
|
|
||||||
<HelpCircle className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<Select
|
|
||||||
value={studyFilter ?? "all"}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
setStudyFilter(value === "all" ? null : value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="bg-background w-[200px]">
|
|
||||||
<SelectValue placeholder="All Studies" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">All Studies</SelectItem>
|
|
||||||
{userStudies.map((study) => (
|
|
||||||
<SelectItem key={study.id} value={study.id}>
|
|
||||||
{study.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button id="tour-new-study" asChild>
|
|
||||||
<Link href="/studies/new">
|
<Link href="/studies/new">
|
||||||
<Plus className="mr-2 h-4 w-4" /> New Study
|
<FlaskConical className="mr-2 h-4 w-4" />
|
||||||
|
New Study
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild className="glow-teal">
|
||||||
|
<Link href={userStudies?.studies?.[0]?.id ? `/studies/${userStudies.studies[0].id}/trials/new` : "/studies/new"}>
|
||||||
|
<Play className="mr-2 h-4 w-4" />
|
||||||
|
Start Trial
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Stats Grid */}
|
{/* Live Trials Banner */}
|
||||||
<div
|
{liveTrials && liveTrials.length > 0 && (
|
||||||
id="tour-dashboard-stats"
|
<Card className="border-red-200 bg-red-50/50 mb-6 dark:border-red-900 dark:bg-red-900/20">
|
||||||
className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"
|
<CardContent className="flex items-center gap-4 py-4">
|
||||||
>
|
<div className="relative">
|
||||||
<StatsCard
|
<Radio className="h-8 w-8 text-red-600 animate-pulse" />
|
||||||
title="Active Trials"
|
<span className="absolute -right-1 -top-1 h-3 w-3 rounded-full bg-red-500 animate-ping" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="font-semibold text-red-600 dark:text-red-400">
|
||||||
|
{liveTrials.length} Active Session{liveTrials.length > 1 ? "s" : ""}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-red-600/70 dark:text-red-400/70">
|
||||||
|
{liveTrials.map((t) => t.participantCode).join(", ")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" variant="secondary" asChild>
|
||||||
|
<Link href={`/studies/${liveTrials?.[0]?.studyId}/trials/${liveTrials?.[0]?.id}/wizard`}>
|
||||||
|
View <ChevronRight className="ml-1 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Stats Row */}
|
||||||
|
<div className="mb-8 grid gap-4 md:grid-cols-4">
|
||||||
|
<StatCard
|
||||||
|
label="Active Trials"
|
||||||
value={stats?.activeTrials ?? 0}
|
value={stats?.activeTrials ?? 0}
|
||||||
icon={Activity}
|
icon={Activity}
|
||||||
description="Currently running sessions"
|
color="teal"
|
||||||
iconColor="text-emerald-500"
|
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatCard
|
||||||
title="Completed Today"
|
label="Completed Today"
|
||||||
value={stats?.completedToday ?? 0}
|
value={stats?.completedToday ?? 0}
|
||||||
icon={CheckCircle}
|
icon={CheckCircle2}
|
||||||
description="Successful completions"
|
color="emerald"
|
||||||
iconColor="text-blue-500"
|
|
||||||
/>
|
/>
|
||||||
<StatsCard
|
<StatCard
|
||||||
title="Scheduled"
|
label="Total Studies"
|
||||||
|
value={userStudies?.studies?.length ?? 0}
|
||||||
|
icon={FlaskConical}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Total Participants"
|
||||||
value={stats?.scheduledTrials ?? 0}
|
value={stats?.scheduledTrials ?? 0}
|
||||||
icon={Calendar}
|
icon={Users}
|
||||||
description="Upcoming sessions"
|
color="violet"
|
||||||
iconColor="text-violet-500"
|
|
||||||
/>
|
|
||||||
<StatsCard
|
|
||||||
title="Total Interventions"
|
|
||||||
value={stats?.totalInterventions ?? 0}
|
|
||||||
icon={Gamepad2}
|
|
||||||
description="Wizard manual overrides"
|
|
||||||
iconColor="text-orange-500"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Center & Recent Activity */}
|
{/* Main Grid */}
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
{/* Quick Actions Card */}
|
{/* Studies List */}
|
||||||
<Card className="from-primary/5 to-background border-primary/20 col-span-3 h-fit bg-gradient-to-br">
|
<Card className="lg:col-span-2">
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>Quick Actions</CardTitle>
|
<div>
|
||||||
<CardDescription>Common tasks to get you started</CardDescription>
|
<CardTitle className="flex items-center gap-2">
|
||||||
</CardHeader>
|
<FlaskConical className="h-5 w-5 text-primary" />
|
||||||
<CardContent className="grid gap-4">
|
Your Studies
|
||||||
<Button
|
</CardTitle>
|
||||||
variant="outline"
|
<CardDescription>Recent studies and quick access</CardDescription>
|
||||||
className="border-primary/20 hover:border-primary/50 hover:bg-primary/5 group h-auto justify-start px-4 py-4"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link href="/studies/new">
|
|
||||||
<div className="bg-primary/10 group-hover:bg-primary/20 mr-4 rounded-full p-2 transition-colors">
|
|
||||||
<FlaskConical className="text-primary h-5 w-5" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<Button variant="ghost" size="sm" asChild>
|
||||||
<div className="font-semibold">Create New Study</div>
|
|
||||||
<div className="text-muted-foreground text-xs font-normal">
|
|
||||||
Design a new experiment protocol
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ArrowRight className="text-muted-foreground group-hover:text-primary ml-auto h-4 w-4 opacity-0 transition-all group-hover:opacity-100" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="group h-auto justify-start px-4 py-4"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link href="/studies">
|
<Link href="/studies">
|
||||||
<div className="bg-secondary mr-4 rounded-full p-2">
|
View all <ChevronRight className="ml-1 h-4 w-4" />
|
||||||
<Search className="text-foreground h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div className="text-left">
|
|
||||||
<div className="font-semibold">Browse Studies</div>
|
|
||||||
<div className="text-muted-foreground text-xs font-normal">
|
|
||||||
Find and manage existing studies
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
<Button
|
<CardContent>
|
||||||
variant="outline"
|
<div className="space-y-3">
|
||||||
className="group h-auto justify-start px-4 py-4"
|
{userStudies?.studies?.slice(0, 5).map((study) => (
|
||||||
asChild
|
<Link
|
||||||
|
key={study.id}
|
||||||
|
href={`/studies/${study.id}`}
|
||||||
|
className="hover:bg-accent/50 group flex items-center justify-between rounded-lg border p-4 transition-colors"
|
||||||
>
|
>
|
||||||
<Link href={userStudies[0] ? `/studies/${userStudies[0].id}/trials` : "/studies"}>
|
<div className="flex items-center gap-4">
|
||||||
<div className="mr-4 rounded-full bg-emerald-500/10 p-2">
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
|
||||||
<Activity className="h-5 w-5 text-emerald-600" />
|
<Bot className="h-6 w-6 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-left">
|
<div>
|
||||||
<div className="font-semibold">Monitor Active Trials</div>
|
<p className="font-semibold group-hover:text-primary">
|
||||||
<div className="text-muted-foreground text-xs font-normal">
|
{study.name}
|
||||||
Jump into the Wizard Interface
|
</p>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{study.status === "active" ? (
|
||||||
|
<span className="text-emerald-600 dark:text-emerald-400">Active</span>
|
||||||
|
) : (
|
||||||
|
<span>{study.status}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ChevronRight className="h-5 w-5 text-muted-foreground group-hover:text-primary" />
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!userStudies?.studies?.length && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<Bot className="text-muted-foreground/50 mb-4 h-16 w-16" />
|
||||||
|
<p className="font-medium">No studies yet</p>
|
||||||
|
<p className="text-muted-foreground mb-4 text-sm">
|
||||||
|
Create your first study to get started
|
||||||
|
</p>
|
||||||
|
<Button asChild>
|
||||||
|
<Link href="/studies/new">
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create Study
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Quick Links & Recent */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">Quick Actions</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
<Button variant="outline" className="w-full justify-start" asChild>
|
||||||
|
<Link href="/studies/new">
|
||||||
|
<Plus className="mr-3 h-4 w-4" />
|
||||||
|
Create New Study
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="w-full justify-start" asChild>
|
||||||
|
<Link href={userStudies?.studies?.[0]?.id ? `/studies/${userStudies.studies[0].id}/experiments/new` : "/studies"}>
|
||||||
|
<FlaskConical className="mr-3 h-4 w-4" />
|
||||||
|
Design Experiment
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="w-full justify-start" asChild>
|
||||||
|
<Link href={userStudies?.studies?.[0]?.id ? `/studies/${userStudies.studies[0].id}/participants/new` : "/studies"}>
|
||||||
|
<Users className="mr-3 h-4 w-4" />
|
||||||
|
Add Participant
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" className="w-full justify-start" asChild>
|
||||||
|
<Link href={userStudies?.studies?.[0]?.id ? `/studies/${userStudies.studies[0].id}/trials/new` : "/studies"}>
|
||||||
|
<Play className="mr-3 h-4 w-4" />
|
||||||
|
Start Trial
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Recent Activity Card */}
|
{/* Recent Trials */}
|
||||||
<Card
|
<Card>
|
||||||
id="tour-recent-activity"
|
|
||||||
className="border-muted/40 col-span-4 shadow-sm"
|
|
||||||
>
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Recent Activity</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-lg">
|
||||||
<CardDescription>
|
<Clock className="h-4 w-4" />
|
||||||
Your latest interactions across the platform
|
Recent Trials
|
||||||
</CardDescription>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ScrollArea className="h-[400px] pr-4">
|
<ScrollArea className="h-[200px]">
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
{recentActivity?.map((activity) => {
|
{recentTrials?.slice(0, 5).map((trial) => (
|
||||||
let eventColor = "bg-primary/30 ring-background";
|
<Link
|
||||||
let Icon = Activity;
|
key={trial.id}
|
||||||
if (activity.type === "trial_started") {
|
href={`/studies/${trial.experiment.studyId}/trials/${trial.id}`}
|
||||||
eventColor = "bg-blue-500 ring-blue-100 dark:ring-blue-900";
|
className="hover:bg-accent/50 block rounded-md border p-3 transition-colors"
|
||||||
Icon = PlayCircle;
|
>
|
||||||
} else if (activity.type === "trial_completed") {
|
<div className="flex items-center justify-between">
|
||||||
eventColor =
|
<span className="font-medium">{trial.participant.participantCode}</span>
|
||||||
"bg-green-500 ring-green-100 dark:ring-green-900";
|
<Badge
|
||||||
Icon = CheckCircle;
|
variant={
|
||||||
} else if (activity.type === "error") {
|
trial.status === "completed"
|
||||||
eventColor = "bg-red-500 ring-red-100 dark:ring-red-900";
|
? "default"
|
||||||
Icon = AlertTriangle;
|
: trial.status === "in_progress"
|
||||||
} else if (activity.type === "intervention") {
|
? "secondary"
|
||||||
eventColor =
|
: "outline"
|
||||||
"bg-orange-500 ring-orange-100 dark:ring-orange-900";
|
|
||||||
Icon = Gamepad2;
|
|
||||||
} else if (activity.type === "annotation") {
|
|
||||||
eventColor =
|
|
||||||
"bg-yellow-500 ring-yellow-100 dark:ring-yellow-900";
|
|
||||||
Icon = MessageSquare;
|
|
||||||
}
|
}
|
||||||
|
className="text-xs"
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={activity.id}
|
|
||||||
className="border-muted-foreground/20 relative border-l pb-4 pl-6 last:border-0"
|
|
||||||
>
|
>
|
||||||
<span
|
{trial.status.replace("_", " ")}
|
||||||
className={`absolute top-0 left-[-9px] flex h-4 w-4 items-center justify-center rounded-full ring-4 ${eventColor}`}
|
</Badge>
|
||||||
>
|
|
||||||
<Icon className="h-2.5 w-2.5 text-white" />
|
|
||||||
</span>
|
|
||||||
<div className="mb-0.5 text-sm leading-none font-medium">
|
|
||||||
{activity.title}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground mb-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
{activity.description}
|
{trial.experiment.name}
|
||||||
</div>
|
</p>
|
||||||
<div className="text-muted-foreground/70 font-mono text-[10px] uppercase">
|
{trial.completedAt && (
|
||||||
{formatDistanceToNow(new Date(activity.time), {
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
addSuffix: true,
|
{formatDistanceToNow(new Date(trial.completedAt), { addSuffix: true })}
|
||||||
})}
|
</p>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</Link>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
{!recentActivity?.length && (
|
{!recentTrials?.length && (
|
||||||
<div className="text-muted-foreground flex flex-col items-center justify-center py-8 text-center">
|
<p className="text-muted-foreground py-4 text-center text-sm">
|
||||||
<Clock className="mb-3 h-10 w-10 opacity-20" />
|
No trials yet
|
||||||
<p>No recent activity recorded.</p>
|
|
||||||
<p className="mt-1 text-xs">
|
|
||||||
Start a trial to see experiment events stream here.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
|
||||||
{/* Live Trials */}
|
|
||||||
<Card
|
|
||||||
id="tour-live-trials"
|
|
||||||
className={`${liveTrials && liveTrials.length > 0 ? "border-primary bg-primary/5 shadow-sm" : "border-muted/40"} col-span-4 transition-colors duration-500`}
|
|
||||||
>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
Live Sessions
|
|
||||||
{liveTrials && liveTrials.length > 0 && (
|
|
||||||
<span className="relative flex h-3 w-3">
|
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75"></span>
|
|
||||||
<span className="relative inline-flex h-3 w-3 rounded-full bg-red-500"></span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Currently running trials in the Wizard interface
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Button variant="ghost" size="sm" asChild>
|
|
||||||
<Link href={userStudies[0] ? `/studies/${userStudies[0].id}/trials` : "/studies"}>
|
|
||||||
View All <ArrowRight className="ml-2 h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{!liveTrials?.length ? (
|
|
||||||
<div className="border-muted-foreground/30 animate-in fade-in-50 bg-background/50 flex h-[150px] flex-col items-center justify-center rounded-md border border-dashed text-center">
|
|
||||||
<Radio className="text-muted-foreground/50 mb-2 h-8 w-8" />
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
No trials are currently running.
|
|
||||||
</p>
|
|
||||||
<Button variant="link" size="sm" asChild className="mt-1">
|
|
||||||
<Link href={userStudies[0] ? `/studies/${userStudies[0].id}/trials/new` : "/studies"}>Start a Trial</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{liveTrials.map((trial) => (
|
|
||||||
<div
|
|
||||||
key={trial.id}
|
|
||||||
className="border-primary/20 bg-background flex items-center justify-between rounded-lg border p-3 shadow-sm transition-all duration-200 hover:shadow"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-red-100 text-red-600 dark:bg-red-900/40 dark:text-red-400">
|
|
||||||
<Radio className="h-5 w-5 animate-pulse" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">
|
|
||||||
{trial.participantCode}
|
|
||||||
<span className="text-muted-foreground ml-2 text-xs font-normal">
|
|
||||||
• {trial.experimentName}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
Started{" "}
|
|
||||||
{trial.startedAt
|
|
||||||
? formatDistanceToNow(new Date(trial.startedAt), {
|
|
||||||
addSuffix: true,
|
|
||||||
})
|
|
||||||
: "just now"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="bg-primary hover:bg-primary/90 gap-2"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link href={`/studies/${trial.studyId}/trials/${trial.id}/wizard`}>
|
|
||||||
<Play className="h-3.5 w-3.5" /> Spectate / Jump In
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Study Progress */}
|
|
||||||
<Card className="border-muted/40 col-span-3 shadow-sm">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Study Progress</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Completion tracking for active studies
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
{studyProgress?.map((study) => (
|
|
||||||
<div key={study.id} className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between text-sm">
|
|
||||||
<div className="font-medium">{study.name}</div>
|
|
||||||
<div className="text-muted-foreground">
|
|
||||||
{study.participants} / {study.totalParticipants}{" "}
|
|
||||||
Participants
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Progress value={study.progress} className="h-2" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{!studyProgress?.length && (
|
|
||||||
<p className="text-muted-foreground py-4 text-center text-sm">
|
|
||||||
No active studies to track.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatsCard({
|
function StatCard({
|
||||||
title,
|
label,
|
||||||
value,
|
value,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
description,
|
color,
|
||||||
trend,
|
|
||||||
iconColor,
|
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
label: string;
|
||||||
value: string | number;
|
value: number;
|
||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
description: string;
|
color: "teal" | "emerald" | "blue" | "violet";
|
||||||
trend?: string;
|
|
||||||
iconColor?: string;
|
|
||||||
}) {
|
}) {
|
||||||
|
const colorClasses = {
|
||||||
|
teal: "bg-teal-500/10 text-teal-600 dark:text-teal-400",
|
||||||
|
emerald: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
||||||
|
blue: "bg-blue-500/10 text-blue-600 dark:text-blue-400",
|
||||||
|
violet: "bg-violet-500/10 text-violet-600 dark:text-violet-400",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-muted/40 hover:border-primary/20 shadow-sm transition-all duration-200 hover:shadow-md">
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardContent className="flex items-center gap-4 p-6">
|
||||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
<div className={`rounded-full p-3 ${colorClasses[color]}`}>
|
||||||
<Icon className={`h-4 w-4 ${iconColor || "text-muted-foreground"}`} />
|
<Icon className="h-6 w-6" />
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
<div>
|
||||||
<div className="text-2xl font-bold">{value}</div>
|
<p className="text-3xl font-bold">{value}</p>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-sm">{label}</p>
|
||||||
{description}
|
</div>
|
||||||
{trend && (
|
|
||||||
<span className="ml-1 font-medium text-green-600 dark:text-green-400">
|
|
||||||
{trend}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user