From 80171b2d70d4a14a0bf618a7b69911af910fe50b Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Thu, 5 Dec 2024 13:21:33 -0500 Subject: [PATCH] feat: Enhance user management and UI components - Updated user API routes to include imageUrl for better user representation. - Added @radix-ui/react-toast dependency for improved user notifications. - Refactored dashboard and studies components to incorporate new user fields and loading states. - Enhanced the layout and structure of the dashboard, studies, and participants pages for better user experience. - Implemented a dialog for adding participants, improving the participant management workflow. - Updated breadcrumb navigation to reflect the current study context more accurately. - Cleaned up unused imports and optimized component rendering for performance. --- package.json | 3 +- pnpm-lock.yaml | 36 ++ src/app/api/studies/[id]/users/route.ts | 3 + src/app/api/webhooks/clerk/route.ts | 22 +- src/app/dashboard/layout.tsx | 19 +- src/app/dashboard/page.tsx | 166 ++++++---- src/app/dashboard/studies/[id]/layout.tsx | 8 +- src/app/dashboard/studies/[id]/page.tsx | 221 +++++++------ .../studies/[id]/participants/page.tsx | 186 +++++++++-- .../dashboard/studies/[id]/settings/page.tsx | 63 ++-- src/app/dashboard/studies/page.tsx | 307 ++++++++---------- src/app/layout.tsx | 25 +- src/components/breadcrumb.tsx | 49 ++- src/components/page-header.tsx | 22 ++ src/components/sidebar.tsx | 10 +- src/components/studies/users-tab.tsx | 22 +- src/components/user-avatar.tsx | 4 +- 17 files changed, 712 insertions(+), 454 deletions(-) create mode 100644 src/components/page-header.tsx diff --git a/package.json b/package.json index 7571586..c416236 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", @@ -26,6 +26,7 @@ "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.2", "@types/nodemailer": "^6.4.17", "@vercel/analytics": "^1.4.1", "@vercel/postgres": "^0.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3207fea..05ef2e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@radix-ui/react-tabs': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': + specifier: ^1.2.2 + version: 1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/nodemailer': specifier: ^6.4.17 version: 6.4.17 @@ -1148,6 +1151,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-toast@1.2.2': + resolution: {integrity: sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: @@ -3885,6 +3901,26 @@ snapshots: '@types/react': 18.3.13 '@types/react-dom': 18.3.1 + '@radix-ui/react-toast@1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.13)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.13)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.13)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.13)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.13)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.13 + '@types/react-dom': 18.3.1 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.13)(react@18.3.1)': dependencies: react: 18.3.1 diff --git a/src/app/api/studies/[id]/users/route.ts b/src/app/api/studies/[id]/users/route.ts index dee0ab3..28e583c 100644 --- a/src/app/api/studies/[id]/users/route.ts +++ b/src/app/api/studies/[id]/users/route.ts @@ -31,6 +31,7 @@ export async function GET( id: usersTable.id, email: usersTable.email, name: usersTable.name, + imageUrl: usersTable.imageUrl, roleId: rolesTable.id, roleName: rolesTable.name, }) @@ -47,6 +48,7 @@ export async function GET( id: curr.id, email: curr.email, name: curr.name, + imageUrl: curr.imageUrl, roles: [{ id: curr.roleId, name: curr.roleName, @@ -63,6 +65,7 @@ export async function GET( id: string; email: string; name: string | null; + imageUrl: string | null; roles: Array<{ id: number; name: string }>; }>); diff --git a/src/app/api/webhooks/clerk/route.ts b/src/app/api/webhooks/clerk/route.ts index 515348d..6fb016a 100644 --- a/src/app/api/webhooks/clerk/route.ts +++ b/src/app/api/webhooks/clerk/route.ts @@ -46,19 +46,27 @@ export async function POST(req: Request) { const eventType = evt.type; console.log(`Webhook received: ${eventType}`); - if (eventType === 'user.created') { - const { id, email_addresses, first_name, last_name } = evt.data; + if (eventType === 'user.created' || eventType === 'user.updated') { + const { id, email_addresses, first_name, last_name, image_url } = evt.data; const primaryEmail = email_addresses?.[0]?.email_address; - // Create user in our database + // Create or update user in our database await db.insert(usersTable).values({ id, email: primaryEmail, - firstName: first_name || null, - lastName: last_name || null, - }).onConflictDoNothing(); + name: [first_name, last_name].filter(Boolean).join(' ') || null, + imageUrl: image_url, + }).onConflictDoUpdate({ + target: usersTable.id, + set: { + email: primaryEmail, + name: [first_name, last_name].filter(Boolean).join(' ') || null, + imageUrl: image_url, + updatedAt: new Date(), + } + }); - console.log(`Created user in database: ${id}`); + console.log(`${eventType === 'user.created' ? 'Created' : 'Updated'} user in database: ${id}`); } return new Response('', { status: 200 }); diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index f6ba5ee..d4c0731 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,7 +1,7 @@ import { Sidebar } from "~/components/sidebar"; -import { cn } from "~/lib/utils"; -import { StudyProvider } from "~/context/StudyContext"; +import { Breadcrumb } from "~/components/breadcrumb"; import { ActiveStudyProvider } from "~/context/active-study"; +import { StudyProvider } from "~/context/StudyContext"; export default function DashboardLayout({ children, @@ -11,15 +11,14 @@ export default function DashboardLayout({ return ( -
+
-
- {children} -
+
+
+ + {children} +
+
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 7d3a873..4fa0b67 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -6,8 +6,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/com import { Button } from "~/components/ui/button"; import { BookOpen, Settings2 } from "lucide-react"; import { useToast } from "~/hooks/use-toast"; -import { Breadcrumb } from "~/components/breadcrumb"; import { getApiUrl } from "~/lib/fetch-utils"; +import { Skeleton } from "~/components/ui/skeleton"; +import { useActiveStudy } from "~/context/active-study"; interface DashboardStats { studyCount: number; @@ -19,114 +20,151 @@ export default function Dashboard() { studyCount: 0, activeInvitationCount: 0, }); - const [loading, setLoading] = useState(true); + const [isLoading, setIsLoading] = useState(true); const router = useRouter(); const { toast } = useToast(); + const { studies, setActiveStudy } = useActiveStudy(); - const fetchDashboardStats = useCallback(async () => { + const fetchStats = useCallback(async () => { try { - const studiesRes = await fetch(getApiUrl('/api/studies')); - const studies = await studiesRes.json(); - - // For now, just show study count + const response = await fetch(getApiUrl('/api/studies')); + if (!response.ok) throw new Error("Failed to fetch studies"); + const { data } = await response.json(); setStats({ - studyCount: studies.data.length, - activeInvitationCount: 0, + studyCount: data.length, + activeInvitationCount: 0 }); + + // If there's only one study and we're on the main dashboard, select it + if (data.length === 1) { + const study = { + ...data[0], + createdAt: new Date(data[0].createdAt), + updatedAt: data[0].updatedAt ? new Date(data[0].updatedAt) : null + }; + setActiveStudy(study); + router.push(`/dashboard/studies/${study.id}`); + } } catch (error) { - console.error('Error fetching dashboard stats:', error); + console.error("Error fetching stats:", error); toast({ title: "Error", description: "Failed to load dashboard statistics", variant: "destructive", }); } finally { - setLoading(false); + setIsLoading(false); } - }, [toast]); + }, [toast, router, setActiveStudy]); useEffect(() => { - fetchDashboardStats(); - }, [fetchDashboardStats]); + fetchStats(); + }, [fetchStats]); - - if (loading) { + if (isLoading) { return ( -
-
+
+
+
+ + +
+ +
+ +
+ {[1, 2].map((i) => ( + + + + + + + + + + + ))} +
+ + + + + + + + + + +
); } return (
- - -
-

Dashboard

-

Overview of your research studies

+
+
+

Dashboard

+

+ Welcome back to your research dashboard +

+
+
-
+
- Total Studies + + Total Studies +
{stats.studyCount}
-

Active research studies

+

+ Active research studies +

- Pending Invitations + + Pending Invitations +
{stats.activeInvitationCount}
-

Awaiting acceptance

-
-
-
- -
- - - Quick Actions - Common tasks and actions - - - - - - - - - Getting Started - Tips for using HRIStudio - - -

- • Create a new study from the Studies page -

-

- • Invite collaborators using study settings -

-

- • Configure study parameters and forms +

+ Awaiting responses

+ + + + Quick Actions + Common tasks and actions + + + + + +
); } diff --git a/src/app/dashboard/studies/[id]/layout.tsx b/src/app/dashboard/studies/[id]/layout.tsx index 2ae4e34..dca9f37 100644 --- a/src/app/dashboard/studies/[id]/layout.tsx +++ b/src/app/dashboard/studies/[id]/layout.tsx @@ -3,7 +3,6 @@ import { useParams } from "next/navigation"; import { useEffect } from "react"; import { useActiveStudy } from "~/context/active-study"; -import { Breadcrumb } from "~/components/breadcrumb"; import { Skeleton } from "~/components/ui/skeleton"; export default function StudyLayout({ @@ -34,10 +33,5 @@ export default function StudyLayout({ ); } - return ( -
- - {children} -
- ); + return children; } \ No newline at end of file diff --git a/src/app/dashboard/studies/[id]/page.tsx b/src/app/dashboard/studies/[id]/page.tsx index b75d0de..0b92e87 100644 --- a/src/app/dashboard/studies/[id]/page.tsx +++ b/src/app/dashboard/studies/[id]/page.tsx @@ -4,170 +4,179 @@ import { useCallback, useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; -import { - Users, - FileText, - BarChart, - PlayCircle, - Plus, - Settings2 -} from "lucide-react"; import { useToast } from "~/hooks/use-toast"; +import { Plus, Users, FileText, BarChart, PlayCircle } from "lucide-react"; import Link from "next/link"; +import { useActiveStudy } from "~/context/active-study"; import { getApiUrl } from "~/lib/fetch-utils"; +import { Skeleton } from "~/components/ui/skeleton"; interface StudyStats { participantCount: number; - completedTrialsCount: number; - pendingFormsCount: number; + formCount: number; + trialCount: number; } export default function StudyDashboard() { const [stats, setStats] = useState({ participantCount: 0, - completedTrialsCount: 0, - pendingFormsCount: 0, + formCount: 0, + trialCount: 0, }); - const [loading, setLoading] = useState(true); + const [isLoading, setIsLoading] = useState(true); const { id } = useParams(); const { toast } = useToast(); + const { activeStudy } = useActiveStudy(); - const fetchStudyStats = useCallback(async () => { + const fetchStats = useCallback(async () => { try { const response = await fetch(getApiUrl(`/api/studies/${id}/stats`)); - if (!response.ok) throw new Error("Failed to fetch study statistics"); - const data = await response.json(); - setStats(data.data); + if (!response.ok) throw new Error("Failed to fetch stats"); + const { data } = await response.json(); + setStats({ + participantCount: data?.participantCount ?? 0, + formCount: data?.formCount ?? 0, + trialCount: data?.trialCount ?? 0 + }); } catch (error) { - console.error("Error fetching study stats:", error); + console.error("Error fetching stats:", error); toast({ title: "Error", description: "Failed to load study statistics", variant: "destructive", }); + // Set default values on error + setStats({ + participantCount: 0, + formCount: 0, + trialCount: 0 + }); } finally { - setLoading(false); + setIsLoading(false); } }, [toast, id]); useEffect(() => { - fetchStudyStats(); - }, [fetchStudyStats]); + fetchStats(); + }, [fetchStats]); - if (loading) { + if (isLoading) { return ( -
-
+
+
+
+ + +
+
+ +
+ {[1, 2, 3].map((i) => ( + + + + + + + + + + + ))} +
+ + + + + + + + + + +
); } return (
-
+
+
+

{activeStudy?.title}

+

+ Overview of your study's progress and statistics +

+
+
+ +
- Participants + + Participants +
{stats.participantCount}
-

Total participants enrolled

+

+ Total enrolled participants +

- Completed Trials - - - -
{stats.completedTrialsCount}
-

Successfully completed trials

-
-
- - - - Pending Forms + + Forms + -
{stats.pendingFormsCount}
-

Forms awaiting completion

-
-
-
- -
- - - Quick Actions - Common tasks for this study - - - - - +
{stats.formCount}
+

+ Active study forms +

- - Recent Activity - Latest updates and changes + + + Trials + + - - - + +
{stats.trialCount}
+

+ Completed trials +

+ + + + Quick Actions + Common tasks and actions for this study + + + + + +
); } \ No newline at end of file diff --git a/src/app/dashboard/studies/[id]/participants/page.tsx b/src/app/dashboard/studies/[id]/participants/page.tsx index c1455c0..41cb7c7 100644 --- a/src/app/dashboard/studies/[id]/participants/page.tsx +++ b/src/app/dashboard/studies/[id]/participants/page.tsx @@ -6,7 +6,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/com import { Button } from "~/components/ui/button"; import { useToast } from "~/hooks/use-toast"; import { Plus, Trash2 } from "lucide-react"; -import Link from "next/link"; import { useActiveStudy } from "~/context/active-study"; import { hasPermission } from "~/lib/permissions-client"; import { PERMISSIONS } from "~/lib/permissions"; @@ -21,6 +20,15 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "~/components/ui/alert-dialog"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog"; import { Table, TableBody, @@ -30,6 +38,10 @@ import { TableRow, } from "~/components/ui/table"; import { getApiUrl } from "~/lib/fetch-utils"; +import { Skeleton } from "~/components/ui/skeleton"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; + interface Participant { id: number; name: string; @@ -40,6 +52,8 @@ interface Participant { export default function ParticipantsList() { const [participants, setParticipants] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [isAddingParticipant, setIsAddingParticipant] = useState(false); + const [newParticipantName, setNewParticipantName] = useState(""); const { id } = useParams(); const { toast } = useToast(); const { activeStudy } = useActiveStudy(); @@ -50,13 +64,7 @@ export default function ParticipantsList() { const fetchParticipants = useCallback(async () => { try { - const response = await fetch(getApiUrl(`/api/studies/${id}/participants`), { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); - + const response = await fetch(getApiUrl(`/api/studies/${id}/participants`)); if (!response.ok) throw new Error("Failed to fetch participants"); const data = await response.json(); setParticipants(data.data || []); @@ -70,7 +78,7 @@ export default function ParticipantsList() { } finally { setIsLoading(false); } - }, [toast, id]); + }, [id, toast]); useEffect(() => { fetchParticipants(); @@ -103,13 +111,82 @@ export default function ParticipantsList() { } }; + const handleAddParticipant = async () => { + if (!newParticipantName.trim()) return; + + setIsAddingParticipant(true); + try { + const response = await fetch(getApiUrl(`/api/studies/${id}/participants`), { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name: newParticipantName }), + }); + + if (!response.ok) throw new Error("Failed to add participant"); + + const data = await response.json(); + setParticipants([...participants, data.data]); + setNewParticipantName(""); + toast({ + title: "Success", + description: "Participant added successfully", + }); + } catch (error) { + console.error("Error adding participant:", error); + toast({ + title: "Error", + description: "Failed to add participant", + variant: "destructive", + }); + } finally { + setIsAddingParticipant(false); + } + }; + if (isLoading) { return ( - - -

Loading participants...

-
-
+
+
+
+ + +
+ +
+ + + + + + + +
+ + + + + + + + + + + {[1, 2, 3].map((i) => ( + + + + + + + ))} + +
+
+
+
+
); } @@ -123,12 +200,41 @@ export default function ParticipantsList() {

{canCreateParticipant && ( - + + + + + + + Add Participant + + Add a new participant to {activeStudy?.title} + + +
+
+ + setNewParticipantName(e.target.value)} + /> +
+
+ + + +
+
)}
@@ -198,12 +304,40 @@ export default function ParticipantsList() { {canCreateParticipant && ( <> .{" "} - - Add your first participant - + + + + + + + Add Participant + + Add a new participant to {activeStudy?.title} + + +
+
+ + setNewParticipantName(e.target.value)} + /> +
+
+ + + +
+
)}
diff --git a/src/app/dashboard/studies/[id]/settings/page.tsx b/src/app/dashboard/studies/[id]/settings/page.tsx index 00fc741..10508d5 100644 --- a/src/app/dashboard/studies/[id]/settings/page.tsx +++ b/src/app/dashboard/studies/[id]/settings/page.tsx @@ -3,14 +3,15 @@ import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { SettingsTab } from "~/components/studies/settings-tab"; -import { ParticipantsTab } from "~/components/studies/participants-tab"; import { UsersTab } from "~/components/studies/users-tab"; import { useEffect } from "react"; import { PERMISSIONS } from "~/lib/permissions-client"; import { Button } from "~/components/ui/button"; -import { Settings2Icon, UsersIcon, UserIcon } from "lucide-react"; +import { Settings2Icon, UsersIcon } from "lucide-react"; import { cn } from "~/lib/utils"; import { getApiUrl } from "~/lib/fetch-utils"; +import { Card, CardContent } from "~/components/ui/card"; +import { Skeleton } from "~/components/ui/skeleton"; interface Study { id: number; @@ -23,7 +24,7 @@ export default function StudySettings() { const [study, setStudy] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [activeTab, setActiveTab] = useState<'settings' | 'participants' | 'users'>('settings'); + const [activeTab, setActiveTab] = useState<'settings' | 'users'>('settings'); const { id } = useParams(); const router = useRouter(); @@ -62,7 +63,36 @@ export default function StudySettings() { }, [id, router]); if (isLoading) { - return
Loading...
; + return ( +
+
+
+ + +
+
+ +
+
+ + +
+ +
+ + +
+ + + + +
+
+
+
+
+
+ ); } if (error || !study) { @@ -70,12 +100,14 @@ export default function StudySettings() { } return ( -
-
-

{study.title}

-

- Manage study settings, participants, and team members -

+
+
+
+

Settings

+

+ Manage study settings and team members +

+
@@ -88,14 +120,6 @@ export default function StudySettings() { Settings -
-
- -
diff --git a/src/app/dashboard/studies/page.tsx b/src/app/dashboard/studies/page.tsx index 9c86029..6ed8472 100644 --- a/src/app/dashboard/studies/page.tsx +++ b/src/app/dashboard/studies/page.tsx @@ -1,13 +1,10 @@ 'use client'; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { PlusIcon, Trash2Icon, Settings2 } from "lucide-react"; +import { PlusIcon, Trash2Icon, Settings2, ArrowRight } from "lucide-react"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; -import { Input } from "~/components/ui/input"; -import { Label } from "~/components/ui/label"; -import { Textarea } from "~/components/ui/textarea"; import { useToast } from "~/hooks/use-toast"; import { PERMISSIONS, hasPermission } from "~/lib/permissions-client"; import { @@ -20,21 +17,22 @@ import { AlertDialogTrigger, AlertDialogFooter } from "~/components/ui/alert-dialog"; -import { ROLES } from "~/lib/roles"; import { getApiUrl } from "~/lib/fetch-utils"; +import { Skeleton } from "~/components/ui/skeleton"; +import { useActiveStudy } from "~/context/active-study"; interface Study { id: number; title: string; description: string | null; - createdAt: string; - updatedAt: string | null; userId: string; + environment: string; + createdAt: Date; + updatedAt: Date | null; permissions: string[]; roles: string[]; } -// Helper function to format role name function formatRoleName(role: string): string { return role .split('_') @@ -44,17 +42,21 @@ function formatRoleName(role: string): string { export default function Studies() { const [studies, setStudies] = useState([]); - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); + const [isLoading, setIsLoading] = useState(true); const router = useRouter(); const { toast } = useToast(); + const { setActiveStudy } = useActiveStudy(); - const fetchStudies = async () => { + const fetchStudies = useCallback(async () => { try { const response = await fetch(getApiUrl("/api/studies")); if (!response.ok) throw new Error("Failed to fetch studies"); - const data = await response.json(); - setStudies(data.data || []); + const { data } = await response.json(); + setStudies(data.map((study: any) => ({ + ...study, + createdAt: new Date(study.createdAt), + updatedAt: study.updatedAt ? new Date(study.updatedAt) : null + }))); } catch (error) { console.error("Error fetching studies:", error); toast({ @@ -62,51 +64,22 @@ export default function Studies() { description: "Failed to load studies", variant: "destructive", }); + } finally { + setIsLoading(false); } - }; + }, [toast]); - const createStudy = async (e: React.FormEvent) => { - e.preventDefault(); - try { - const response = await fetch(getApiUrl("/api/studies"), { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ title, description }), - }); + useEffect(() => { + fetchStudies(); + }, [fetchStudies]); - if (!response.ok) { - throw new Error("Failed to create study"); - } - - const data = await response.json(); - setStudies([...studies, data.data]); - setTitle(""); - setDescription(""); - toast({ - title: "Success", - description: "Study created successfully", - }); - } catch (error) { - console.error("Error creating study:", error); - toast({ - title: "Error", - description: "Failed to create study", - variant: "destructive", - }); - } - }; - - const deleteStudy = async (id: number) => { + const handleDelete = async (id: number) => { try { const response = await fetch(getApiUrl(`/api/studies/${id}`), { method: "DELETE", }); - if (!response.ok) { - throw new Error("Failed to delete study"); - } + if (!response.ok) throw new Error("Failed to delete study"); setStudies(studies.filter(study => study.id !== id)); toast({ @@ -123,139 +96,137 @@ export default function Studies() { } }; - // Fetch studies on mount - useState(() => { - fetchStudies(); - }); + const handleEnterStudy = (study: Study) => { + setActiveStudy(study); + router.push(`/dashboard/studies/${study.id}`); + }; + + if (isLoading) { + return ( +
+
+
+ + +
+ +
+ +
+ {[1, 2, 3].map((i) => ( + + +
+
+ + + +
+
+ + +
+
+
+
+ ))} +
+
+ ); + } return ( -
-
-

Studies

-

- Manage your research studies and experiments -

+
+
+
+

Studies

+

+ Manage your research studies and experiments +

+
+ {hasPermission(studies[0]?.permissions || [], PERMISSIONS.CREATE_STUDY) && ( + + )}
- {hasPermission(studies[0]?.permissions || [], PERMISSIONS.CREATE_STUDY) && ( - - - Create New Study - Add a new research study to your collection - - -
-
- - setTitle(e.target.value)} - placeholder="Enter study title" - required - /> -
-
- -