aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/app')
-rw-r--r--frontend/src/app/(auth)/layout.tsx36
-rw-r--r--frontend/src/app/(auth)/login/page.tsx88
-rw-r--r--frontend/src/app/(auth)/signup/page.tsx116
-rw-r--r--frontend/src/app/(main)/dashboard/page.tsx10
-rw-r--r--frontend/src/app/(main)/layout.tsx94
-rw-r--r--frontend/src/app/(main)/loans/page.tsx279
-rw-r--r--frontend/src/app/favicon.icobin0 -> 25931 bytes
-rw-r--r--frontend/src/app/globals.css122
-rw-r--r--frontend/src/app/layout.tsx43
-rw-r--r--frontend/src/app/page.tsx27
-rw-r--r--frontend/src/app/providers.tsx25
11 files changed, 840 insertions, 0 deletions
diff --git a/frontend/src/app/(auth)/layout.tsx b/frontend/src/app/(auth)/layout.tsx
new file mode 100644
index 0000000..9651b4b
--- /dev/null
+++ b/frontend/src/app/(auth)/layout.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+
+export default function AuthLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ const router = useRouter();
+
+ useEffect(() => {
+ // If already logged in, redirect to dashboard
+ const token = localStorage.getItem('token');
+ if (token) {
+ router.push('/dashboard');
+ }
+ }, [router]);
+
+ return (
+ <div className="flex min-h-screen flex-col items-center justify-center bg-muted/40">
+ <div className="w-full max-w-md rounded-lg border bg-card p-6 shadow-sm">
+ <div className="flex flex-col space-y-2 text-center mb-6">
+ <h1 className="text-2xl font-semibold tracking-tight">
+ Finance Management
+ </h1>
+ <p className="text-sm text-muted-foreground">
+ Manage your personal finances
+ </p>
+ </div>
+ {children}
+ </div>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..7460b8a
--- /dev/null
+++ b/frontend/src/app/(auth)/login/page.tsx
@@ -0,0 +1,88 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useRouter, useSearchParams } 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 { useNotification } from '@/components/shared/NotificationContext';
+
+export default function LoginPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const { showNotification } = useNotification();
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ // Check if the user was redirected from signup
+ if (searchParams.get('signup') === 'success') {
+ showNotification('success', 'Account created successfully! Please log in.');
+ }
+ }, [searchParams, showNotification]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setIsLoading(true);
+
+ try {
+ await authApi.login(email, password);
+ showNotification('success', 'Logged in successfully!');
+ router.push('/dashboard');
+ } catch (err: any) {
+ setError(err.message || 'Login failed');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <div>
+ <form onSubmit={handleSubmit} className="space-y-4">
+ <div className="space-y-2">
+ <Label htmlFor="email">Email</Label>
+ <Input
+ id="email"
+ type="email"
+ placeholder="your@email.com"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ required
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="password">Password</Label>
+ <Input
+ id="password"
+ type="password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ required
+ />
+ </div>
+
+ {error && (
+ <div className="text-sm text-red-500">
+ {error}
+ </div>
+ )}
+
+ <Button type="submit" className="w-full" disabled={isLoading}>
+ {isLoading ? 'Logging in...' : 'Login'}
+ </Button>
+ </form>
+
+ <div className="mt-4 text-center text-sm">
+ Don&apos;t have an account?{' '}
+ <Link href="/signup" className="text-primary underline underline-offset-2">
+ Sign up
+ </Link>
+ </div>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/frontend/src/app/(auth)/signup/page.tsx b/frontend/src/app/(auth)/signup/page.tsx
new file mode 100644
index 0000000..af25031
--- /dev/null
+++ b/frontend/src/app/(auth)/signup/page.tsx
@@ -0,0 +1,116 @@
+'use client';
+
+import { useState } 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 { useNotification } from '@/components/shared/NotificationContext';
+
+export default function SignupPage() {
+ const router = useRouter();
+ const { showNotification } = useNotification();
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [confirmPassword, setConfirmPassword] = useState('');
+ const [error, setError] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+
+ // Validate passwords match
+ if (password !== confirmPassword) {
+ setError('Passwords do not match');
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ await authApi.signup(name, email, password);
+ // Show success notification
+ showNotification('success', `Your email ${email} has been successfully registered!`);
+ // Redirect to login after successful signup with a slight delay to see the notification
+ setTimeout(() => {
+ router.push('/login?signup=success');
+ }, 1500);
+ } catch (err: any) {
+ setError(err.message || 'Signup failed');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <div>
+ <form onSubmit={handleSubmit} className="space-y-4">
+ <div className="space-y-2">
+ <Label htmlFor="name">Name</Label>
+ <Input
+ id="name"
+ placeholder="Your Name"
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="email">Email</Label>
+ <Input
+ id="email"
+ type="email"
+ placeholder="your@email.com"
+ value={email}
+ onChange={(e) => setEmail(e.target.value)}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="password">Password</Label>
+ <Input
+ id="password"
+ type="password"
+ value={password}
+ onChange={(e) => setPassword(e.target.value)}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="confirmPassword">Confirm Password</Label>
+ <Input
+ id="confirmPassword"
+ type="password"
+ value={confirmPassword}
+ onChange={(e) => setConfirmPassword(e.target.value)}
+ required
+ />
+ </div>
+
+ {error && (
+ <div className="text-sm text-red-500">
+ {error}
+ </div>
+ )}
+
+ <Button type="submit" className="w-full" disabled={isLoading}>
+ {isLoading ? 'Creating Account...' : 'Create Account'}
+ </Button>
+ </form>
+
+ <div className="mt-4 text-center text-sm">
+ Already have an account?{' '}
+ <Link href="/login" className="text-primary underline underline-offset-2">
+ Login
+ </Link>
+ </div>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/frontend/src/app/(main)/dashboard/page.tsx b/frontend/src/app/(main)/dashboard/page.tsx
new file mode 100644
index 0000000..ac5fd0d
--- /dev/null
+++ b/frontend/src/app/(main)/dashboard/page.tsx
@@ -0,0 +1,10 @@
+export default function DashboardPage() {
+ return (
+ <div>
+ <h1 className="text-3xl font-bold mb-6">Dashboard</h1>
+ <p className="text-muted-foreground">
+ Welcome to your Finance Management Dashboard. Navigate to the Loans section to manage your loans.
+ </p>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/frontend/src/app/(main)/layout.tsx b/frontend/src/app/(main)/layout.tsx
new file mode 100644
index 0000000..846a118
--- /dev/null
+++ b/frontend/src/app/(main)/layout.tsx
@@ -0,0 +1,94 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+import { authApi, userApi } from '@/lib/api';
+import { ThemeToggle } from '@/components/shared/ThemeToggle';
+
+
+export default function MainLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ const router = useRouter();
+ const [userName, setUserName] = useState<string>('');
+ const [isLoading, setIsLoading] = useState(true);
+
+ 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');
+ });
+ }, [router]);
+
+ const handleLogout = () => {
+ authApi.logout();
+ };
+
+ if (isLoading) {
+ return <div className="flex h-screen items-center justify-center">Loading...</div>;
+ }
+
+ return (
+ <div className="min-h-screen bg-background">
+ {/* Top Navigation */}
+ <header className="border-b">
+ <div className="w-full flex h-16 items-center px-4">
+ <div className="flex items-center gap-4">
+ <div className="font-semibold text-xl">Finance Management</div>
+ </div>
+
+ <div className="flex items-center ms-auto gap-4">
+ <span className='text-sm font-medium'>Welcome!, {userName}</span>
+ <ThemeToggle />
+ <Button variant="ghost" size="sm" onClick={handleLogout}>
+ Logout
+ </Button>
+ </div>
+ </div>
+ </header>
+
+ {/* Main Content */}
+ <div className="container flex flex-row">
+ {/* Sidebar */}
+ <aside className="w-64 pt-8 pr-6">
+ <nav className="space-y-2">
+ <Link href="/dashboard" className="block p-2 rounded-md hover:bg-muted">
+ Dashboard
+ </Link>
+ <Link href="/loans" className="block p-2 rounded-md hover:bg-muted">
+ Loans
+ </Link>
+ <Link href="/goals" className="block p-2 rounded-md hover:bg-muted">
+ Goals
+ </Link>
+ <Link href="/settings" className="block p-2 rounded-md hover:bg-muted">
+ Settings
+ </Link>
+ </nav>
+ </aside>
+
+ {/* Page Content */}
+ <main className="flex-1 p-8">
+ {children}
+ </main>
+ </div>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/frontend/src/app/(main)/loans/page.tsx b/frontend/src/app/(main)/loans/page.tsx
new file mode 100644
index 0000000..07c1428
--- /dev/null
+++ b/frontend/src/app/(main)/loans/page.tsx
@@ -0,0 +1,279 @@
+'use client';
+
+import { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '@/components/ui/table';
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { loanApi, Loan, LoanInput } from '@/lib/api';
+
+export default function LoansPage() {
+ const queryClient = useQueryClient();
+ const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
+ const [selectedLoan, setSelectedLoan] = useState<Loan | null>(null);
+
+ // Form state
+ const [loanName, setLoanName] = useState('');
+ const [originalAmount, setOriginalAmount] = useState('');
+ const [currentBalance, setCurrentBalance] = useState('');
+ const [interestRate, setInterestRate] = useState('');
+ const [startDate, setStartDate] = useState('');
+ const [endDate, setEndDate] = useState('');
+ const [formError, setFormError] = useState('');
+
+ // Query loans
+ const { data, isLoading, error } = useQuery({
+ queryKey: ['loans'],
+ queryFn: async () => {
+ const response = await loanApi.getLoans();
+ return response.loans as Loan[];
+ }
+ });
+
+ // Create loan mutation
+ const createLoanMutation = useMutation({
+ mutationFn: (loanData: LoanInput) => loanApi.createLoan(loanData),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['loans'] });
+ resetForm();
+ setIsAddDialogOpen(false);
+ },
+ onError: (error: Error) => {
+ setFormError(error.message);
+ }
+ });
+
+ // Delete loan mutation
+ const deleteLoanMutation = useMutation({
+ mutationFn: (id: number) => loanApi.deleteLoan(id),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['loans'] });
+ }
+ });
+
+ const resetForm = () => {
+ setLoanName('');
+ setOriginalAmount('');
+ setCurrentBalance('');
+ setInterestRate('');
+ setStartDate('');
+ setEndDate('');
+ setFormError('');
+ setSelectedLoan(null);
+ };
+
+ const handleCreateLoan = (e: React.FormEvent) => {
+ e.preventDefault();
+ setFormError('');
+
+ try {
+ // Validate inputs
+ if (!loanName || !originalAmount || !currentBalance || !startDate || !endDate) {
+ setFormError('Please fill in all required fields');
+ return;
+ }
+
+ const loanData: LoanInput = {
+ name: loanName,
+ originalAmount: Number(originalAmount) * 100, // Convert to cents
+ currentBalance: Number(currentBalance) * 100, // Convert to cents
+ interestRate: Number(interestRate) || 0,
+ startDate,
+ endDate,
+ };
+
+ createLoanMutation.mutate(loanData);
+ } catch (err: any) {
+ setFormError(err.message || 'Error creating loan');
+ }
+ };
+
+ const handleDeleteLoan = (id: number) => {
+ if (window.confirm('Are you sure you want to delete this loan?')) {
+ deleteLoanMutation.mutate(id);
+ }
+ };
+
+ // Format currency (convert cents to dollars)
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ }).format(amount / 100);
+ };
+
+ // Format date
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString();
+ };
+
+ if (isLoading) {
+ return <div>Loading loans...</div>;
+ }
+
+ if (error) {
+ return <div>Error loading loans: {error.toString()}</div>;
+ }
+
+ return (
+ <div className="space-y-6">
+ <div className="flex items-center justify-between">
+ <h1 className="text-3xl font-bold">Loans</h1>
+ <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
+ <DialogTrigger asChild>
+ <Button onClick={resetForm}>Add New Loan</Button>
+ </DialogTrigger>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Add New Loan</DialogTitle>
+ </DialogHeader>
+ <form onSubmit={handleCreateLoan} className="space-y-4">
+ <div className="space-y-2">
+ <Label htmlFor="name">Loan Name</Label>
+ <Input
+ id="name"
+ placeholder="e.g., Car Loan"
+ value={loanName}
+ onChange={(e) => setLoanName(e.target.value)}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="originalAmount">Original Amount ($)</Label>
+ <Input
+ id="originalAmount"
+ type="number"
+ step="0.01"
+ placeholder="10000.00"
+ value={originalAmount}
+ onChange={(e) => setOriginalAmount(e.target.value)}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="currentBalance">Current Balance ($)</Label>
+ <Input
+ id="currentBalance"
+ type="number"
+ step="0.01"
+ placeholder="9500.00"
+ value={currentBalance}
+ onChange={(e) => setCurrentBalance(e.target.value)}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="interestRate">Interest Rate (%)</Label>
+ <Input
+ id="interestRate"
+ type="number"
+ step="0.01"
+ placeholder="5.25"
+ value={interestRate}
+ onChange={(e) => setInterestRate(e.target.value)}
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="startDate">Start Date</Label>
+ <Input
+ id="startDate"
+ type="date"
+ value={startDate}
+ onChange={(e) => setStartDate(e.target.value)}
+ required
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="endDate">End Date</Label>
+ <Input
+ id="endDate"
+ type="date"
+ value={endDate}
+ onChange={(e) => setEndDate(e.target.value)}
+ required
+ />
+ </div>
+
+ {formError && (
+ <div className="text-sm text-red-500">
+ {formError}
+ </div>
+ )}
+
+ <div className="flex justify-end space-x-2">
+ <Button type="button" variant="outline" onClick={() => setIsAddDialogOpen(false)}>
+ Cancel
+ </Button>
+ <Button type="submit" disabled={createLoanMutation.isPending}>
+ {createLoanMutation.isPending ? 'Saving...' : 'Save Loan'}
+ </Button>
+ </div>
+ </form>
+ </DialogContent>
+ </Dialog>
+ </div>
+
+ {data && data.length > 0 ? (
+ <Card>
+ <CardHeader>
+ <CardTitle>Your Loans</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>Name</TableHead>
+ <TableHead>Original Amount</TableHead>
+ <TableHead>Current Balance</TableHead>
+ <TableHead>Interest Rate</TableHead>
+ <TableHead>Start Date</TableHead>
+ <TableHead>End Date</TableHead>
+ <TableHead>Actions</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {data.map((loan) => (
+ <TableRow key={loan.ID}>
+ <TableCell className="font-medium">{loan.Name}</TableCell>
+ <TableCell>{formatCurrency(loan.OriginalAmount)}</TableCell>
+ <TableCell>{formatCurrency(loan.CurrentBalance)}</TableCell>
+ <TableCell>{loan.InterestRate}%</TableCell>
+ <TableCell>{formatDate(loan.StartDate)}</TableCell>
+ <TableCell>{formatDate(loan.EndDate)}</TableCell>
+ <TableCell>
+ <div className="flex items-center space-x-2">
+ <Button
+ variant="destructive"
+ size="sm"
+ onClick={() => handleDeleteLoan(loan.ID)}
+ disabled={deleteLoanMutation.isPending}
+ >
+ Delete
+ </Button>
+ </div>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ </CardContent>
+ </Card>
+ ) : (
+ <Card>
+ <CardContent className="flex flex-col items-center justify-center p-10">
+ <p className="text-muted-foreground mb-4">You don&apos;t have any loans yet.</p>
+ <Button onClick={() => setIsAddDialogOpen(true)}>Add Your First Loan</Button>
+ </CardContent>
+ </Card>
+ )}
+ </div>
+ );
+} \ No newline at end of file
diff --git a/frontend/src/app/favicon.ico b/frontend/src/app/favicon.ico
new file mode 100644
index 0000000..718d6fe
--- /dev/null
+++ b/frontend/src/app/favicon.ico
Binary files differ
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
new file mode 100644
index 0000000..dc98be7
--- /dev/null
+++ b/frontend/src/app/globals.css
@@ -0,0 +1,122 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
new file mode 100644
index 0000000..f746389
--- /dev/null
+++ b/frontend/src/app/layout.tsx
@@ -0,0 +1,43 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "./globals.css";
+import { Providers } from "./providers";
+import { ThemeProvider } from "@/components/shared/ThemeProvider";
+import { NotificationProvider } from "@/components/shared/NotificationContext";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "Finance Management",
+ description: "Enhanced Personal Finance Management Application",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+ <html lang="en" suppressHydrationWarning>
+ <body
+ className={`${geistSans.variable} ${geistMono.variable} antialiased`}
+ >
+ <ThemeProvider>
+ <Providers>
+ <NotificationProvider>
+ {children}
+ </NotificationProvider>
+ </Providers>
+ </ThemeProvider>
+ </body>
+ </html>
+ );
+}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
new file mode 100644
index 0000000..b2f055e
--- /dev/null
+++ b/frontend/src/app/page.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import Image from "next/image";
+
+export default function Home() {
+ const router = useRouter();
+
+ useEffect(() => {
+ // Check if user is authenticated
+ const token = localStorage.getItem('token');
+ if (token) {
+ // If authenticated, redirect to dashboard
+ router.push('/dashboard');
+ } else {
+ // Otherwise, redirect to login
+ router.push('/login');
+ }
+ }, [router]);
+
+ return (
+ <div className="flex min-h-screen items-center justify-center">
+ <p>Redirecting...</p>
+ </div>
+ );
+}
diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx
new file mode 100644
index 0000000..efedc0b
--- /dev/null
+++ b/frontend/src/app/providers.tsx
@@ -0,0 +1,25 @@
+'use client';
+
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactNode, useState } from 'react';
+
+interface ProvidersProps {
+ children: ReactNode;
+}
+
+export function Providers({ children }: ProvidersProps) {
+ const [queryClient] = useState(() => new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ retry: 1,
+ },
+ },
+ }));
+
+ return (
+ <QueryClientProvider client={queryClient}>
+ {children}
+ </QueryClientProvider>
+ );
+} \ No newline at end of file