diff options
Diffstat (limited to 'frontend/src/components/shared')
-rw-r--r-- | frontend/src/components/shared/AuthContext.tsx | 92 | ||||
-rw-r--r-- | frontend/src/components/shared/Notification.tsx | 58 | ||||
-rw-r--r-- | frontend/src/components/shared/ProtectedRoute.tsx | 37 |
3 files changed, 163 insertions, 24 deletions
diff --git a/frontend/src/components/shared/AuthContext.tsx b/frontend/src/components/shared/AuthContext.tsx new file mode 100644 index 0000000..a3ec5a0 --- /dev/null +++ b/frontend/src/components/shared/AuthContext.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +import { useRouter } from 'next/navigation'; +import { authApi, userApi } from '@/lib/api'; + +interface User { + ID: number; + Name: string; + Email: string; +} + +interface AuthContextType { + user: User | null; + isLoading: boolean; + isAuthenticated: boolean; + login: (email: string, password: string) => Promise<void>; + signup: (name: string, email: string, password: string) => Promise<void>; + logout: () => void; +} + +const AuthContext = createContext<AuthContextType | null>(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState<User | null>(null); + const [isLoading, setIsLoading] = useState<boolean>(true); + const router = useRouter(); + + // Check if the user is already logged in + useEffect(() => { + const checkAuth = async () => { + const token = localStorage.getItem('token'); + if (!token) { + setIsLoading(false); + return; + } + + try { + const userData = await userApi.getProfile(); + setUser(userData); + } catch (error) { + // Clear invalid token + localStorage.removeItem('token'); + } finally { + setIsLoading(false); + } + }; + + checkAuth(); + }, []); + + // Login function + const login = async (email: string, password: string) => { + const response = await authApi.login(email, password); + setUser(response.user); + return response; + }; + + // Signup function + const signup = async (name: string, email: string, password: string) => { + return await authApi.signup(name, email, password); + }; + + // Logout function + const logout = () => { + authApi.logout(); + setUser(null); + }; + + return ( + <AuthContext.Provider + value={{ + user, + isLoading, + isAuthenticated: !!user, + login, + signup, + logout + }} + > + {children} + </AuthContext.Provider> + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}
\ No newline at end of file diff --git a/frontend/src/components/shared/Notification.tsx b/frontend/src/components/shared/Notification.tsx index bcc11c4..68cbc7a 100644 --- a/frontend/src/components/shared/Notification.tsx +++ b/frontend/src/components/shared/Notification.tsx @@ -1,11 +1,29 @@ 'use client'; import { useEffect, useState } from 'react'; -import { CheckCircle, AlertCircle, X } from 'lucide-react'; +import { CheckCircle, AlertCircle, Info, X } from 'lucide-react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { cn } from '@/lib/utils'; export type NotificationType = 'success' | 'error' | 'info'; -interface NotificationProps { +const notificationVariants = cva( + "fixed z-50 top-4 right-4 flex items-center gap-3 p-4 rounded-lg shadow-lg border max-w-sm transition-all duration-300 animate-in fade-in slide-in-from-top-5", + { + variants: { + variant: { + success: "bg-background border-border text-foreground", + error: "bg-background border-border text-foreground", + info: "bg-background border-border text-foreground", + } + }, + defaultVariants: { + variant: "info", + }, + } +); + +interface NotificationProps extends VariantProps<typeof notificationVariants> { type: NotificationType; message: string; duration?: number; @@ -41,38 +59,30 @@ export function Notification({ const getIcon = () => { switch (type) { case 'success': - return <CheckCircle className="h-5 w-5 text-green-500" />; + return <CheckCircle className="h-5 w-5 text-primary" />; case 'error': - return <AlertCircle className="h-5 w-5 text-red-500" />; + return <AlertCircle className="h-5 w-5 text-destructive" />; case 'info': - return <AlertCircle className="h-5 w-5 text-blue-500" />; + return <Info className="h-5 w-5 text-primary" />; 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> + <div className={cn(notificationVariants({ variant: type as any }))}> + <div className={cn( + "flex h-8 w-8 items-center justify-center rounded-full", + type === 'success' && "bg-primary/10", + type === 'error' && "bg-destructive/10", + type === 'info' && "bg-primary/10" + )}> + {getIcon()} + </div> + <div className="flex-1 text-sm font-medium">{message}</div> <button onClick={handleClose} - className="text-gray-500 hover:text-gray-700 focus:outline-none" + className="rounded-full p-1 text-muted-foreground hover:bg-muted hover:text-foreground focus:outline-none transition-colors" aria-label="Close notification" > <X className="h-4 w-4" /> diff --git a/frontend/src/components/shared/ProtectedRoute.tsx b/frontend/src/components/shared/ProtectedRoute.tsx new file mode 100644 index 0000000..f5ecede --- /dev/null +++ b/frontend/src/components/shared/ProtectedRoute.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuth } from './AuthContext'; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export default function ProtectedRoute({ children }: ProtectedRouteProps) { + const { isAuthenticated, isLoading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + router.push('/login'); + } + }, [isAuthenticated, isLoading, router]); + + // Show nothing while checking authentication + if (isLoading) { + return ( + <div className="flex items-center justify-center h-screen"> + <div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div> + </div> + ); + } + + // If not authenticated, don't render children (will redirect in useEffect) + if (!isAuthenticated) { + return null; + } + + // If authenticated, render children + return <>{children}</>; +}
\ No newline at end of file |