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

2
.gitignore vendored
View File

@@ -58,3 +58,5 @@ pnpm-lock.yaml
# End of https://www.toptal.com/developers/gitignore/api/nextjs,react # End of https://www.toptal.com/developers/gitignore/api/nextjs,react
pnpm-lock.yaml pnpm-lock.yaml
/content

View File

@@ -19,6 +19,7 @@
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0", "@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.1.1", "@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",

View File

@@ -0,0 +1,67 @@
import { NextResponse } from "next/server";
import { db } from "~/server/db";
import { contents, informedConsentForms, contentTypes } from "~/server/db/schema";
import { auth } from "@clerk/nextjs/server";
import { eq } from "drizzle-orm";
import { saveFile } from '~/lib/fileStorage';
export async function POST(request: Request) {
const { userId } = auth();
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const formData = await request.formData();
const file = formData.get('file') as File;
const studyId = formData.get('studyId') as string;
const participantId = formData.get('participantId') as string;
if (!file || !studyId || !participantId) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
try {
const [contentType] = await db
.select()
.from(contentTypes)
.where(eq(contentTypes.name, "Informed Consent Form"));
if (!contentType) {
return NextResponse.json({ error: 'Content type not found' }, { status: 500 });
}
const [content] = await db
.insert(contents)
.values({
contentTypeId: contentType.id,
uploader: userId,
location: '', // We'll update this after saving the file
})
.returning();
if (!content) {
throw new Error("Content not found");
}
const fileLocation = await saveFile(file, content.id);
await db
.update(contents)
.set({ location: fileLocation })
.where(eq(contents.id, content.id));
const [form] = await db
.insert(informedConsentForms)
.values({
studyId: parseInt(studyId),
participantId: parseInt(participantId),
contentId: content.id,
})
.returning();
return NextResponse.json(form);
} catch (error) {
console.error('Error uploading informed consent form:', error);
return NextResponse.json({ error: 'Failed to upload form' }, { status: 500 });
}
}

View File

@@ -1,7 +1,7 @@
import { ClerkProvider } from '@clerk/nextjs' import { ClerkProvider } from '@clerk/nextjs'
import { Inter } from "next/font/google" import { Inter } from "next/font/google"
import { StudyProvider } from '~/context/StudyContext' import { StudyProvider } from '~/context/StudyContext'
import { ThemeProvider } from '~/components/ThemeProvider'
import "~/styles/globals.css" import "~/styles/globals.css"
const inter = Inter({ const inter = Inter({
@@ -19,13 +19,15 @@ export const metadata = {
export default function RootLayout({ children }: React.PropsWithChildren) { export default function RootLayout({ children }: React.PropsWithChildren) {
return ( return (
<ClerkProvider> <ClerkProvider>
<StudyProvider> <html lang="en" className={inter.variable}>
<html lang="en" className={inter.variable}> <body className="font-sans">
<body className="font-sans"> <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children} <StudyProvider>
</body> {children}
</html> </StudyProvider>
</StudyProvider> </ThemeProvider>
</body>
</html>
</ClerkProvider> </ClerkProvider>
) )
} }

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

View File

@@ -8,6 +8,7 @@ import { Participant } from '../../types/Participant';
import { CreateParticipantDialog } from './CreateParticipantDialog'; import { CreateParticipantDialog } from './CreateParticipantDialog';
import { Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import { useToast } from '~/hooks/use-toast'; import { useToast } from '~/hooks/use-toast';
import { UploadConsentForm } from './UploadConsentForm';
export function Participants() { export function Participants() {
const [participants, setParticipants] = useState<Participant[]>([]); const [participants, setParticipants] = useState<Participant[]>([]);
@@ -88,18 +89,21 @@ export function Participants() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{participants.length > 0 ? ( {participants.length > 0 ? (
<ul className="space-y-2"> <ul className="space-y-4">
{participants.map(participant => ( {participants.map(participant => (
<li key={participant.id} className="bg-gray-100 p-2 rounded flex justify-between items-center"> <li key={participant.id} className="bg-gray-100 p-4 rounded">
<span>{participant.name}</span> <div className="flex justify-between items-center mb-2">
<Button <span className="font-semibold">{participant.name}</span>
variant="ghost" <Button
size="sm" variant="ghost"
onClick={() => deleteParticipant(participant.id)} size="sm"
className="text-red-500 hover:text-red-700" onClick={() => deleteParticipant(participant.id)}
> className="text-red-500 hover:text-red-700"
<Trash2 className="h-4 w-4" /> >
</Button> <Trash2 className="h-4 w-4" />
</Button>
</div>
<UploadConsentForm studyId={selectedStudy.id} participantId={participant.id} />
</li> </li>
))} ))}
</ul> </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 { Button } from "~/components/ui/button"
import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet" import { Sheet, SheetContent, SheetTrigger } from "~/components/ui/sheet"
import { cn } from "~/lib/utils" import { cn } from "~/lib/utils"
import { ThemeToggle } from "~/components/ThemeToggle"
const navItems = [ const navItems = [
{ name: "Dashboard", href: "/dash", icon: LayoutDashboard }, { name: "Dashboard", href: "/dash", icon: LayoutDashboard },
@@ -30,8 +31,16 @@ export function Sidebar() {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const { user } = useUser() 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 = () => ( 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"> <nav className="flex-1 overflow-y-auto p-4">
<ul className="space-y-2"> <ul className="space-y-2">
{navItems.map((item) => { {navItems.map((item) => {
@@ -42,8 +51,8 @@ export function Sidebar() {
asChild asChild
variant="ghost" variant="ghost"
className={cn( className={cn(
"w-full justify-start text-blue-800 hover:bg-blue-100", "w-full justify-start text-[hsl(var(--sidebar-foreground))] hover:bg-[hsl(var(--sidebar-hover))]",
pathname === item.href && "bg-blue-100 font-semibold" pathname === item.href && "bg-[hsl(var(--sidebar-hover))] font-semibold"
)} )}
> >
<Link href={item.href} onClick={() => setIsOpen(false)}> <Link href={item.href} onClick={() => setIsOpen(false)}>
@@ -56,31 +65,24 @@ export function Sidebar() {
})} })}
</ul> </ul>
</nav> </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 justify-between">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<UserButton /> <UserButton />
<div> <div>
<p className="text-sm font-medium text-blue-800">{user?.fullName ?? 'User'}</p> <p className="text-sm font-medium text-[hsl(var(--sidebar-foreground))]">{user?.fullName ?? 'User'}</p>
<p className="text-xs text-blue-600">{user?.primaryEmailAddress?.emailAddress ?? 'user@example.com'}</p> <p className="text-xs text-[hsl(var(--sidebar-muted))]">{user?.primaryEmailAddress?.emailAddress ?? 'user@example.com'}</p>
</div> </div>
</div> </div>
<ThemeToggle />
</div> </div>
</div> </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 ( 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"> <div className="flex h-14 items-center justify-between border-b px-4 bg-background">
<HRIStudioLogo /> <HRIStudioLogo />
<Sheet open={isOpen} onOpenChange={setIsOpen}> <Sheet open={isOpen} onOpenChange={setIsOpen}>
@@ -95,7 +97,7 @@ export function Sidebar() {
</Sheet> </Sheet>
</div> </div>
</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"> <div className="flex h-14 items-center border-b px-4">
<HRIStudioLogo /> <HRIStudioLogo />
</div> </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 }

View File

@@ -18,9 +18,17 @@ export const StudyProvider: React.FC<React.PropsWithChildren> = ({ children }) =
const [studies, setStudies] = useState<Study[]>([]); const [studies, setStudies] = useState<Study[]>([]);
const fetchAndSetStudies = async () => { const fetchAndSetStudies = async () => {
const response = await fetch('/api/studies'); try {
const fetchedStudies = await response.json(); const response = await fetch('/api/studies');
setStudies(fetchedStudies); if (!response.ok) {
throw new Error('Failed to fetch studies');
}
const fetchedStudies = await response.json();
setStudies(fetchedStudies);
} catch (error) {
console.error('Error fetching studies:', error);
setStudies([]);
}
}; };
const validateAndSetSelectedStudy = async (studyId: number) => { const validateAndSetSelectedStudy = async (studyId: number) => {

15
src/lib/fileStorage.ts Normal file
View File

@@ -0,0 +1,15 @@
import fs from 'fs/promises';
import path from 'path';
const CONTENT_DIR = path.join(process.cwd(), 'content');
export async function saveFile(file: File, contentId: number): Promise<string> {
const contentFolder = path.join(CONTENT_DIR, contentId.toString());
await fs.mkdir(contentFolder, { recursive: true });
const buffer = Buffer.from(await file.arrayBuffer());
const filePath = path.join(contentFolder, file.name);
await fs.writeFile(filePath, buffer);
return filePath;
}

View File

@@ -16,3 +16,8 @@ const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
if (env.NODE_ENV !== "production") globalForDb.conn = conn; if (env.NODE_ENV !== "production") globalForDb.conn = conn;
export const db = drizzle(conn, { schema }); export const db = drizzle(conn, { schema });
import { initializeContentTypes } from "./init";
// Initialize content types
initializeContentTypes().catch(console.error);

16
src/server/db/init.ts Normal file
View File

@@ -0,0 +1,16 @@
import { db } from "./index";
import { contentTypes } from "./schema";
export async function initializeContentTypes() {
const existingTypes = await db.select().from(contentTypes);
if (existingTypes.length === 0) {
await db.insert(contentTypes).values([
{ name: "Informed Consent Form" },
// Add other content types as needed
]);
console.log("Content types initialized");
} else {
console.log("Content types already exist");
}
}

View File

@@ -45,4 +45,38 @@ export const participants = createTable(
.default(sql`CURRENT_TIMESTAMP`) .default(sql`CURRENT_TIMESTAMP`)
.notNull(), .notNull(),
} }
);
export const contentTypes = createTable(
"content_type",
{
id: serial("id").primaryKey(),
name: varchar("name", { length: 50 }).notNull().unique(),
}
);
export const contents = createTable(
"content",
{
id: serial("id").primaryKey(),
contentTypeId: integer("content_type_id").references(() => contentTypes.id).notNull(),
uploader: varchar("uploader", { length: 256 }).notNull(),
location: varchar("location", { length: 1000 }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
}
);
export const informedConsentForms = createTable(
"informed_consent_form",
{
id: serial("id").primaryKey(),
studyId: integer("study_id").references(() => studies.id).notNull(),
participantId: integer("participant_id").references(() => participants.id).notNull(),
contentId: integer("content_id").references(() => contents.id).notNull(),
uploadedAt: timestamp("uploaded_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
}
); );

View File

@@ -1,61 +1,76 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 210 50% 98%;
--foreground: 240 10% 3.9%; --foreground: 215 25% 27%;
--card: 0 0% 100%; --card: 210 50% 98%;
--card-foreground: 240 10% 3.9%; --card-foreground: 215 25% 27%;
--popover: 0 0% 100%; --popover: 210 50% 98%;
--popover-foreground: 240 10% 3.9%; --popover-foreground: 215 25% 27%;
--primary: 240 5.9% 10%; --primary: 215 60% 40%;
--primary-foreground: 0 0% 98%; --primary-foreground: 210 50% 98%;
--secondary: 240 4.8% 95.9%; --secondary: 210 55% 92%;
--secondary-foreground: 240 5.9% 10%; --secondary-foreground: 215 25% 27%;
--muted: 240 4.8% 95.9%; --muted: 210 55% 92%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 215 20% 50%;
--accent: 240 4.8% 95.9%; --accent: 210 55% 92%;
--accent-foreground: 240 5.9% 10%; --accent-foreground: 215 25% 27%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84% 60%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 210 50% 98%;
--border: 240 5.9% 90%; --border: 214 32% 91%;
--input: 240 5.9% 90%; --input: 214 32% 91%;
--ring: 240 10% 3.9%; --ring: 215 60% 40%;
--chart-1: 12 76% 61%; --radius: 0.5rem;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%; /* Update gradient variables */
--chart-4: 43 74% 66%; --gradient-start: 210 50% 96%;
--chart-5: 27 87% 67%; --gradient-end: 210 50% 98%;
--radius: 0.5rem
/* Update sidebar variables */
--sidebar-background-top: 210 55% 92%;
--sidebar-background-bottom: 210 55% 88%;
--sidebar-foreground: 215 25% 27%;
--sidebar-muted: 215 20% 50%;
--sidebar-hover: 210 60% 86%;
} }
.dark { .dark {
--background: 240 10% 3.9%; --background: 220 20% 15%;
--foreground: 0 0% 98%; --foreground: 220 15% 85%;
--card: 240 10% 3.9%; --card: 220 20% 15%;
--card-foreground: 0 0% 98%; --card-foreground: 220 15% 85%;
--popover: 240 10% 3.9%; --popover: 220 20% 15%;
--popover-foreground: 0 0% 98%; --popover-foreground: 220 15% 85%;
--primary: 0 0% 98%; --primary: 220 60% 50%;
--primary-foreground: 240 5.9% 10%; --primary-foreground: 220 15% 85%;
--secondary: 240 3.7% 15.9%; --secondary: 220 25% 20%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 220 15% 85%;
--muted: 240 3.7% 15.9%; --muted: 220 25% 20%;
--muted-foreground: 240 5% 64.9%; --muted-foreground: 220 15% 65%;
--accent: 240 3.7% 15.9%; --accent: 220 25% 20%;
--accent-foreground: 0 0% 98%; --accent-foreground: 220 15% 85%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62% 30%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 220 15% 85%;
--border: 240 3.7% 15.9%; --border: 220 25% 20%;
--input: 240 3.7% 15.9%; --input: 220 25% 20%;
--ring: 240 4.9% 83.9%; --ring: 220 60% 50%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; /* Update gradient variables for dark mode */
--chart-3: 30 80% 55%; --gradient-start: 220 20% 13%;
--chart-4: 280 65% 60%; --gradient-end: 220 20% 15%;
--chart-5: 340 75% 55%
/* Update sidebar variables for dark mode */
--sidebar-background-top: 220 20% 15%;
--sidebar-background-bottom: 220 20% 12%;
--sidebar-foreground: 220 15% 85%;
--sidebar-muted: 220 15% 65%;
--sidebar-hover: 220 25% 18%;
} }
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;