mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
feat: Enhance trial event display with improved formatting and icons, refine trial wizard panels, and update dashboard page layouts.
This commit is contained in:
@@ -16,8 +16,19 @@ import { Separator } from "~/components/ui/separator";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { formatRole, getRoleDescription } from "~/lib/auth-client";
|
||||
import { User, Shield, Download, Trash2, ExternalLink } from "lucide-react";
|
||||
import {
|
||||
User,
|
||||
Shield,
|
||||
Download,
|
||||
Trash2,
|
||||
ExternalLink,
|
||||
Lock,
|
||||
UserCog,
|
||||
Mail,
|
||||
Fingerprint
|
||||
} from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
interface ProfileUser {
|
||||
id: string;
|
||||
@@ -32,185 +43,141 @@ interface ProfileUser {
|
||||
|
||||
function ProfileContent({ user }: { user: ProfileUser }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8 animate-in fade-in duration-500">
|
||||
<PageHeader
|
||||
title="Profile"
|
||||
description="Manage your account settings and preferences"
|
||||
title={user.name ?? "User"}
|
||||
description={user.email}
|
||||
icon={User}
|
||||
badges={[
|
||||
{ label: `ID: ${user.id}`, variant: "outline" },
|
||||
...(user.roles?.map((r) => ({
|
||||
label: formatRole(r.role),
|
||||
variant: "secondary" as const,
|
||||
})) ?? []),
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Profile Information */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Basic Information */}
|
||||
<Card className="hover:shadow-md transition-shadow duration-200">
|
||||
<CardHeader>
|
||||
<CardTitle>Basic Information</CardTitle>
|
||||
<CardDescription>
|
||||
Your personal account information
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ProfileEditForm
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
|
||||
{/* Main Content (Left Column) */}
|
||||
<div className="space-y-8 lg:col-span-2">
|
||||
|
||||
{/* Password Change */}
|
||||
<Card className="hover:shadow-md transition-shadow duration-200">
|
||||
<CardHeader>
|
||||
<CardTitle>Password</CardTitle>
|
||||
<CardDescription>Change your account password</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PasswordChangeForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Personal Information */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b">
|
||||
<User className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Personal Information</h3>
|
||||
</div>
|
||||
<Card className="border-border/60 hover:border-border transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Contact Details</CardTitle>
|
||||
<CardDescription>Update your public profile information</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ProfileEditForm
|
||||
user={{
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* Account Actions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Actions</CardTitle>
|
||||
<CardDescription>Manage your account settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium">Export Data</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Download all your research data and account information
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" disabled>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
Export Data
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-destructive text-sm font-medium">
|
||||
Delete Account
|
||||
</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Permanently delete your account and all associated data
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="destructive" disabled>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Security */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b">
|
||||
<Lock className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Security</h3>
|
||||
</div>
|
||||
<Card className="border-border/60 hover:border-border transition-colors">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Password</CardTitle>
|
||||
<CardDescription>Ensure your account stays secure</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PasswordChangeForm />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* User Summary */}
|
||||
<Card className="hover:shadow-md transition-shadow duration-200">
|
||||
<CardHeader>
|
||||
<CardTitle>Account Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="bg-primary/10 flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<span className="text-primary text-lg font-semibold">
|
||||
{(user.name ?? user.email ?? "U").charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{user.name ?? "Unnamed User"}</p>
|
||||
<p className="text-muted-foreground text-sm">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Sidebar (Right Column) */}
|
||||
<div className="space-y-8">
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-sm font-medium">User ID</p>
|
||||
<p className="text-muted-foreground bg-muted rounded p-2 font-mono text-xs break-all">
|
||||
{user.id}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* System Roles */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
System Roles
|
||||
</CardTitle>
|
||||
<CardDescription>Your current system permissions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{user.roles && user.roles.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{user.roles.map((roleInfo, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start justify-between"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Badge variant="secondary">
|
||||
{formatRole(roleInfo.role)}
|
||||
</Badge>
|
||||
{/* Permissions */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b">
|
||||
<Shield className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Permissions</h3>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
{user.roles && user.roles.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{user.roles.map((roleInfo, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-sm">{formatRole(roleInfo.role)}</span>
|
||||
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
|
||||
Since {new Date(roleInfo.grantedAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
{getRoleDescription(roleInfo.role)}
|
||||
</p>
|
||||
<p className="text-muted-foreground/80 mt-1 text-xs">
|
||||
Granted{" "}
|
||||
{new Date(roleInfo.grantedAt).toLocaleDateString()}
|
||||
</p>
|
||||
{index < (user.roles?.length || 0) - 1 && <Separator className="my-2" />}
|
||||
</div>
|
||||
))}
|
||||
<div className="bg-blue-50/50 dark:bg-blue-900/10 p-3 rounded-lg border border-blue-100 dark:border-blue-900/30 text-xs text-muted-foreground mt-4">
|
||||
<div className="flex items-center gap-2 mb-1 text-primary font-medium">
|
||||
<Shield className="h-3 w-3" />
|
||||
<span>Role Management</span>
|
||||
</div>
|
||||
System roles are managed by administrators. Contact support if you need access adjustments.
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Need additional permissions?{" "}
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="h-auto p-0 text-xs"
|
||||
>
|
||||
Contact an administrator
|
||||
<ExternalLink className="ml-1 h-3 w-3" />
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-6 text-center">
|
||||
<div className="bg-muted mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<Shield className="text-muted-foreground h-6 w-6" />
|
||||
) : (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-sm font-medium">No Roles Assigned</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">Contact an admin to request access.</p>
|
||||
<Button size="sm" variant="outline" className="mt-3 w-full">Request Access</Button>
|
||||
</div>
|
||||
<p className="mb-1 text-sm font-medium">No Roles Assigned</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
You don't have any system roles yet. Contact an
|
||||
administrator to get access to HRIStudio features.
|
||||
</p>
|
||||
<Button size="sm" variant="outline">
|
||||
Request Access
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* Data & Privacy */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center gap-2 pb-2 border-b">
|
||||
<Download className="h-5 w-5 text-primary" />
|
||||
<h3 className="text-lg font-semibold">Data & Privacy</h3>
|
||||
</div>
|
||||
|
||||
<Card className="border-destructive/10 bg-destructive/5 overflow-hidden">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold mb-1">Export Data</h4>
|
||||
<p className="text-xs text-muted-foreground mb-3">Download a copy of your personal data.</p>
|
||||
<Button variant="outline" size="sm" className="w-full bg-background" disabled>
|
||||
<Download className="mr-2 h-3 w-3" />
|
||||
Download Archive
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Separator className="bg-destructive/10" />
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-destructive mb-1">Delete Account</h4>
|
||||
<p className="text-xs text-muted-foreground mb-3">This action is irreversible.</p>
|
||||
<Button variant="destructive" size="sm" className="w-full" disabled>
|
||||
<Trash2 className="mr-2 h-3 w-3" />
|
||||
Delete Account
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,13 +185,17 @@ function ProfileContent({ user }: { user: ProfileUser }) {
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { data: session } = useSession();
|
||||
const { data: session, status } = useSession();
|
||||
|
||||
useBreadcrumbsEffect([
|
||||
{ label: "Dashboard", href: "/dashboard" },
|
||||
{ label: "Profile" },
|
||||
]);
|
||||
|
||||
if (status === "loading") {
|
||||
return <div className="p-8 text-muted-foreground animate-pulse">Loading profile...</div>;
|
||||
}
|
||||
|
||||
if (!session?.user) {
|
||||
redirect("/auth/signin");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Calendar, Clock, Edit, Play, Settings, Users } from "lucide-react";
|
||||
import { Calendar, Clock, Edit, Play, Settings, Users, TestTube } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
import {
|
||||
EntityView,
|
||||
EntityViewHeader,
|
||||
@@ -183,18 +184,19 @@ export default function ExperimentDetailPage({
|
||||
|
||||
return (
|
||||
<EntityView>
|
||||
<EntityViewHeader
|
||||
<PageHeader
|
||||
title={displayName}
|
||||
subtitle={description ?? undefined}
|
||||
icon="TestTube"
|
||||
status={{
|
||||
label: statusInfo?.label ?? "Unknown",
|
||||
variant: statusInfo?.variant ?? "secondary",
|
||||
icon: statusInfo?.icon ?? "TestTube",
|
||||
}}
|
||||
description={description ?? undefined}
|
||||
icon={TestTube}
|
||||
badges={[
|
||||
{
|
||||
label: statusInfo?.label ?? "Unknown",
|
||||
variant: statusInfo?.variant ?? "secondary",
|
||||
}
|
||||
]}
|
||||
actions={
|
||||
canEdit ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/studies/${studyId}/experiments/${experimentId}/designer`}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
@@ -209,7 +211,7 @@ export default function ExperimentDetailPage({
|
||||
Start Trial
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { Plus, Settings, Shield } from "lucide-react";
|
||||
import { Plus, Settings, Shield, Building } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
QuickActions,
|
||||
StatsGrid,
|
||||
} from "~/components/ui/entity-view";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
import { useBreadcrumbsEffect } from "~/components/ui/breadcrumb-provider";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { api } from "~/trpc/react";
|
||||
@@ -167,17 +168,18 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
|
||||
return (
|
||||
<EntityView>
|
||||
{/* Header */}
|
||||
<EntityViewHeader
|
||||
<PageHeader
|
||||
title={study.name}
|
||||
subtitle={study.description ?? undefined}
|
||||
icon="Building"
|
||||
status={{
|
||||
label: statusInfo?.label ?? "Unknown",
|
||||
variant: statusInfo?.variant ?? "secondary",
|
||||
icon: statusInfo?.icon ?? "FileText",
|
||||
}}
|
||||
description={study.description ?? undefined}
|
||||
icon={Building}
|
||||
badges={[
|
||||
{
|
||||
label: statusInfo?.label ?? "Unknown",
|
||||
variant: statusInfo?.variant ?? "secondary",
|
||||
}
|
||||
]}
|
||||
actions={
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/studies/${study.id}/edit`}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
@@ -190,7 +192,7 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
|
||||
New Experiment
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -271,10 +273,10 @@ export default function StudyDetailPage({ params }: StudyDetailPageProps) {
|
||||
</h4>
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${experiment.status === "draft"
|
||||
? "bg-gray-100 text-gray-800"
|
||||
: experiment.status === "ready"
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-blue-100 text-blue-800"
|
||||
? "bg-gray-100 text-gray-800"
|
||||
: experiment.status === "ready"
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-blue-100 text-blue-800"
|
||||
}`}
|
||||
>
|
||||
{experiment.status}
|
||||
|
||||
@@ -10,8 +10,9 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "~/com
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Edit } from "lucide-react";
|
||||
import { Edit, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { PageHeader } from "~/components/ui/page-header";
|
||||
|
||||
import { ParticipantConsentManager } from "~/components/participants/ParticipantConsentManager";
|
||||
|
||||
@@ -37,14 +38,16 @@ export default async function ParticipantDetailPage({
|
||||
|
||||
return (
|
||||
<EntityView>
|
||||
<EntityViewHeader
|
||||
<PageHeader
|
||||
title={participant.participantCode}
|
||||
subtitle={participant.name ?? "Unnamed Participant"}
|
||||
icon="Users"
|
||||
status={{
|
||||
label: participant.consentGiven ? "Consent Given" : "No Consent",
|
||||
variant: participant.consentGiven ? "default" : "secondary"
|
||||
}}
|
||||
description={participant.name ?? "Unnamed Participant"}
|
||||
icon={Users}
|
||||
badges={[
|
||||
{
|
||||
label: participant.consentGiven ? "Consent Given" : "No Consent",
|
||||
variant: participant.consentGiven ? "default" : "secondary"
|
||||
}
|
||||
]}
|
||||
actions={
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href={`/studies/${studyId}/participants/${participantId}/edit`}>
|
||||
|
||||
@@ -86,7 +86,7 @@ function AnalysisPageContent() {
|
||||
);
|
||||
}
|
||||
|
||||
const trialData = {
|
||||
const customTrialData = {
|
||||
...trial,
|
||||
startedAt: trial.startedAt ? new Date(trial.startedAt) : null,
|
||||
completedAt: trial.completedAt ? new Date(trial.completedAt) : null,
|
||||
@@ -96,7 +96,7 @@ function AnalysisPageContent() {
|
||||
|
||||
return (
|
||||
<TrialAnalysisView
|
||||
trial={trialData}
|
||||
trial={customTrialData}
|
||||
backHref={`/studies/${studyId}/trials/${trialId}`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -140,6 +140,12 @@ function TrialDetailContent() {
|
||||
title={`Trial: ${trial.participant.participantCode}`}
|
||||
description={`${trial.experiment.name} - Session ${trial.sessionNumber}`}
|
||||
icon={Play}
|
||||
badges={[
|
||||
{
|
||||
label: trial.status.replace("_", " ").toUpperCase(),
|
||||
variant: getStatusBadgeVariant(trial.status),
|
||||
}
|
||||
]}
|
||||
actions={
|
||||
<div className="flex gap-2">
|
||||
{trial.status === "scheduled" && (
|
||||
|
||||
Reference in New Issue
Block a user