mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-12 07:04:44 -05:00
Studies, basic experiment designer
This commit is contained in:
26
src/app/(dashboard)/experiments/[id]/designer/page.tsx
Normal file
26
src/app/(dashboard)/experiments/[id]/designer/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { ExperimentDesignerClient } from "~/components/experiments/designer/ExperimentDesignerClient";
|
||||
import { api } from "~/trpc/server";
|
||||
|
||||
interface ExperimentDesignerPageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ExperimentDesignerPage({
|
||||
params,
|
||||
}: ExperimentDesignerPageProps) {
|
||||
try {
|
||||
const experiment = await api.experiments.get({ id: params.id });
|
||||
|
||||
if (!experiment) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <ExperimentDesignerClient experiment={experiment} />;
|
||||
} catch (error) {
|
||||
console.error("Error loading experiment:", error);
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
344
src/app/(dashboard)/experiments/new/page.tsx
Normal file
344
src/app/(dashboard)/experiments/new/page.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, FlaskConical } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/ui/select";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
const createExperimentSchema = z.object({
|
||||
name: z.string().min(1, "Experiment name is required").max(100, "Name too long"),
|
||||
description: z
|
||||
.string()
|
||||
.min(10, "Description must be at least 10 characters")
|
||||
.max(1000, "Description too long"),
|
||||
studyId: z.string().uuid("Please select a study"),
|
||||
estimatedDuration: z
|
||||
.number()
|
||||
.min(1, "Duration must be at least 1 minute")
|
||||
.max(480, "Duration cannot exceed 8 hours")
|
||||
.optional(),
|
||||
status: z.enum(["draft", "active", "completed", "archived"]),
|
||||
});
|
||||
|
||||
type CreateExperimentFormData = z.infer<typeof createExperimentSchema>;
|
||||
|
||||
export default function NewExperimentPage() {
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<CreateExperimentFormData>({
|
||||
resolver: zodResolver(createExperimentSchema),
|
||||
defaultValues: {
|
||||
status: "draft" as const,
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch user's studies for the dropdown
|
||||
const { data: studiesData, isLoading: studiesLoading } = api.studies.list.useQuery(
|
||||
{ memberOnly: true },
|
||||
);
|
||||
|
||||
const createExperimentMutation = api.experiments.create.useMutation({
|
||||
onSuccess: (experiment) => {
|
||||
router.push(`/experiments/${experiment.id}/designer`);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to create experiment:", error);
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: CreateExperimentFormData) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await createExperimentMutation.mutateAsync({
|
||||
...data,
|
||||
estimatedDuration: data.estimatedDuration || null,
|
||||
});
|
||||
} catch (error) {
|
||||
// Error handling is done in the mutation's onError callback
|
||||
}
|
||||
};
|
||||
|
||||
const watchedStatus = watch("status");
|
||||
const watchedStudyId = watch("studyId");
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center space-x-2 text-sm text-slate-600 mb-4">
|
||||
<Link href="/experiments" className="hover:text-slate-900 flex items-center">
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
Experiments
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-slate-900">New Experiment</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-blue-100">
|
||||
<FlaskConical className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900">Create New Experiment</h1>
|
||||
<p className="text-slate-600">Design a new experimental protocol for your HRI study</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Form */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Experiment Details</CardTitle>
|
||||
<CardDescription>
|
||||
Define the basic information for your experiment. You'll design the protocol steps next.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||
{/* Experiment Name */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Experiment Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
{...register("name")}
|
||||
placeholder="Enter experiment name..."
|
||||
className={errors.name ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-sm text-red-600">{errors.name.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description *</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
{...register("description")}
|
||||
placeholder="Describe the experiment objectives, methodology, and expected outcomes..."
|
||||
rows={4}
|
||||
className={errors.description ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.description && (
|
||||
<p className="text-sm text-red-600">{errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Study Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="studyId">Study *</Label>
|
||||
<Select
|
||||
value={watchedStudyId}
|
||||
onValueChange={(value) => setValue("studyId", value)}
|
||||
disabled={studiesLoading}
|
||||
>
|
||||
<SelectTrigger className={errors.studyId ? "border-red-500" : ""}>
|
||||
<SelectValue placeholder={studiesLoading ? "Loading studies..." : "Select a study"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{studiesData?.studies?.map((study) => (
|
||||
<SelectItem key={study.id} value={study.id}>
|
||||
{study.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.studyId && (
|
||||
<p className="text-sm text-red-600">{errors.studyId.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Estimated Duration */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="estimatedDuration">Estimated Duration (minutes)</Label>
|
||||
<Input
|
||||
id="estimatedDuration"
|
||||
type="number"
|
||||
min="1"
|
||||
max="480"
|
||||
{...register("estimatedDuration", { valueAsNumber: true })}
|
||||
placeholder="e.g., 30"
|
||||
className={errors.estimatedDuration ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.estimatedDuration && (
|
||||
<p className="text-sm text-red-600">{errors.estimatedDuration.message}</p>
|
||||
)}
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Optional: How long do you expect this experiment to take per participant?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status">Initial Status</Label>
|
||||
<Select
|
||||
value={watchedStatus}
|
||||
onValueChange={(value) =>
|
||||
setValue("status", value as "draft" | "active" | "completed" | "archived")
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="draft">Draft - Design in progress</SelectItem>
|
||||
<SelectItem value="active">Active - Ready for trials</SelectItem>
|
||||
<SelectItem value="completed">Completed - Data collection finished</SelectItem>
|
||||
<SelectItem value="archived">Archived - Experiment concluded</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{createExperimentMutation.error && (
|
||||
<div className="rounded-md bg-red-50 p-3">
|
||||
<p className="text-sm text-red-800">
|
||||
Failed to create experiment: {createExperimentMutation.error.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form Actions */}
|
||||
<Separator />
|
||||
<div className="flex justify-end space-x-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => router.back()}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || studiesLoading}
|
||||
className="min-w-[140px]"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<svg className="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Creating...</span>
|
||||
</div>
|
||||
) : (
|
||||
"Create & Design"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Next Steps */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<FlaskConical className="h-5 w-5" />
|
||||
<span>What's Next?</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="mt-1 h-2 w-2 rounded-full bg-blue-600"></div>
|
||||
<div>
|
||||
<p className="font-medium">Design Protocol</p>
|
||||
<p className="text-slate-600">Use the visual designer to create experiment steps</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="mt-1 h-2 w-2 rounded-full bg-slate-300"></div>
|
||||
<div>
|
||||
<p className="font-medium">Configure Actions</p>
|
||||
<p className="text-slate-600">Set up robot actions and wizard controls</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="mt-1 h-2 w-2 rounded-full bg-slate-300"></div>
|
||||
<div>
|
||||
<p className="font-medium">Test & Validate</p>
|
||||
<p className="text-slate-600">Run test trials to verify the protocol</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="mt-1 h-2 w-2 rounded-full bg-slate-300"></div>
|
||||
<div>
|
||||
<p className="font-medium">Schedule Trials</p>
|
||||
<p className="text-slate-600">Begin data collection with participants</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Tips */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>💡 Tips</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-slate-600">
|
||||
<p>
|
||||
<strong>Start simple:</strong> Begin with a basic protocol and add complexity later.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Plan interactions:</strong> Consider both robot behaviors and participant responses.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Test early:</strong> Validate your protocol with team members before recruiting participants.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/app/(dashboard)/experiments/page.tsx
Normal file
18
src/app/(dashboard)/experiments/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ExperimentsGrid } from "~/components/experiments/ExperimentsGrid";
|
||||
|
||||
export default function ExperimentsPage() {
|
||||
return (
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-900">Experiments</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Design and manage experimental protocols for your HRI studies
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Experiments Grid */}
|
||||
<ExperimentsGrid />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
187
src/app/(dashboard)/layout.tsx
Normal file
187
src/app/(dashboard)/layout.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { auth } from "~/server/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import {
|
||||
Users,
|
||||
FlaskConical,
|
||||
Play,
|
||||
BarChart3,
|
||||
Settings,
|
||||
User,
|
||||
LogOut,
|
||||
Home,
|
||||
UserCog,
|
||||
} from "lucide-react";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
label: "Studies",
|
||||
href: "/studies",
|
||||
icon: FlaskConical,
|
||||
roles: ["administrator", "researcher", "wizard", "observer"],
|
||||
},
|
||||
{
|
||||
label: "Experiments",
|
||||
href: "/experiments",
|
||||
icon: Settings,
|
||||
roles: ["administrator", "researcher"],
|
||||
},
|
||||
{
|
||||
label: "Trials",
|
||||
href: "/trials",
|
||||
icon: Play,
|
||||
roles: ["administrator", "researcher", "wizard"],
|
||||
},
|
||||
{
|
||||
label: "Analytics",
|
||||
href: "/analytics",
|
||||
icon: BarChart3,
|
||||
roles: ["administrator", "researcher"],
|
||||
},
|
||||
{
|
||||
label: "Participants",
|
||||
href: "/participants",
|
||||
icon: Users,
|
||||
roles: ["administrator", "researcher"],
|
||||
},
|
||||
];
|
||||
|
||||
const adminItems = [
|
||||
{
|
||||
label: "Administration",
|
||||
href: "/admin",
|
||||
icon: UserCog,
|
||||
roles: ["administrator"],
|
||||
},
|
||||
];
|
||||
|
||||
export default async function DashboardLayout({
|
||||
children,
|
||||
}: DashboardLayoutProps) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
redirect("/auth/signin");
|
||||
}
|
||||
|
||||
const userRole = session.user.roles[0]?.role || "observer";
|
||||
const userName = session.user.name || session.user.email;
|
||||
|
||||
// Filter navigation items based on user role
|
||||
const allowedNavItems = navigationItems.filter((item) =>
|
||||
item.roles.includes(userRole),
|
||||
);
|
||||
const allowedAdminItems = adminItems.filter((item) =>
|
||||
item.roles.includes(userRole),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
{/* Sidebar */}
|
||||
<div className="fixed inset-y-0 left-0 z-50 w-64 border-r border-slate-200 bg-white">
|
||||
{/* Header */}
|
||||
<div className="flex h-16 items-center border-b border-slate-200 px-6">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600">
|
||||
<FlaskConical className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="text-xl font-bold text-slate-900">HRIStudio</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex h-full flex-col">
|
||||
<nav className="flex-1 space-y-2 px-4 py-6">
|
||||
{/* Main Navigation */}
|
||||
<div className="space-y-1">
|
||||
{allowedNavItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Admin Section */}
|
||||
{allowedAdminItems.length > 0 && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-1">
|
||||
<h3 className="px-3 text-xs font-semibold tracking-wider text-slate-500 uppercase">
|
||||
Administration
|
||||
</h3>
|
||||
{allowedAdminItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
<item.icon className="h-5 w-5" />
|
||||
<span>{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* User Section */}
|
||||
<div className="border-t border-slate-200 p-4">
|
||||
<div className="mb-3 flex items-center space-x-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100">
|
||||
<User className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-slate-900">
|
||||
{userName}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 capitalize">{userRole}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Link
|
||||
href="/profile"
|
||||
className="flex w-full items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<span>Profile</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/"
|
||||
className="flex w-full items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
<span>Home</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/auth/signout"
|
||||
className="flex w-full items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-700"
|
||||
>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span>Sign Out</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="pl-64">
|
||||
<main className="min-h-screen">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
315
src/app/(dashboard)/studies/[id]/page.tsx
Normal file
315
src/app/(dashboard)/studies/[id]/page.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import {
|
||||
Users,
|
||||
FlaskConical,
|
||||
Calendar,
|
||||
Building,
|
||||
Shield,
|
||||
Settings,
|
||||
Plus,
|
||||
BarChart3,
|
||||
} from "lucide-react";
|
||||
import { api } from "~/trpc/server";
|
||||
|
||||
interface StudyDetailPageProps {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
draft: {
|
||||
label: "Draft",
|
||||
className: "bg-gray-100 text-gray-800",
|
||||
icon: "📝",
|
||||
},
|
||||
active: {
|
||||
label: "Active",
|
||||
className: "bg-green-100 text-green-800",
|
||||
icon: "🟢",
|
||||
},
|
||||
completed: {
|
||||
label: "Completed",
|
||||
className: "bg-blue-100 text-blue-800",
|
||||
icon: "✅",
|
||||
},
|
||||
archived: {
|
||||
label: "Archived",
|
||||
className: "bg-orange-100 text-orange-800",
|
||||
icon: "📦",
|
||||
},
|
||||
};
|
||||
|
||||
export default async function StudyDetailPage({ params }: StudyDetailPageProps) {
|
||||
try {
|
||||
const study = await api.studies.get({ id: params.id });
|
||||
const members = await api.studies.getMembers({ studyId: params.id });
|
||||
|
||||
if (!study) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const statusInfo = statusConfig[study.status];
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center space-x-2 text-sm text-slate-600 mb-4">
|
||||
<Link href="/studies" className="hover:text-slate-900">
|
||||
Studies
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-slate-900">{study.name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
<h1 className="text-3xl font-bold text-slate-900 truncate">
|
||||
{study.name}
|
||||
</h1>
|
||||
<Badge className={statusInfo.className} variant="secondary">
|
||||
<span className="mr-1">{statusInfo.icon}</span>
|
||||
{statusInfo.label}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-slate-600 text-lg">{study.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/studies/${study.id}/edit`}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Edit Study
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href={`/studies/${study.id}/experiments/new`}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Experiment
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* 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 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-700">Institution</label>
|
||||
<p className="text-slate-900">{study.institution}</p>
|
||||
</div>
|
||||
{study.irbProtocolNumber && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-700">IRB Protocol</label>
|
||||
<p className="text-slate-900">{study.irbProtocolNumber}</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="h-4 w-4 mr-2" />
|
||||
Add Experiment
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Design and manage experimental protocols for this study
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Placeholder for experiments list */}
|
||||
<div className="text-center py-8">
|
||||
<FlaskConical className="h-12 w-12 text-slate-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-2">
|
||||
No Experiments Yet
|
||||
</h3>
|
||||
<p className="text-slate-600 mb-4">
|
||||
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="text-center py-8">
|
||||
<Calendar className="h-12 w-12 text-slate-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-2">
|
||||
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="h-4 w-4 mr-2" />
|
||||
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="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-900 truncate">
|
||||
{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="h-4 w-4 mr-2" />
|
||||
Manage Participants
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="w-full justify-start">
|
||||
<Link href={`/studies/${study.id}/trials`}>
|
||||
<Calendar className="h-4 w-4 mr-2" />
|
||||
Schedule Trials
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" className="w-full justify-start">
|
||||
<Link href={`/studies/${study.id}/analytics`}>
|
||||
<BarChart3 className="h-4 w-4 mr-2" />
|
||||
View Analytics
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error loading study:", error);
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
@@ -1,155 +1,18 @@
|
||||
import { auth } from "~/server/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
|
||||
export default async function StudiesPage() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
redirect("/auth/signin");
|
||||
}
|
||||
import { StudiesGrid } from "~/components/studies/StudiesGrid";
|
||||
|
||||
export default function StudiesPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900">Studies</h1>
|
||||
<p className="text-slate-600">
|
||||
Manage your Human-Robot Interaction research studies
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-slate-600">
|
||||
Welcome, {session.user.name ?? session.user.email}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/auth/signout">Sign Out</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/">← Back to Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Studies Grid */}
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Create New Study Card */}
|
||||
<Card className="border-2 border-dashed border-slate-300 transition-colors hover:border-slate-400">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-lg bg-blue-100">
|
||||
<svg
|
||||
className="h-8 w-8 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<CardTitle>Create New Study</CardTitle>
|
||||
<CardDescription>Start a new HRI research study</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button className="w-full" disabled>
|
||||
Create Study
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Example Study Cards */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Robot Navigation Study</CardTitle>
|
||||
<CardDescription>
|
||||
Investigating user preferences for robot navigation patterns
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-between text-sm text-slate-600">
|
||||
<span>Created: Dec 2024</span>
|
||||
<span>Status: Active</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" className="flex-1" disabled>
|
||||
View Details
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="flex-1" disabled>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Social Robot Interaction</CardTitle>
|
||||
<CardDescription>
|
||||
Analyzing human responses to social robot behaviors
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-between text-sm text-slate-600">
|
||||
<span>Created: Nov 2024</span>
|
||||
<span>Status: Draft</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" className="flex-1" disabled>
|
||||
View Details
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" className="flex-1" disabled>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Empty State for No Studies */}
|
||||
<div className="mt-12 text-center">
|
||||
<div className="mx-auto mb-4 flex h-24 w-24 items-center justify-center rounded-lg bg-slate-100">
|
||||
<svg
|
||||
className="h-12 w-12 text-slate-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="mb-2 text-lg font-semibold text-slate-900">
|
||||
Authentication Test Successful!
|
||||
</h3>
|
||||
<p className="mb-4 text-slate-600">
|
||||
You're viewing a protected page. The authentication system is
|
||||
working correctly. This page will be replaced with actual study
|
||||
management functionality.
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">User ID: {session.user.id}</p>
|
||||
</div>
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-slate-900">Studies</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Manage your Human-Robot Interaction research studies
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Studies Grid */}
|
||||
<StudiesGrid />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user