aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/components/shared
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/shared')
-rw-r--r--frontend/src/components/shared/Notification.tsx82
-rw-r--r--frontend/src/components/shared/NotificationContext.tsx55
-rw-r--r--frontend/src/components/shared/ThemeProvider.tsx56
-rw-r--r--frontend/src/components/shared/ThemeToggle.tsx36
4 files changed, 229 insertions, 0 deletions
diff --git a/frontend/src/components/shared/Notification.tsx b/frontend/src/components/shared/Notification.tsx
new file mode 100644
index 0000000..bcc11c4
--- /dev/null
+++ b/frontend/src/components/shared/Notification.tsx
@@ -0,0 +1,82 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { CheckCircle, AlertCircle, X } from 'lucide-react';
+
+export type NotificationType = 'success' | 'error' | 'info';
+
+interface NotificationProps {
+ type: NotificationType;
+ message: string;
+ duration?: number;
+ onClose?: () => void;
+}
+
+export function Notification({
+ type,
+ message,
+ duration = 5000, // Default duration of 5 seconds
+ onClose
+}: NotificationProps) {
+ const [isVisible, setIsVisible] = useState(true);
+
+ useEffect(() => {
+ if (duration > 0) {
+ const timer = setTimeout(() => {
+ setIsVisible(false);
+ if (onClose) onClose();
+ }, duration);
+
+ return () => clearTimeout(timer);
+ }
+ }, [duration, onClose]);
+
+ const handleClose = () => {
+ setIsVisible(false);
+ if (onClose) onClose();
+ };
+
+ if (!isVisible) return null;
+
+ const getIcon = () => {
+ switch (type) {
+ case 'success':
+ return <CheckCircle className="h-5 w-5 text-green-500" />;
+ case 'error':
+ return <AlertCircle className="h-5 w-5 text-red-500" />;
+ case 'info':
+ return <AlertCircle className="h-5 w-5 text-blue-500" />;
+ default:
+ return null;
+ }
+ };
+
+ const getContainerClasses = () => {
+ const baseClasses = 'fixed top-4 right-4 z-50 flex items-center gap-3 rounded-lg p-4 shadow-md max-w-sm';
+
+ switch (type) {
+ case 'success':
+ return `${baseClasses} bg-green-50 text-green-800 border border-green-200`;
+ case 'error':
+ return `${baseClasses} bg-red-50 text-red-800 border border-red-200`;
+ case 'info':
+ return `${baseClasses} bg-blue-50 text-blue-800 border border-blue-200`;
+ default:
+ return baseClasses;
+ }
+ };
+
+ return (
+ <div className={getContainerClasses()}>
+ {getIcon()}
+ <div className="flex-1">{message}</div>
+ <button
+ onClick={handleClose}
+ className="text-gray-500 hover:text-gray-700 focus:outline-none"
+ aria-label="Close notification"
+ >
+ <X className="h-4 w-4" />
+ </button>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/frontend/src/components/shared/NotificationContext.tsx b/frontend/src/components/shared/NotificationContext.tsx
new file mode 100644
index 0000000..a8f6454
--- /dev/null
+++ b/frontend/src/components/shared/NotificationContext.tsx
@@ -0,0 +1,55 @@
+'use client';
+
+import { createContext, useContext, useState, ReactNode } from 'react';
+import { Notification, NotificationType } from './Notification';
+
+type NotificationData = {
+ id: string;
+ type: NotificationType;
+ message: string;
+ duration?: number;
+};
+
+interface NotificationContextType {
+ showNotification: (type: NotificationType, message: string, duration?: number) => void;
+ hideNotification: (id: string) => void;
+}
+
+const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
+
+export function NotificationProvider({ children }: { children: ReactNode }) {
+ const [notifications, setNotifications] = useState<NotificationData[]>([]);
+
+ const showNotification = (type: NotificationType, message: string, duration?: number) => {
+ const id = Math.random().toString(36).substring(2, 9);
+ setNotifications(prev => [...prev, { id, type, message, duration }]);
+ return id;
+ };
+
+ const hideNotification = (id: string) => {
+ setNotifications(prev => prev.filter(notification => notification.id !== id));
+ };
+
+ return (
+ <NotificationContext.Provider value={{ showNotification, hideNotification }}>
+ {children}
+ {notifications.map(notification => (
+ <Notification
+ key={notification.id}
+ type={notification.type}
+ message={notification.message}
+ duration={notification.duration}
+ onClose={() => hideNotification(notification.id)}
+ />
+ ))}
+ </NotificationContext.Provider>
+ );
+}
+
+export function useNotification() {
+ const context = useContext(NotificationContext);
+ if (context === undefined) {
+ throw new Error('useNotification must be used within a NotificationProvider');
+ }
+ return context;
+} \ No newline at end of file
diff --git a/frontend/src/components/shared/ThemeProvider.tsx b/frontend/src/components/shared/ThemeProvider.tsx
new file mode 100644
index 0000000..2fc67d9
--- /dev/null
+++ b/frontend/src/components/shared/ThemeProvider.tsx
@@ -0,0 +1,56 @@
+'use client';
+
+import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
+
+type Theme = 'light' | 'dark' | 'system';
+
+interface ThemeContextType {
+ theme: Theme;
+ setTheme: (theme: Theme) => void;
+}
+
+const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
+
+export function ThemeProvider({ children }: { children: ReactNode }) {
+ const [theme, setTheme] = useState<Theme>('system');
+
+ useEffect(() => {
+ // Get the theme preference from localStorage if available
+ const storedTheme = localStorage.getItem('theme') as Theme | null;
+ if (storedTheme) {
+ setTheme(storedTheme);
+ }
+ }, []);
+
+ useEffect(() => {
+ const root = window.document.documentElement;
+
+ // Remove all theme classes
+ root.classList.remove('light', 'dark');
+
+ // Add the appropriate class based on theme
+ if (theme === 'system') {
+ const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ root.classList.add(systemTheme);
+ } else {
+ root.classList.add(theme);
+ }
+
+ // Store the preference
+ localStorage.setItem('theme', theme);
+ }, [theme]);
+
+ return (
+ <ThemeContext.Provider value={{ theme, setTheme }}>
+ {children}
+ </ThemeContext.Provider>
+ );
+}
+
+export function useTheme() {
+ const context = useContext(ThemeContext);
+ if (context === undefined) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+ return context;
+} \ No newline at end of file
diff --git a/frontend/src/components/shared/ThemeToggle.tsx b/frontend/src/components/shared/ThemeToggle.tsx
new file mode 100644
index 0000000..679bbc5
--- /dev/null
+++ b/frontend/src/components/shared/ThemeToggle.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { Moon, Sun } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { useTheme } from './ThemeProvider';
+
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+
+ const toggleTheme = () => {
+ if (theme === 'dark') {
+ setTheme('light');
+ } else {
+ setTheme('dark');
+ }
+ };
+
+ return (
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={toggleTheme}
+ aria-label={
+ theme === 'dark'
+ ? 'Switch to light theme'
+ : 'Switch to dark theme'
+ }
+ >
+ {theme === 'dark' ? (
+ <Sun className="h-[1.2rem] w-[1.2rem]" />
+ ) : (
+ <Moon className="h-[1.2rem] w-[1.2rem]" />
+ )}
+ </Button>
+ );
+} \ No newline at end of file