Begin file upload, add theme change

This commit is contained in:
2024-09-26 01:00:46 -04:00
parent ccc3423953
commit 66137ff7b4
17 changed files with 487 additions and 90 deletions

View File

@@ -0,0 +1,35 @@
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
import { useEffect, useState } from "react"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = () => {
document.documentElement.classList.toggle('dark', mediaQuery.matches)
}
mediaQuery.addListener(handleChange)
handleChange() // Initial check
return () => mediaQuery.removeListener(handleChange)
}, [])
if (!mounted) {
return null
}
return (
<NextThemesProvider
attribute="class"
defaultTheme="system"
enableSystem
{...props}
>
{children}
</NextThemesProvider>
)
}

View File

@@ -0,0 +1,67 @@
"use client"
import { useTheme } from "next-themes"
import { Button } from "~/components/ui/button"
import { MoonIcon, SunIcon, LaptopIcon } from "lucide-react"
import { useEffect, useState } from "react"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "~/components/ui/popover"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2">
<div className="flex flex-col space-y-1">
<Button
variant="ghost"
size="sm"
onClick={() => setTheme('light')}
className={theme === 'light' ? 'bg-accent' : ''}
>
<SunIcon className="h-4 w-4 mr-2" />
Light
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setTheme('dark')}
className={theme === 'dark' ? 'bg-accent' : ''}
>
<MoonIcon className="h-4 w-4 mr-2" />
Dark
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setTheme('system')}
className={theme === 'system' ? 'bg-accent' : ''}
>
<LaptopIcon className="h-4 w-4 mr-2" />
System
</Button>
</div>
</PopoverContent>
</Popover>
)
}

View File

@@ -1,15 +1,17 @@
import { PropsWithChildren } from "react";
import { Sidebar } from "~/components/sidebar";
import { StudyHeader } from "~/components/study/StudyHeader";
import { Toaster } from "~/components/ui/toaster";
const Layout = ({ children }: PropsWithChildren) => {
return (
<div className="flex h-screen bg-gradient-to-b from-blue-100 to-white">
<div className="flex h-screen">
<Sidebar />
<main className="flex-1 overflow-y-auto p-4 pt-16 lg:pt-4">
<div className="container mx-auto space-y-4">
<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">
<StudyHeader />
{children}
<Toaster />
</div>
</main>
</div>

View File

@@ -8,6 +8,7 @@ import { Participant } from '../../types/Participant';
import { CreateParticipantDialog } from './CreateParticipantDialog';
import { Trash2 } from 'lucide-react';
import { useToast } from '~/hooks/use-toast';
import { UploadConsentForm } from './UploadConsentForm';
export function Participants() {
const [participants, setParticipants] = useState<Participant[]>([]);
@@ -88,18 +89,21 @@ export function Participants() {
</CardHeader>
<CardContent>
{participants.length > 0 ? (
<ul className="space-y-2">
<ul className="space-y-4">
{participants.map(participant => (
<li key={participant.id} className="bg-gray-100 p-2 rounded flex justify-between items-center">
<span>{participant.name}</span>
<Button
variant="ghost"
size="sm"
onClick={() => deleteParticipant(participant.id)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
<li key={participant.id} className="bg-gray-100 p-4 rounded">
<div className="flex justify-between items-center mb-2">
<span className="font-semibold">{participant.name}</span>
<Button
variant="ghost"
size="sm"
onClick={() => deleteParticipant(participant.id)}
className="text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<UploadConsentForm studyId={selectedStudy.id} participantId={participant.id} />
</li>
))}
</ul>

View File

@@ -0,0 +1,89 @@
import React, { useState, useEffect } from 'react';
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { useToast } from "~/hooks/use-toast";
interface UploadConsentFormProps {
studyId: number;
participantId: number;
}
export function UploadConsentForm({ studyId, participantId }: UploadConsentFormProps) {
const [file, setFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
const { toast } = useToast();
useEffect(() => {
toast({
title: "Test Toast",
description: "This is a test toast message",
});
}, []);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
setFile(e.target.files[0]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
toast({
title: "Error",
description: "Please select a file to upload",
variant: "destructive",
});
return;
}
setIsUploading(true);
const formData = new FormData();
formData.append('file', file);
formData.append('studyId', studyId.toString());
formData.append('participantId', participantId.toString());
try {
const response = await fetch('/api/informed-consent', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Failed to upload form');
}
toast({
title: "Success",
description: "Informed consent form uploaded successfully",
});
setFile(null);
} catch (error) {
console.error('Error uploading form:', error);
toast({
title: "Error",
description: "Failed to upload informed consent form",
variant: "destructive",
});
} finally {
setIsUploading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Input
type="file"
accept=".pdf"
onChange={handleFileChange}
disabled={isUploading}
/>
</div>
<Button type="submit" disabled={!file || isUploading}>
{isUploading ? 'Uploading...' : 'Upload Consent Form'}
</Button>
</form>
);
}

View File

@@ -16,6 +16,7 @@ import { useState } from "react"
import { Button } from "~/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet"
import { cn } from "~/lib/utils"
import { ThemeToggle } from "~/components/ThemeToggle"
const navItems = [
{ name: "Dashboard", href: "/dash", icon: LayoutDashboard },
@@ -30,8 +31,16 @@ export function Sidebar() {
const [isOpen, setIsOpen] = useState(false)
const { user } = useUser()
const HRIStudioLogo = () => (
<Link href="/dash" className="flex items-center font-sans text-xl text-[hsl(var(--sidebar-foreground))]">
<BotIcon className="h-6 w-6 mr-1 text-[hsl(var(--sidebar-muted))]" />
<span className="font-extrabold">HRI</span>
<span className="font-normal">Studio</span>
</Link>
)
const SidebarContent = () => (
<div className="flex h-full flex-col lg:bg-blue-50">
<div className="flex h-full flex-col bg-gradient-to-b from-[hsl(var(--sidebar-background-top))] to-[hsl(var(--sidebar-background-bottom))]">
<nav className="flex-1 overflow-y-auto p-4">
<ul className="space-y-2">
{navItems.map((item) => {
@@ -42,8 +51,8 @@ export function Sidebar() {
asChild
variant="ghost"
className={cn(
"w-full justify-start text-blue-800 hover:bg-blue-100",
pathname === item.href && "bg-blue-100 font-semibold"
"w-full justify-start text-[hsl(var(--sidebar-foreground))] hover:bg-[hsl(var(--sidebar-hover))]",
pathname === item.href && "bg-[hsl(var(--sidebar-hover))] font-semibold"
)}
>
<Link href={item.href} onClick={() => setIsOpen(false)}>
@@ -56,31 +65,24 @@ export function Sidebar() {
})}
</ul>
</nav>
<div className="border-t border-blue-200 p-4">
<div className="border-t p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<UserButton />
<div>
<p className="text-sm font-medium text-blue-800">{user?.fullName ?? 'User'}</p>
<p className="text-xs text-blue-600">{user?.primaryEmailAddress?.emailAddress ?? 'user@example.com'}</p>
<p className="text-sm font-medium text-[hsl(var(--sidebar-foreground))]">{user?.fullName ?? 'User'}</p>
<p className="text-xs text-[hsl(var(--sidebar-muted))]">{user?.primaryEmailAddress?.emailAddress ?? 'user@example.com'}</p>
</div>
</div>
<ThemeToggle />
</div>
</div>
</div>
)
const HRIStudioLogo = () => (
<Link href="/dash" className="flex items-center font-sans text-xl text-blue-800">
<BotIcon className="h-6 w-6 mr-1 text-blue-600" />
<span className="font-extrabold">HRI</span>
<span className="font-normal">Studio</span>
</Link>
)
return (
<>
<div className="lg:hidden fixed top-0 left-0 right-0">
<div className="lg:hidden fixed top-0 left-0 right-0 z-50">
<div className="flex h-14 items-center justify-between border-b px-4 bg-background">
<HRIStudioLogo />
<Sheet open={isOpen} onOpenChange={setIsOpen}>
@@ -95,7 +97,7 @@ export function Sidebar() {
</Sheet>
</div>
</div>
<div className="hidden lg:flex lg:w-64 lg:flex-col lg:border-r lg:bg-background">
<div className="hidden lg:flex lg:w-64 lg:flex-col lg:border-r lg:bg-gradient-to-b lg:from-[hsl(var(--sidebar-background-top))] lg:to-[hsl(var(--sidebar-background-bottom))]">
<div className="flex h-14 items-center border-b px-4">
<HRIStudioLogo />
</div>

View File

@@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "~/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }