ability to create trials added; form uploader cleaned up

This commit is contained in:
2024-10-03 16:35:19 -04:00
parent 93e2b0323b
commit 7ef9180026
19 changed files with 559 additions and 151 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,9 +1,10 @@
import { db } from "~/server/db"; import { db } from "~/server/db";
import { participants } from "~/server/db/schema"; import { participants, trialParticipants, trials } from "~/server/db/schema";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { eq } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
export async function GET(request: Request) { export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const studyId = searchParams.get('studyId'); const studyId = searchParams.get('studyId');
@@ -11,8 +12,25 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Study ID is required' }, { status: 400 }); return NextResponse.json({ error: 'Study ID is required' }, { status: 400 });
} }
const allParticipants = await db.select().from(participants).where(eq(participants.studyId, parseInt(studyId))); const participantsWithLatestTrial = await db
return NextResponse.json(allParticipants); .select({
id: participants.id,
name: participants.name,
createdAt: participants.createdAt,
latestTrialTimestamp: sql<Date | null>`MAX(${trials.createdAt})`.as('latestTrialTimestamp')
})
.from(participants)
.leftJoin(trialParticipants, eq(participants.id, trialParticipants.participantId))
.leftJoin(trials, eq(trialParticipants.trialId, trials.id))
.where(eq(participants.studyId, parseInt(studyId)))
.groupBy(participants.id)
.orderBy(sql`COALESCE(MAX(${trials.createdAt}), ${participants.createdAt}) DESC`);
return NextResponse.json(participantsWithLatestTrial);
} catch (error) {
console.error('Error in GET /api/participants:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
} }
export async function POST(request: Request) { export async function POST(request: Request) {

View File

@@ -0,0 +1,52 @@
import { db } from "~/server/db";
import { trials, trialParticipants } from "~/server/db/schema";
import { NextResponse } from "next/server";
import { eq, sql } from "drizzle-orm";
export async function GET() {
const allTrials = await db
.select({
id: trials.id,
title: trials.title,
participantIds: sql`ARRAY_AGG(${trialParticipants.participantId})`.as('participantIds'),
})
.from(trials)
.leftJoin(trialParticipants, eq(trials.id, trialParticipants.trialId))
.groupBy(trials.id);
return NextResponse.json(allTrials);
}
export async function POST(request: Request) {
const { title, participantIds } = await request.json();
if (!title || !Array.isArray(participantIds) || participantIds.some(id => typeof id !== 'number')) {
return NextResponse.json({ error: 'Title and valid Participant IDs are required' }, { status: 400 });
}
// Insert the new trial into the trials table
const newTrial = await db.insert(trials).values({ title }).returning();
// Check if newTrial is defined and has at least one element
if (!newTrial || newTrial.length === 0) {
throw new Error('Failed to create a new trial');
}
// Insert the participant associations into the trial_participants table
const trialId = newTrial[0]?.id; // Use optional chaining to safely get the ID of the newly created trial
if (trialId === undefined) {
throw new Error('Trial ID is undefined');
}
const trialParticipantEntries = participantIds.map(participantId => ({
trialId,
participantId,
}));
await db.insert(trialParticipants).values(trialParticipantEntries);
return NextResponse.json(newTrial[0]);
}
export async function DELETE(request: Request) {
const { id } = await request.json();
await db.delete(trials).where(eq(trials.id, id));
return NextResponse.json({ message: "Trial deleted successfully" });
}

View File

@@ -1,11 +1,46 @@
"use client";
import Layout from "~/components/layout"; import Layout from "~/components/layout";
import { Card, CardHeader, CardTitle, CardContent } from "~/components/ui/card"; import { Card, CardHeader, CardTitle, CardContent } from "~/components/ui/card";
import { useStudyContext } from '~/context/StudyContext';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { Button } from "~/components/ui/button";
import { Avatar, AvatarFallback } from "~/components/ui/avatar";
interface ParticipantWithTrial {
id: number;
name: string;
latestTrialTimestamp: string | null;
createdAt: string; // Add createdAt to the interface
}
const DashboardPage: React.FC = () => { const DashboardPage: React.FC = () => {
const { selectedStudy } = useStudyContext();
const [participants, setParticipants] = useState<ParticipantWithTrial[]>([]);
useEffect(() => {
const fetchParticipants = async () => {
if (selectedStudy) {
const response = await fetch(`/api/participants?studyId=${selectedStudy.id}`);
const data = await response.json();
setParticipants(data);
}
};
fetchParticipants();
}, [selectedStudy]);
const formatDate = (dateString: string | null) => {
if (!dateString) return 'No trials yet';
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
};
return ( return (
<Layout> <Layout pageTitle="Dashboard">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card> <Card className="card-level-1">
<CardHeader> <CardHeader>
<CardTitle>Platform Information</CardTitle> <CardTitle>Platform Information</CardTitle>
</CardHeader> </CardHeader>
@@ -13,12 +48,35 @@ const DashboardPage: React.FC = () => {
{/* Add content for Platform Information */} {/* Add content for Platform Information */}
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card className="card-level-1">
<CardHeader> <CardHeader>
<CardTitle>Participants</CardTitle> <CardTitle>Participants</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{/* Add content for Participants */} <div className="grid grid-cols-1 gap-4">
{participants.slice(0, 4).map(participant => (
<Card key={participant.id} className="card-level-2 p-3 px-4 flex items-center">
<Avatar className="mr-4">
<AvatarFallback>{participant.name.split(' ').map(n => n[0]).join('')}</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="font-semibold">{participant.name}</h3>
<p className="text-sm text-muted-foreground">
Last trial: {formatDate(participant.latestTrialTimestamp)}
</p>
</div>
</Card>
))}
</div>
{participants.length > 4 && (
<div className="mt-4 text-center">
<Link href="/participants">
<Button variant="outline" className="text-blue-600 hover:underline">
View More Participants
</Button>
</Link>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>

View File

@@ -1,15 +1,22 @@
import Layout from "~/components/layout"; import Layout from "~/components/layout";
import { FormsGrid } from "~/components/forms/FormsGrid"; import { FormsGrid } from "~/components/forms/FormsGrid";
import { UploadFormButton } from "~/components/forms/UploadFormButton"; import { UploadFormButton } from "~/components/forms/UploadFormButton";
import { Card, CardHeader, CardTitle, CardContent } from "~/components/ui/card";
export default function FormsPage() { export default function FormsPage() {
return ( return (
<Layout> <Layout pageTitle="Forms">
<div className="flex justify-between items-center mb-6"> <Card>
<h1 className="text-3xl font-bold">Forms</h1> <CardHeader>
<CardTitle className="flex justify-between items-center">
<span>Forms</span>
<UploadFormButton /> <UploadFormButton />
</div> </CardTitle>
</CardHeader>
<CardContent>
<FormsGrid /> <FormsGrid />
</CardContent>
</Card>
</Layout> </Layout>
); );
} }

View File

@@ -3,7 +3,7 @@ import { Participants } from "~/components/participant/Participants";
const ParticipantsPage = () => { const ParticipantsPage = () => {
return ( return (
<Layout> <Layout pageTitle="Participants">
<Participants /> <Participants />
</Layout> </Layout>
); );

View File

@@ -3,7 +3,7 @@ import { Studies } from "~/components/study/Studies";
export default function StudiesPage() { export default function StudiesPage() {
return ( return (
<Layout> <Layout pageTitle="Studies">
<Studies /> <Studies />
</Layout> </Layout>
); );

10
src/app/trials/page.tsx Normal file
View File

@@ -0,0 +1,10 @@
import Layout from "~/components/layout";
import { Trials } from "~/components/trial/Trials";
export default function TrialsPage() {
return (
<Layout pageTitle="Trials">
<Trials />
</Layout>
);
}

View File

@@ -2,6 +2,7 @@ import Image from 'next/image';
import { Card, CardContent, CardFooter } from "~/components/ui/card"; import { Card, CardContent, CardFooter } from "~/components/ui/card";
import { Badge } from "~/components/ui/badge"; import { Badge } from "~/components/ui/badge";
import { Trash2 } from "lucide-react"; import { Trash2 } from "lucide-react";
import { Button } from "~/components/ui/button";
interface FormCardProps { interface FormCardProps {
form: { form: {
@@ -23,7 +24,7 @@ export function FormCard({ form, onDelete }: FormCardProps) {
}; };
return ( return (
<Card className="overflow-hidden cursor-pointer" onClick={handleCardClick}> <Card className="overflow-hidden cursor-pointer" onClick={handleCardClick} style={{ backgroundColor: 'var(--primary-card-background)' }}>
<CardContent className="p-0 h-40 relative"> <CardContent className="p-0 h-40 relative">
<Image <Image
src={form.previewLocation} src={form.previewLocation}
@@ -36,16 +37,21 @@ export function FormCard({ form, onDelete }: FormCardProps) {
}} }}
/> />
</CardContent> </CardContent>
<CardFooter className="flex flex-col items-start p-4"> <CardFooter className="flex flex-col items-start p-4" style={{ backgroundColor: 'var(--secondary-card-background)' }}>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<h3 className="font-semibold mb-2">{form.title}</h3> <h3 className="font-semibold mb-2">{form.title}</h3>
<Trash2 <Button
className="h-4 w-4 text-destructive cursor-pointer" variant="ghost"
size="sm"
className="text-destructive"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onDelete(form.id); onDelete(form.id);
}} }}
/> >
<Trash2 className="h-4 w-4" />
Delete
</Button>
</div> </div>
<div className="flex flex-wrap gap-2 mb-2"> <div className="flex flex-wrap gap-2 mb-2">
<Badge variant="secondary">{form.studyTitle}</Badge> <Badge variant="secondary">{form.studyTitle}</Badge>

View File

@@ -0,0 +1 @@

View File

@@ -3,13 +3,17 @@ import { Sidebar } from "~/components/sidebar";
import { StudyHeader } from "~/components/study/StudyHeader"; import { StudyHeader } from "~/components/study/StudyHeader";
import { Toaster } from "~/components/ui/toaster"; import { Toaster } from "~/components/ui/toaster";
const Layout = ({ children }: PropsWithChildren) => { interface LayoutProps {
pageTitle: string;
}
const Layout = ({ children, pageTitle }: PropsWithChildren<LayoutProps>) => {
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
<Sidebar /> <Sidebar />
<main className="flex-1 overflow-y-auto bg-gradient-to-b from-[hsl(var(--gradient-start))] to-[hsl(var(--gradient-end))]"> <main className="flex-1 overflow-y-auto bg-gradient-to-b from-[hsl(var(--gradient-start))] to-[hsl(var(--gradient-end))]">
<div className="container mx-auto space-y-4 p-4 pt-16 lg:pt-4"> <div className="container mx-auto space-y-4 p-4 pt-16 lg:pt-4">
<StudyHeader /> <StudyHeader pageTitle={pageTitle} />
{children} {children}
<Toaster /> <Toaster />
</div> </div>

View File

@@ -8,9 +8,17 @@ import { Participant } from '../../types/Participant';
import { CreateParticipantDialog } from './CreateParticipantDialog'; import { CreateParticipantDialog } from './CreateParticipantDialog';
import { useToast } from '~/hooks/use-toast'; import { useToast } from '~/hooks/use-toast';
import { ParticipantCard } from './ParticipantCard'; import { ParticipantCard } from './ParticipantCard';
import { Avatar, AvatarFallback } from "~/components/ui/avatar";
interface ParticipantWithTrial {
id: number;
name: string;
latestTrialTimestamp: string | null;
createdAt: string;
}
export function Participants() { export function Participants() {
const [participants, setParticipants] = useState<Participant[]>([]); const [participants, setParticipants] = useState<ParticipantWithTrial[]>([]);
const { selectedStudy } = useStudyContext(); const { selectedStudy } = useStudyContext();
const { toast } = useToast(); const { toast } = useToast();
@@ -22,9 +30,23 @@ export function Participants() {
const fetchParticipants = async () => { const fetchParticipants = async () => {
if (!selectedStudy) return; if (!selectedStudy) return;
try {
const response = await fetch(`/api/participants?studyId=${selectedStudy.id}`); const response = await fetch(`/api/participants?studyId=${selectedStudy.id}`);
const data = await response.json(); if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
try {
const data = JSON.parse(text);
setParticipants(data); setParticipants(data);
} catch (e) {
console.error('Failed to parse JSON:', text);
throw new Error('Invalid JSON in response');
}
} catch (error) {
console.error('Error fetching participants:', error);
// Handle the error appropriately, e.g., show a toast notification
}
}; };
const createParticipant = async (name: string) => { const createParticipant = async (name: string) => {
@@ -41,19 +63,12 @@ export function Participants() {
const deleteParticipant = async (id: number) => { const deleteParticipant = async (id: number) => {
if (!selectedStudy) return; if (!selectedStudy) return;
try { try {
console.log(`Attempting to delete participant with ID: ${id}`);
const response = await fetch(`/api/participants/${id}`, { const response = await fetch(`/api/participants/${id}`, {
method: 'DELETE', method: 'DELETE',
}); });
console.log('Delete response:', response);
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
const result = await response.json();
console.log('Delete result:', result);
if (!response.ok) { if (!response.ok) {
throw new Error(result.error || `Failed to delete participant. Status: ${response.status}`); throw new Error('Failed to delete participant');
} }
setParticipants(participants.filter(p => p.id !== id)); setParticipants(participants.filter(p => p.id !== id));
@@ -61,13 +76,7 @@ export function Participants() {
title: "Success", title: "Success",
description: "Participant deleted successfully", description: "Participant deleted successfully",
}); });
} else {
const text = await response.text();
console.error('Unexpected response:', text);
throw new Error(`Unexpected response from server. Status: ${response.status}`);
}
} catch (error) { } catch (error) {
console.error('Error deleting participant:', error);
toast({ toast({
title: "Error", title: "Error",
description: error instanceof Error ? error.message : 'Failed to delete participant', description: error instanceof Error ? error.message : 'Failed to delete participant',
@@ -81,16 +90,36 @@ export function Participants() {
} }
return ( return (
<Card> <Card className="card-level-1">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-2xl font-bold">Participants for {selectedStudy.title}</CardTitle> <CardTitle className="text-2xl font-bold">Participants for {selectedStudy.title}</CardTitle>
<CreateParticipantDialog onCreateParticipant={createParticipant} /> <CreateParticipantDialog onCreateParticipant={createParticipant} />
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{participants.length > 0 ? ( {participants.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{participants.map(participant => ( {participants.map(participant => (
<ParticipantCard key={participant.id} participant={participant} onDelete={deleteParticipant} /> <Card key={participant.id} className="card-level-2 p-3 flex items-center w-full">
<Avatar className="mr-4">
<AvatarFallback>{participant.name.split(' ').map(n => n[0]).join('')}</AvatarFallback>
</Avatar>
<div className="flex-1">
<h3 className="font-semibold">{participant.name}</h3>
<p className="text-sm text-muted-foreground">
{participant.latestTrialTimestamp
? `Last trial: ${new Date(participant.latestTrialTimestamp).toLocaleString()}`
: 'No trials yet'}
</p>
</div>
<Button
variant="ghost"
size="sm"
className="text-destructive"
onClick={() => deleteParticipant(participant.id)}
>
Delete
</Button>
</Card>
))} ))}
</div> </div>
) : ( ) : (

View File

@@ -3,7 +3,8 @@
import { UserButton, useUser } from "@clerk/nextjs" import { UserButton, useUser } from "@clerk/nextjs"
import { import {
BarChartIcon, BarChartIcon,
BeakerIcon, UsersRoundIcon,
LandPlotIcon,
BotIcon, BotIcon,
FolderIcon, FolderIcon,
FileTextIcon, FileTextIcon,
@@ -22,7 +23,8 @@ import { ThemeToggle } from "~/components/ThemeToggle"
const navItems = [ const navItems = [
{ name: "Dashboard", href: "/dash", icon: LayoutDashboard }, { name: "Dashboard", href: "/dash", icon: LayoutDashboard },
{ name: "Studies", href: "/studies", icon: FolderIcon }, { name: "Studies", href: "/studies", icon: FolderIcon },
{ name: "Participants", href: "/participants", icon: BeakerIcon }, { name: "Participants", href: "/participants", icon: UsersRoundIcon },
{ name: "Trials", href: "/trials", icon: LandPlotIcon },
{ name: "Forms", href: "/forms", icon: FileTextIcon }, { name: "Forms", href: "/forms", icon: FileTextIcon },
{ name: "Data Analysis", href: "/analysis", icon: BarChartIcon }, { name: "Data Analysis", href: "/analysis", icon: BarChartIcon },
{ name: "Settings", href: "/settings", icon: Settings }, { name: "Settings", href: "/settings", icon: Settings },

View File

@@ -8,7 +8,11 @@ import { StudySelector } from './StudySelector';
import { CreateStudyDialog } from '~/components/study/CreateStudyDialog'; import { CreateStudyDialog } from '~/components/study/CreateStudyDialog';
import { Study } from '~/types/Study'; import { Study } from '~/types/Study';
export function StudyHeader() { interface StudyHeaderProps {
pageTitle: string;
}
export const StudyHeader: React.FC<StudyHeaderProps> = ({ pageTitle }) => {
const { studies, selectedStudy, setSelectedStudy, validateAndSetSelectedStudy, fetchAndSetStudies } = useStudyContext(); const { studies, selectedStudy, setSelectedStudy, validateAndSetSelectedStudy, fetchAndSetStudies } = useStudyContext();
useEffect(() => { useEffect(() => {
@@ -53,7 +57,7 @@ export function StudyHeader() {
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<h2 className="text-2xl font-bold truncate max-w-[200px]"> <h2 className="text-2xl font-bold truncate max-w-[200px]">
{selectedStudy ? selectedStudy.title : 'Select a Study'} {pageTitle}
</h2> </h2>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
@@ -72,4 +76,4 @@ export function StudyHeader() {
</CardContent> </CardContent>
</Card> </Card>
); );
} };

View File

@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog";
import { Label } from "~/components/ui/label";
interface CreateTrialDialogProps {
onCreateTrial: (title: string, participantIds: number[]) => void;
}
export function CreateTrialDialog({ onCreateTrial }: CreateTrialDialogProps) {
const [title, setTitle] = useState('');
const [participantIds, setParticipantIds] = useState<string>('');
const handleCreate = () => {
const ids = participantIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
if (title && ids.length > 0) {
onCreateTrial(title, ids);
setTitle('');
setParticipantIds('');
}
};
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Add Trial</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Trial</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">Title</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="col-span-3"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="participants" className="text-right">Participant IDs (comma-separated)</Label>
<Input
id="participants"
value={participantIds}
onChange={(e) => setParticipantIds(e.target.value)}
className="col-span-3"
placeholder="e.g. 1, 2, 3"
/>
</div>
</div>
<Button onClick={handleCreate}>Add Trial</Button>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,109 @@
"use client";
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { useToast } from '~/hooks/use-toast';
import { CreateTrialDialog } from '~/components/trial/CreateTrialDialog';
interface Trial {
id: number;
title: string;
participantIds: number[];
createdAt: string;
}
export function Trials() {
const [trials, setTrials] = useState<Trial[]>([]);
const { toast } = useToast();
useEffect(() => {
fetchTrials();
}, []);
const fetchTrials = async () => {
const response = await fetch('/api/trials');
if (!response.ok) {
const errorText = await response.text();
console.error('Error fetching trials:', response.status, errorText);
return;
}
const data = await response.json();
if (!data || data.length === 0) {
console.warn('No trials found');
setTrials([]); // Set to an empty array if no trials are found
return;
}
setTrials(data);
};
const createTrial = async (title: string, participantIds: number[]) => {
const response = await fetch('/api/trials', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, participantIds }),
});
const newTrial = await response.json();
setTrials([...trials, newTrial]);
toast({
title: "Success",
description: "Trial created successfully",
});
};
const deleteTrial = async (id: number) => {
const response = await fetch(`/api/trials/${id}`, {
method: 'DELETE',
});
if (response.ok) {
setTrials(trials.filter(trial => trial.id !== id));
toast({
title: "Success",
description: "Trial deleted successfully",
});
} else {
toast({
title: "Error",
description: "Failed to delete trial",
variant: "destructive",
});
}
};
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-2xl font-bold">Trials</CardTitle>
<CreateTrialDialog onCreateTrial={createTrial} />
</CardHeader>
<CardContent>
{trials.length > 0 ? (
<div className="grid grid-cols-1 gap-4">
{trials.map(trial => (
<Card key={trial.id} className="bg-gray-100 p-3 flex items-center justify-between">
<div>
<h3 className="font-semibold">{trial.title}</h3>
<p className="text-sm text-gray-500">Participants: {trial.participantIds.join(', ')}</p>
</div>
<Button
variant="ghost"
size="sm"
className="text-destructive"
onClick={() => deleteTrial(trial.id)}
>
Delete
</Button>
</Card>
))}
</div>
) : (
<p>No trials added yet.</p>
)}
</CardContent>
</Card>
);
}

View File

@@ -90,3 +90,23 @@ export const users = pgTable(
.notNull(), .notNull(),
} }
); );
export const trials = pgTable(
"trial",
{
id: serial("id").primaryKey(),
title: varchar("title", { length: 256 }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
}
);
export const trialParticipants = pgTable(
"trial_participants",
{
id: serial("id").primaryKey(),
trialId: integer("trial_id").references(() => trials.id).notNull(),
participantId: integer("participant_id").references(() => participants.id).notNull(),
}
);

View File

@@ -6,8 +6,8 @@
:root { :root {
--background: 210 50% 98%; --background: 210 50% 98%;
--foreground: 215 25% 27%; --foreground: 215 25% 27%;
--card: 210 50% 98%; --card: 210 50% 98%; /* Card background color */
--card-foreground: 215 25% 27%; --card-foreground: 215 25% 27%; /* Card text color */
--popover: 210 50% 98%; --popover: 210 50% 98%;
--popover-foreground: 215 25% 27%; --popover-foreground: 215 25% 27%;
--primary: 215 60% 40%; --primary: 215 60% 40%;
@@ -35,39 +35,60 @@
--sidebar-foreground: 215 25% 27%; --sidebar-foreground: 215 25% 27%;
--sidebar-muted: 215 20% 50%; --sidebar-muted: 215 20% 50%;
--sidebar-hover: 210 60% 86%; --sidebar-hover: 210 60% 86%;
--card-level-1: 210 50% 95%; /* Level 1 card background color */
--card-level-2: 210 50% 90%; /* Level 2 card background color */
--card-level-3: 210 50% 85%; /* Level 3 card background color */
} }
.dark { .dark {
--background: 220 20% 15%; --background: 220 20% 15%; /* Dark mode background */
--foreground: 220 15% 85%; --foreground: 220 20% 90%; /* Dark mode foreground */
--card: 220 20% 15%; --card: 220 20% 15%; /* Dark mode card background color */
--card-foreground: 220 15% 85%; --card-foreground: 220 20% 90%; /* Dark mode card text color */
--popover: 220 20% 15%; --popover: 220 20% 15%;
--popover-foreground: 220 15% 85%; --popover-foreground: 220 20% 90%;
--primary: 220 60% 50%; --primary: 220 60% 50%;
--primary-foreground: 220 15% 85%; --primary-foreground: 220 20% 90%;
--secondary: 220 25% 20%; --secondary: 220 30% 20%; /* Darker secondary */
--secondary-foreground: 220 15% 85%; --secondary-foreground: 220 20% 90%;
--muted: 220 25% 20%; --muted: 220 30% 20%;
--muted-foreground: 220 15% 65%; --muted-foreground: 220 20% 70%;
--accent: 220 25% 20%; --accent: 220 30% 20%;
--accent-foreground: 220 15% 85%; --accent-foreground: 220 20% 90%;
--destructive: 0 62% 30%; --destructive: 0 62% 40%; /* Darker destructive */
--destructive-foreground: 220 15% 85%; --destructive-foreground: 220 20% 90%;
--border: 220 25% 20%; --border: 220 30% 20%;
--input: 220 25% 20%; --input: 220 30% 20%;
--ring: 220 60% 50%; --ring: 220 60% 50%;
/* Update gradient variables for dark mode */ /* Update gradient variables for dark mode */
--gradient-start: 220 20% 13%; --gradient-start: 220 20% 12%;
--gradient-end: 220 20% 15%; --gradient-end: 220 20% 15%;
/* Update sidebar variables for dark mode */ /* Update sidebar variables for dark mode */
--sidebar-background-top: 220 20% 15%; --sidebar-background-top: 220 20% 15%;
--sidebar-background-bottom: 220 20% 12%; --sidebar-background-bottom: 220 20% 12%;
--sidebar-foreground: 220 15% 85%; --sidebar-foreground: 220 20% 90%;
--sidebar-muted: 220 15% 65%; --sidebar-muted: 220 20% 70%;
--sidebar-hover: 220 25% 18%; --sidebar-hover: 220 30% 20%;
--card-level-1: 220 20% 12%; /* Dark mode Level 1 card background color */
--card-level-2: 220 20% 10%; /* Dark mode Level 2 card background color */
--card-level-3: 220 20% 8%; /* Dark mode Level 3 card background color */
}
/* Add these utility classes */
.card-level-1 {
background-color: hsl(var(--card-level-1));
}
.card-level-2 {
background-color: hsl(var(--card-level-2));
}
.card-level-3 {
background-color: hsl(var(--card-level-3));
} }
} }

View File

@@ -8,25 +8,32 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"moduleDetection": "force", "moduleDetection": "force",
"isolatedModules": true, "isolatedModules": true,
/* Strictness */ /* Strictness */
"strict": true, "strict": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"checkJs": true, "checkJs": true,
/* Bundled projects */ /* Bundled projects */
"lib": ["dom", "dom.iterable", "ES2022"], "lib": [
"dom",
"dom.iterable",
"ES2022"
],
"noEmit": true, "noEmit": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"jsx": "preserve", "jsx": "preserve", // or "react" for older versions
"plugins": [{ "name": "next" }], "plugins": [
{
"name": "next"
}
],
"incremental": true, "incremental": true,
/* Path Aliases */ /* Path Aliases */
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"~/*": ["./src/*"] "~/*": [
"./src/*"
]
} }
}, },
"include": [ "include": [
@@ -38,5 +45,7 @@
"**/*.js", "**/*.js",
".next/types/**/*.ts" ".next/types/**/*.ts"
], ],
"exclude": ["node_modules"] "exclude": [
"node_modules"
]
} }