diff options
author | 2025-04-25 01:09:30 +0530 | |
---|---|---|
committer | 2025-04-25 01:09:30 +0530 | |
commit | 8733795c8449f3514369d7b4220934760e386f1b (patch) | |
tree | 86b70bd897f22646df401f5e24e28bb35f3ca155 /frontend | |
parent | 4cd8498ad30e6ea5f01e81346a81a2a1134864be (diff) | |
download | finance-8733795c8449f3514369d7b4220934760e386f1b.tar.gz finance-8733795c8449f3514369d7b4220934760e386f1b.tar.bz2 finance-8733795c8449f3514369d7b4220934760e386f1b.zip |
finance/frontend: fix: did some minor changes to the frontend
Diffstat (limited to 'frontend')
-rw-r--r-- | frontend/src/app/(auth)/login/page.tsx | 14 | ||||
-rw-r--r-- | frontend/src/app/(auth)/signup/page.tsx | 16 | ||||
-rw-r--r-- | frontend/src/app/(main)/layout.tsx | 437 | ||||
-rw-r--r-- | frontend/src/app/providers.tsx | 5 | ||||
-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 |
7 files changed, 392 insertions, 267 deletions
diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index 7460b8a..23cdd24 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -6,13 +6,14 @@ import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { authApi } from '@/lib/api'; +import { useAuth } from '@/components/shared/AuthContext'; import { useNotification } from '@/components/shared/NotificationContext'; export default function LoginPage() { const router = useRouter(); const searchParams = useSearchParams(); const { showNotification } = useNotification(); + const { login, isAuthenticated } = useAuth(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); @@ -23,7 +24,12 @@ export default function LoginPage() { if (searchParams.get('signup') === 'success') { showNotification('success', 'Account created successfully! Please log in.'); } - }, [searchParams, showNotification]); + + // Redirect if already authenticated + if (isAuthenticated) { + router.push('/dashboard'); + } + }, [searchParams, showNotification, isAuthenticated, router]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -31,8 +37,8 @@ export default function LoginPage() { setIsLoading(true); try { - await authApi.login(email, password); - showNotification('success', 'Logged in successfully!'); + const response = await login(email, password); + showNotification('success', `Welcome back, ${response?.user?.Name || 'user'}! You've been successfully logged in.`); router.push('/dashboard'); } catch (err: any) { setError(err.message || 'Login failed'); diff --git a/frontend/src/app/(auth)/signup/page.tsx b/frontend/src/app/(auth)/signup/page.tsx index af25031..cd9daab 100644 --- a/frontend/src/app/(auth)/signup/page.tsx +++ b/frontend/src/app/(auth)/signup/page.tsx @@ -1,17 +1,18 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import { authApi } from '@/lib/api'; +import { useAuth } from '@/components/shared/AuthContext'; import { useNotification } from '@/components/shared/NotificationContext'; export default function SignupPage() { const router = useRouter(); const { showNotification } = useNotification(); + const { signup, isAuthenticated } = useAuth(); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -19,6 +20,13 @@ export default function SignupPage() { const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); + useEffect(() => { + // Redirect if already authenticated + if (isAuthenticated) { + router.push('/dashboard'); + } + }, [isAuthenticated, router]); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); @@ -32,9 +40,9 @@ export default function SignupPage() { setIsLoading(true); try { - await authApi.signup(name, email, password); + await signup(name, email, password); // Show success notification - showNotification('success', `Your email ${email} has been successfully registered!`); + showNotification('success', `Welcome to Finance Management, ${name}! Your account has been successfully created.`); // Redirect to login after successful signup with a slight delay to see the notification setTimeout(() => { router.push('/login?signup=success'); diff --git a/frontend/src/app/(main)/layout.tsx b/frontend/src/app/(main)/layout.tsx index ee1026c..11e557b 100644 --- a/frontend/src/app/(main)/layout.tsx +++ b/frontend/src/app/(main)/layout.tsx @@ -1,12 +1,12 @@ 'use client'; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useEffect } from 'react'; import Link from 'next/link'; import { Button } from '@/components/ui/button'; -import { authApi, userApi } from '@/lib/api'; import { ThemeToggle } from '@/components/shared/ThemeToggle'; import { PageTransition } from '@/components/shared/PageTransition'; +import ProtectedRoute from '@/components/shared/ProtectedRoute'; +import { useAuth } from '@/components/shared/AuthContext'; import { ChevronLeftIcon, ChevronRightIcon, @@ -25,38 +25,18 @@ export default function MainLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const router = useRouter(); const pathname = usePathname(); - const [userName, setUserName] = useState<string>(''); - const [isLoading, setIsLoading] = useState(true); + const { user, logout } = useAuth(); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); useEffect(() => { - // Check if user is authenticated - const token = localStorage.getItem('token'); - if (!token) { - router.push('/login'); - return; - } - - // Fetch user profile - userApi.getProfile() - .then(data => { - setUserName(data.user.Name); - setIsLoading(false); - }) - .catch(() => { - // If error fetching profile, redirect to login - router.push('/login'); - }); - // Load sidebar state from localStorage const savedSidebarState = localStorage.getItem('sidebarCollapsed'); if (savedSidebarState !== null) { setIsSidebarCollapsed(savedSidebarState === 'true'); } - }, [router]); + }, []); // Close mobile menu when changing routes useEffect(() => { @@ -64,7 +44,7 @@ export default function MainLayout({ }, [pathname]); const handleLogout = () => { - authApi.logout(); + logout(); }; const toggleSidebar = () => { @@ -77,227 +57,216 @@ export default function MainLayout({ setIsMobileMenuOpen(!isMobileMenuOpen); }; - if (isLoading) { - return ( - <div className="flex h-screen items-center justify-center bg-background"> - <div className="flex flex-col items-center space-y-4"> - <div className="relative h-12 w-12"> - <div className="absolute top-0 h-full w-full rounded-full border-4 border-t-primary border-r-transparent border-b-transparent border-l-transparent animate-spin"></div> - </div> - <p className="text-sm font-medium text-muted-foreground animate-pulse">Loading...</p> - </div> - </div> - ); - } - return ( - <div className="min-h-screen bg-background flex flex-col"> - {/* Top Navigation */} - <header className="border-b shadow-sm backdrop-blur-sm bg-background/90 sticky top-0 z-50 w-full"> - <div className="w-full flex h-16 items-center px-4 md:px-6"> - <div className="flex items-center gap-4"> - {/* Mobile menu button - only visible on small screens */} - <button - className="md:hidden rounded-full p-2 hover:bg-muted transition-colors" - onClick={toggleMobileMenu} - aria-label="Toggle mobile menu" - > - <MenuIcon size={20} /> - </button> - <div className="font-semibold text-xl"> - <span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/70"> - Finance Management - </span> + <ProtectedRoute> + <div className="min-h-screen bg-background flex flex-col"> + {/* Top Navigation */} + <header className="border-b shadow-sm backdrop-blur-sm bg-background/90 sticky top-0 z-50 w-full"> + <div className="w-full flex h-16 items-center px-4 md:px-6"> + <div className="flex items-center gap-4"> + {/* Mobile menu button - only visible on small screens */} + <button + className="md:hidden rounded-full p-2 hover:bg-muted transition-colors" + onClick={toggleMobileMenu} + aria-label="Toggle mobile menu" + > + <MenuIcon size={20} /> + </button> + <div className="font-semibold text-xl"> + <span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/70"> + Finance Management + </span> + </div> </div> - </div> - - <div className="flex items-center ms-auto gap-4"> - <div className="hidden md:flex items-center gap-1 bg-muted/50 px-3 py-1.5 rounded-full"> - <span className="relative flex h-2 w-2"> - <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75"></span> - <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span> - </span> - <span className='text-sm font-medium ml-1.5 transition-all duration-300'> - {userName} - </span> + + <div className="flex items-center ms-auto gap-4"> + <div className="hidden md:flex items-center gap-1 bg-muted/50 px-3 py-1.5 rounded-full"> + <span className="relative flex h-2 w-2"> + <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary/50 opacity-75"></span> + <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span> + </span> + <span className='text-sm font-medium ml-1.5 transition-all duration-300'> + {user?.Name} + </span> + </div> + <ThemeToggle /> + <Button + variant="ghost" + size="icon" + onClick={handleLogout} + className="hover:bg-destructive/10 transition-colors duration-300" + aria-label="Logout" + > + <LogOutIcon size={18} className="text-destructive/90 transition-transform hover:scale-110 duration-300" /> + </Button> </div> - <ThemeToggle /> - <Button - variant="ghost" - size="icon" - onClick={handleLogout} - className="hover:bg-destructive/10 transition-colors duration-300" - aria-label="Logout" - > - <LogOutIcon size={18} className="text-destructive/90 transition-transform hover:scale-110 duration-300" /> - </Button> - </div> - </div> - </header> - - {/* Mobile Menu - outside the normal flow and only visible when toggled */} - <div - className={` - md:hidden fixed inset-0 z-40 bg-background/95 backdrop-blur-sm transition-all duration-300 - ${isMobileMenuOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'} - `} - > - <div className="pt-20 px-4"> - <nav className="space-y-4"> - <Link - href="/dashboard" - className={` - flex items-center p-3 rounded-lg transition-colors - ${pathname === '/dashboard' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'} - `} - > - <LayoutDashboardIcon size={20} className="mr-3" /> - Dashboard - </Link> - <Link - href="/loans" - className={` - flex items-center p-3 rounded-lg transition-colors - ${pathname === '/loans' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'} - `} - > - <CoinsIcon size={20} className="mr-3" /> - Loans - </Link> - <Link - href="/goals" - className={` - flex items-center p-3 rounded-lg transition-colors - ${pathname === '/goals' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'} - `} - > - <TargetIcon size={20} className="mr-3" /> - Goals - </Link> - <Link - href="/settings" - className={` - flex items-center p-3 rounded-lg transition-colors - ${pathname === '/settings' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'} - `} - > - <SettingsIcon size={20} className="mr-3" /> - Settings - </Link> - </nav> - <div className="mt-8 border-t pt-4"> - <Button - variant="outline" - className="w-full justify-start text-destructive hover:text-destructive hover:bg-destructive/10" - onClick={handleLogout} - > - <LogOutIcon size={18} className="mr-2" /> - Logout - </Button> </div> - </div> - </div> + </header> - {/* Main Content */} - <div className="flex flex-1 overflow-hidden"> - {/* Sidebar - hidden on mobile */} - <aside + {/* Mobile Menu - outside the normal flow and only visible when toggled */} + <div className={` - hidden md:flex flex-col h-[calc(100vh-4rem)] border-r bg-background flex-shrink-0 relative - transition-all duration-300 ease-in-out - ${isSidebarCollapsed ? 'w-16' : 'w-64'} + md:hidden fixed inset-0 z-40 bg-background/95 backdrop-blur-sm transition-all duration-300 + ${isMobileMenuOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'} `} > - {/* Sidebar Toggle Button */} - <button - onClick={toggleSidebar} - className="absolute -right-3 top-10 bg-primary text-primary-foreground rounded-full p-1 shadow-md hover:bg-primary/90 transition-colors duration-300 hover:scale-110 group z-10" - aria-label={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} - > - <div className="transition-transform duration-300 group-hover:rotate-[360deg]"> - {isSidebarCollapsed ? <ChevronRightIcon size={16} /> : <ChevronLeftIcon size={16} />} - </div> - </button> - - <nav className="space-y-2 px-2 mt-4 flex-1"> - <Link - href="/dashboard" - className={` - flex items-center p-2 rounded-md transition-all duration-200 overflow-hidden - ${pathname === '/dashboard' - ? 'bg-primary/10 text-primary font-medium' - : 'hover:bg-muted text-foreground/80 hover:text-foreground' - } - ${isSidebarCollapsed ? 'justify-center' : 'justify-start'} - `} - title="Dashboard" - > - <LayoutDashboardIcon size={18} className={`transition-transform duration-300 ${pathname === '/dashboard' ? 'scale-110' : ''}`} /> - <span className={`ml-2 transition-all duration-300 overflow-hidden whitespace-nowrap ${isSidebarCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}> + <div className="pt-20 px-4"> + <nav className="space-y-4"> + <Link + href="/dashboard" + className={` + flex items-center p-3 rounded-lg transition-colors + ${pathname === '/dashboard' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'} + `} + > + <LayoutDashboardIcon size={20} className="mr-3" /> Dashboard - </span> - </Link> - <Link - href="/loans" - className={` - flex items-center p-2 rounded-md transition-all duration-200 overflow-hidden - ${pathname === '/loans' - ? 'bg-primary/10 text-primary font-medium' - : 'hover:bg-muted text-foreground/80 hover:text-foreground' - } - ${isSidebarCollapsed ? 'justify-center' : 'justify-start'} - `} - title="Loans" - > - <CoinsIcon size={18} className={`transition-transform duration-300 ${pathname === '/loans' ? 'scale-110' : ''}`} /> - <span className={`ml-2 transition-all duration-300 overflow-hidden whitespace-nowrap ${isSidebarCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}> + </Link> + <Link + href="/loans" + className={` + flex items-center p-3 rounded-lg transition-colors + ${pathname === '/loans' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'} + `} + > + <CoinsIcon size={20} className="mr-3" /> Loans - </span> - </Link> - <Link - href="/goals" - className={` - flex items-center p-2 rounded-md transition-all duration-200 overflow-hidden - ${pathname === '/goals' - ? 'bg-primary/10 text-primary font-medium' - : 'hover:bg-muted text-foreground/80 hover:text-foreground' - } - ${isSidebarCollapsed ? 'justify-center' : 'justify-start'} - `} - title="Goals" - > - <TargetIcon size={18} className={`transition-transform duration-300 ${pathname === '/goals' ? 'scale-110' : ''}`} /> - <span className={`ml-2 transition-all duration-300 overflow-hidden whitespace-nowrap ${isSidebarCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}> + </Link> + <Link + href="/goals" + className={` + flex items-center p-3 rounded-lg transition-colors + ${pathname === '/goals' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'} + `} + > + <TargetIcon size={20} className="mr-3" /> Goals - </span> - </Link> - <Link - href="/settings" - className={` - flex items-center p-2 rounded-md transition-all duration-200 overflow-hidden - ${pathname === '/settings' - ? 'bg-primary/10 text-primary font-medium' - : 'hover:bg-muted text-foreground/80 hover:text-foreground' - } - ${isSidebarCollapsed ? 'justify-center' : 'justify-start'} - `} - title="Settings" - > - <SettingsIcon size={18} className={`transition-transform duration-300 ${pathname === '/settings' ? 'scale-110' : ''}`} /> - <span className={`ml-2 transition-all duration-300 overflow-hidden whitespace-nowrap ${isSidebarCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}> + </Link> + <Link + href="/settings" + className={` + flex items-center p-3 rounded-lg transition-colors + ${pathname === '/settings' ? 'bg-primary/10 text-primary' : 'hover:bg-muted'} + `} + > + <SettingsIcon size={20} className="mr-3" /> Settings - </span> - </Link> - </nav> - </aside> + </Link> + </nav> + <div className="mt-8 border-t pt-4"> + <Button + variant="outline" + className="w-full justify-start text-destructive hover:text-destructive hover:bg-destructive/10" + onClick={handleLogout} + > + <LogOutIcon size={18} className="mr-2" /> + Logout + </Button> + </div> + </div> + </div> - {/* Page Content */} - <main className="flex-1 p-4 md:p-8 overflow-auto h-[calc(100vh-4rem)] transition-all duration-300"> - <PageTransition> - {children} - </PageTransition> - </main> + {/* Main Content */} + <div className="flex flex-1 overflow-hidden"> + {/* Sidebar - hidden on mobile */} + <aside + className={` + hidden md:flex flex-col h-[calc(100vh-4rem)] border-r bg-background flex-shrink-0 relative + transition-all duration-300 ease-in-out + ${isSidebarCollapsed ? 'w-16' : 'w-64'} + `} + > + {/* Sidebar Toggle Button */} + <button + onClick={toggleSidebar} + className="absolute -right-3 top-10 bg-primary text-primary-foreground rounded-full p-1 shadow-md hover:bg-primary/90 transition-colors duration-300 hover:scale-110 group z-10" + aria-label={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} + > + <div className="transition-transform duration-300 group-hover:rotate-[360deg]"> + {isSidebarCollapsed ? <ChevronRightIcon size={16} /> : <ChevronLeftIcon size={16} />} + </div> + </button> + + <nav className="space-y-2 px-2 mt-4 flex-1"> + <Link + href="/dashboard" + className={` + flex items-center p-2 rounded-md transition-all duration-200 overflow-hidden + ${pathname === '/dashboard' + ? 'bg-primary/10 text-primary font-medium' + : 'hover:bg-muted text-foreground/80 hover:text-foreground' + } + ${isSidebarCollapsed ? 'justify-center' : 'justify-start'} + `} + title="Dashboard" + > + <LayoutDashboardIcon size={18} className={`transition-transform duration-300 ${pathname === '/dashboard' ? 'scale-110' : ''}`} /> + <span className={`ml-2 transition-all duration-300 overflow-hidden whitespace-nowrap ${isSidebarCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}> + Dashboard + </span> + </Link> + <Link + href="/loans" + className={` + flex items-center p-2 rounded-md transition-all duration-200 overflow-hidden + ${pathname === '/loans' + ? 'bg-primary/10 text-primary font-medium' + : 'hover:bg-muted text-foreground/80 hover:text-foreground' + } + ${isSidebarCollapsed ? 'justify-center' : 'justify-start'} + `} + title="Loans" + > + <CoinsIcon size={18} className={`transition-transform duration-300 ${pathname === '/loans' ? 'scale-110' : ''}`} /> + <span className={`ml-2 transition-all duration-300 overflow-hidden whitespace-nowrap ${isSidebarCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}> + Loans + </span> + </Link> + <Link + href="/goals" + className={` + flex items-center p-2 rounded-md transition-all duration-200 overflow-hidden + ${pathname === '/goals' + ? 'bg-primary/10 text-primary font-medium' + : 'hover:bg-muted text-foreground/80 hover:text-foreground' + } + ${isSidebarCollapsed ? 'justify-center' : 'justify-start'} + `} + title="Goals" + > + <TargetIcon size={18} className={`transition-transform duration-300 ${pathname === '/goals' ? 'scale-110' : ''}`} /> + <span className={`ml-2 transition-all duration-300 overflow-hidden whitespace-nowrap ${isSidebarCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}> + Goals + </span> + </Link> + <Link + href="/settings" + className={` + flex items-center p-2 rounded-md transition-all duration-200 overflow-hidden + ${pathname === '/settings' + ? 'bg-primary/10 text-primary font-medium' + : 'hover:bg-muted text-foreground/80 hover:text-foreground' + } + ${isSidebarCollapsed ? 'justify-center' : 'justify-start'} + `} + title="Settings" + > + <SettingsIcon size={18} className={`transition-transform duration-300 ${pathname === '/settings' ? 'scale-110' : ''}`} /> + <span className={`ml-2 transition-all duration-300 overflow-hidden whitespace-nowrap ${isSidebarCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}> + Settings + </span> + </Link> + </nav> + </aside> + + {/* Page Content */} + <main className="flex-1 p-4 md:p-8 overflow-auto h-[calc(100vh-4rem)] transition-all duration-300"> + <PageTransition> + {children} + </PageTransition> + </main> + </div> </div> - </div> + </ProtectedRoute> ); }
\ No newline at end of file diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx index efedc0b..40ce3a9 100644 --- a/frontend/src/app/providers.tsx +++ b/frontend/src/app/providers.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactNode, useState } from 'react'; +import { AuthProvider } from '@/components/shared/AuthContext'; interface ProvidersProps { children: ReactNode; @@ -19,7 +20,9 @@ export function Providers({ children }: ProvidersProps) { return ( <QueryClientProvider client={queryClient}> - {children} + <AuthProvider> + {children} + </AuthProvider> </QueryClientProvider> ); }
\ No newline at end of file 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 |