aboutsummaryrefslogtreecommitdiffstats
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/src/app/(auth)/login/page.tsx14
-rw-r--r--frontend/src/app/(auth)/signup/page.tsx16
-rw-r--r--frontend/src/app/(main)/layout.tsx437
-rw-r--r--frontend/src/app/providers.tsx5
-rw-r--r--frontend/src/components/shared/AuthContext.tsx92
-rw-r--r--frontend/src/components/shared/Notification.tsx58
-rw-r--r--frontend/src/components/shared/ProtectedRoute.tsx37
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