From 8733795c8449f3514369d7b4220934760e386f1b Mon Sep 17 00:00:00 2001 From: Biswa Kalyan Bhuyan Date: Fri, 25 Apr 2025 01:09:30 +0530 Subject: finance/frontend: fix: did some minor changes to the frontend --- frontend/src/app/(auth)/login/page.tsx | 14 +- frontend/src/app/(auth)/signup/page.tsx | 16 +- frontend/src/app/(main)/layout.tsx | 437 ++++++++++------------ frontend/src/app/providers.tsx | 5 +- frontend/src/components/shared/AuthContext.tsx | 92 +++++ frontend/src/components/shared/Notification.tsx | 58 +-- frontend/src/components/shared/ProtectedRoute.tsx | 37 ++ 7 files changed, 392 insertions(+), 267 deletions(-) create mode 100644 frontend/src/components/shared/AuthContext.tsx create mode 100644 frontend/src/components/shared/ProtectedRoute.tsx (limited to 'frontend') 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(''); - 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 ( -
-
-
-
-
-

Loading...

-
-
- ); - } - return ( -
- {/* Top Navigation */} -
-
-
- {/* Mobile menu button - only visible on small screens */} - -
- - Finance Management - + +
+ {/* Top Navigation */} +
+
+
+ {/* Mobile menu button - only visible on small screens */} + +
+ + Finance Management + +
-
- -
-
- - - - - - {userName} - + +
+
+ + + + + + {user?.Name} + +
+ +
- - -
-
-
- - {/* Mobile Menu - outside the normal flow and only visible when toggled */} -
-
- -
-
-
-
+
- {/* Main Content */} -
- {/* Sidebar - hidden on mobile */} - + + +
+ +
+
+
- {/* Page Content */} -
- - {children} - -
+ {/* Main Content */} +
+ {/* Sidebar - hidden on mobile */} + + + {/* Page Content */} +
+ + {children} + +
+
- + ); } \ 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 ( - {children} + + {children} + ); } \ 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; + signup: (name: string, email: string, password: string) => Promise; + logout: () => void; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(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 ( + + {children} + + ); +} + +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 { type: NotificationType; message: string; duration?: number; @@ -41,38 +59,30 @@ export function Notification({ const getIcon = () => { switch (type) { case 'success': - return ; + return ; case 'error': - return ; + return ; case 'info': - return ; + return ; 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 ( -
- {getIcon()} -
{message}
+
+
+ {getIcon()} +
+
{message}