mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-11 22:54:45 -05:00
Begin file upload, add theme change
This commit is contained in:
35
src/components/ThemeProvider.tsx
Normal file
35
src/components/ThemeProvider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
67
src/components/ThemeToggle.tsx
Normal file
67
src/components/ThemeToggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
89
src/components/participant/UploadConsentForm.tsx
Normal file
89
src/components/participant/UploadConsentForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
33
src/components/ui/popover.tsx
Normal file
33
src/components/ui/popover.tsx
Normal 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 }
|
||||
Reference in New Issue
Block a user