version 1

the reminder not showing content on the card at dashboard view and the quizes are hardcoded must move to the db make api's for it
This commit is contained in:
Mann Patel
2025-02-16 00:05:12 -07:00
parent c1728153d2
commit 807dd273fd
1130 changed files with 306733 additions and 0 deletions

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

8
frontend/README.md Normal file
View File

@@ -0,0 +1,8 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

38
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,38 @@
import js from '@eslint/js'
import globals from 'globals'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
settings: { react: { version: '18.3' } },
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

5887
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
frontend/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.48.1",
"@tailwindcss/vite": "^4.0.6",
"date-fns": "^4.1.0",
"lucide-react": "^0.475.0",
"plaid": "^31.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.5",
"recharts": "^2.15.1"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.19.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"postcss": "^8.5.2",
"tailwindcss": "^4.0.6",
"vite": "^6.1.0"
}
}

1
frontend/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

418
frontend/src/App.jsx Normal file
View File

@@ -0,0 +1,418 @@
import React, { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route, Link, Navigate, useNavigate } from 'react-router-dom';
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { Bell, DollarSign, Home, Calendar, ChevronRight, Building, CreditCard, MessageSquare, LogOut, FileText } from 'lucide-react';
import { supabase } from './supabaseClient';
import { Auth } from '@supabase/auth-ui-react';
import { ThemeSupa } from '@supabase/auth-ui-shared';
import ReminderWidget from './components/ReminderWidget';
import ReminderPage from './pages/ReminderPage'
import QuizPage from './pages/QuizPage';
import ExpenseTracker from './pages/ExpenceTracker';
import { PlaidProvider } from './PlaidProvider';
import { usePlaid } from './PlaidProvider'; // Import the usePlaid hook
// Error Boundary Component
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return (
<div className="p-4 text-red-500">
<h2>Something went wrong.</h2>
<button
onClick={() => window.location.reload()}
className="mt-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// Auth hook
const useAuth = () => {
const [session, setSession] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setLoading(false);
});
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
return () => subscription.unsubscribe();
}, []);
return {
session,
loading,
isAuthenticated: !!session,
};
};
// Login Component
const Login = () => {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="bg-white p-8 rounded-lg shadow-md w-96">
<h2 className="text-2xl font-bold mb-6 text-center">Welcome</h2>
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
providers={[]}
theme="default"
/>
</div>
</div>
);
};
const DashboardContent = () => {
const navigate = useNavigate();
const { accounts, transactions, loading } = usePlaid(); // Use the Plaid context
const [reminders, setReminders] = useState([]);
// useEffect(() => {
// const fetchReminders = async () => {
// const { data, error } = await supabase
// .from('reminders')
// .select('*')
// .order('due_date', { ascending: true });
// if (data) setReminders(data);
// };
// fetchReminders();
// }, []);
// Process Plaid data for charts
const processChartData = () => {
if (!transactions?.length) return [];
// Group transactions by month
const monthlyData = transactions.reduce((acc, transaction) => {
const month = transaction.date.substring(0, 7); // Get YYYY-MM
if (!acc[month]) {
acc[month] = { month, income: 0, spending: 0, savings: 0 };
}
// Amount > 0 is spending, < 0 is income in Plaid
if (transaction.amount > 0) {
acc[month].spending += transaction.amount;
} else {
acc[month].income += Math.abs(transaction.amount);
}
// Calculate savings
acc[month].savings = acc[month].income - acc[month].spending;
return acc;
}, {});
// Convert to array and sort by month
return Object.values(monthlyData).sort((a, b) => a.month.localeCompare(b.month))
.map(data => ({
...data,
month: new Date(data.month + '-01').toLocaleDateString('en-US', { month: 'short' })
}));
};
const chartData = processChartData();
// Show loading state if Plaid data is still loading
if (loading) {
return (
<div className="w-full h-64 flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
</div>
);
}
return (
<div className="w-full space-y-6">
{/* Financial Overview Graphs */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 max-w-7xl mx-auto">
{/* Income Graph */}
<div className="bg-white rounded-lg shadow p-4">
<div className="mb-4">
<h3 className="text-lg font-semibold">Income</h3>
</div>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis tickFormatter={(value) => `$${value.toLocaleString()}`} />
<Tooltip
formatter={(value) => [`$${value.toLocaleString()}`, 'Income']}
labelFormatter={(label) => `Month: ${label}`}
/>
<Line
type="monotone"
dataKey="income"
stroke="#4CAF50"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* Spending Graph */}
<div className="bg-white rounded-lg shadow p-4">
<div className="mb-4">
<h3 className="text-lg font-semibold">Spending</h3>
</div>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis tickFormatter={(value) => `$${value.toLocaleString()}`} />
<Tooltip
formatter={(value) => [`$${value.toLocaleString()}`, 'Spending']}
labelFormatter={(label) => `Month: ${label}`}
/>
<Bar dataKey="spending" fill="#FF5722" />
</BarChart>
</ResponsiveContainer>
</div>
{/* Savings Graph */}
<div className="bg-white rounded-lg shadow p-4">
<div className="mb-4">
<h3 className="text-lg font-semibold">Savings</h3>
</div>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis tickFormatter={(value) => `$${value.toLocaleString()}`} />
<Tooltip
formatter={(value) => [`$${value.toLocaleString()}`, 'Savings']}
labelFormatter={(label) => `Month: ${label}`}
/>
<Line
type="monotone"
dataKey="savings"
stroke="#2196F3"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
{/* Navigation Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-8">
<ReminderWidget/>
{/* Apartment Finder Card */}
{/* Expense Tracker Card */}
<div
onClick={() => navigate('/expenses')}
className="bg-white rounded-lg shadow transition-all hover:shadow-lg cursor-pointer p-6"
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="bg-green-100 p-3 rounded-lg">
<DollarSign className="h-6 w-6 text-green-600" />
</div>
<h3 className="text-lg font-semibold">Track Expenses</h3>
</div>
<ChevronRight className="h-5 w-5 text-gray-400" />
</div>
<p className="text-gray-600">Monitor your spending and manage your budget</p>
</div>
{/* Quizzes Card */}
<div
onClick={() => navigate('/quizzes')}
className="bg-white rounded-lg shadow transition-all hover:shadow-lg cursor-pointer p-6"
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="bg-orange-100 p-3 rounded-lg">
<FileText className="h-6 w-6 text-orange-600" />
</div>
<h3 className="text-lg font-semibold">Financial Quizzes</h3>
</div>
<ChevronRight className="h-5 w-5 text-gray-400" />
</div>
<p className="text-gray-600">Test your financial knowledge and learn new skills</p>
</div>
</div>
<div>
<footer className="bg-gray-100 mt-12 py-8">
<div className="max-w-7xl mx-auto px-4 grid grid-cols-1 md:grid-cols-4 gap-6">
{/* Company Info Column */}
<div>
<h4 className="text-lg font-semibold mb-4">FinTrack</h4>
<p className="text-gray-600 text-sm">
Your personal financial management and tracking solution.
</p>
</div>
</div>
</footer>
</div></div>
);
};
// Layout Component with Navigation Context
const DashboardLayout = ({ children }) => {
const navigate = useNavigate();
const [userName, setUserName] = useState('');
const handleLogout = async () => {
await supabase.auth.signOut();
navigate('/login');
};
useEffect(() => {
const getUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (user?.email) {
const name = user.email.split('@')[0];
setUserName(name.charAt(0).toUpperCase() + name.slice(1));
}
};
getUser();
}, []);
return (
<div className="min-h-screen bg-gray-100">
{/* Fixed Navbar */}
<div className="fixed top-0 left-0 right-0 bg-white shadow z-10">
<div className="max-w-7xl mx-auto px-4">
<div className="flex justify-between items-center h-16">
{/* Logo and Brand */}
<div className="flex items-center">
<DollarSign className="h-8 w-8 text-green-600" />
<span className="ml-2 text-xl font-bold text-gray-900"></span>
</div>
{/* User Info and Logout */}
<div className="flex items-center gap-6">
<span className="text-gray-600">Hi, {userName}</span>
<button
onClick={handleLogout}
className="px-4 py-2 text-red-500 hover:text-red-600 flex items-center gap-2"
>
<LogOut size={20} />
<span>Logout</span>
</button>
</div>
</div>
</div>
</div>
{/* Main Content with top padding for navbar */}
<div className="max-w-7xl mx-auto px-4 pt-20">
<ErrorBoundary>
{children}
</ErrorBoundary>
</div>
</div>
);
};
// Protected Route Component
const ProtectedRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
</div>
);
}
return isAuthenticated ? children : <Navigate to="/login" />;
};
// Main App Component
const App = () => {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900">
</div>
</div>
);
}
return (
<BrowserRouter>
<ErrorBoundary>
<PlaidProvider>
<Routes>
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/dashboard" /> : <Login />}
/>
<Route path="/" element={<Navigate to="/dashboard" />} />
<Route
path="/reminders"
element={
<ProtectedRoute>
<ReminderPage />
</ProtectedRoute>
}/>
<Route
path="/expenses"
element={
<ProtectedRoute>
<ExpenseTracker />
</ProtectedRoute>
}/>
<Route
path="/quizzes"
element={
<ProtectedRoute>
<QuizPage />
</ProtectedRoute>
}/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardLayout>
<DashboardContent />
</DashboardLayout>
</ProtectedRoute>
}
/>
{/* Other routes */}
</Routes>
</PlaidProvider>
</ErrorBoundary>
</BrowserRouter>
);
};
export default App;

View File

@@ -0,0 +1,151 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { supabase } from './supabaseClient';
const PlaidContext = createContext();
export const PlaidProvider = ({ children }) => {
const [linkToken, setLinkToken] = useState(null);
const [accessToken, setAccessToken] = useState(null);
const [accounts, setAccounts] = useState([]);
const [transactions, setTransactions] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Check for existing access token on mount
useEffect(() => {
checkExistingConnection();
}, []);
const checkExistingConnection = async () => {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) return;
// Check if user has an access token in Supabase
const { data: plaidData, error } = await supabase
.from('plaid_connections')
.select('access_token')
.eq('user_id', user.id)
.single();
if (plaidData?.access_token) {
setAccessToken(plaidData.access_token);
await fetchTransactions(plaidData.access_token);
}
setLoading(false);
} catch (err) {
console.error('Error checking existing connection:', err);
setLoading(false);
}
};
const initializePlaidLink = async () => {
try {
setLoading(true);
setError(null);
// Get the current user
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('No user logged in');
// Create link token
const response = await fetch('http://localhost:3000/api/create_link_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id })
});
const data = await response.json();
setLinkToken(data.link_token);
// Load Plaid script
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
script.async = true;
script.onload = () => {
const handler = window.Plaid.create({
token: data.link_token,
onSuccess: async (public_token, metadata) => {
try {
const response = await fetch('http://localhost:3000/api/exchange_public_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ public_token })
});
const data = await response.json();
// Save access token to Supabase
await supabase
.from('plaid_connections')
.upsert({
user_id: user.id,
access_token: data.access_token,
item_id: data.item_id
});
setAccessToken(data.access_token);
await fetchTransactions(data.access_token);
resolve();
} catch (err) {
setError('Failed to connect to bank');
reject(err);
}
},
onExit: (err, metadata) => {
if (err) setError('Connection process interrupted');
setLoading(false);
reject(err);
},
});
handler.open();
};
document.head.appendChild(script);
});
} catch (err) {
setError('Failed to initialize bank connection');
setLoading(false);
throw err;
}
};
const fetchTransactions = async (token) => {
try {
setLoading(true);
const response = await fetch('http://localhost:3000/api/transactions', {
headers: { 'plaid-access-token': token || accessToken }
});
const data = await response.json();
setTransactions(data.transactions);
setAccounts(data.accounts || []);
setLoading(false);
} catch (err) {
setError('Failed to fetch transactions');
setLoading(false);
}
};
const value = {
linkToken,
accessToken,
accounts,
transactions,
loading,
error,
initializePlaidLink,
fetchTransactions,
};
return (
<PlaidContext.Provider value={value}>
{children}
</PlaidContext.Provider>
);
};
export const usePlaid = () => {
const context = useContext(PlaidContext);
if (context === undefined) {
throw new Error('usePlaid must be used within a PlaidProvider');
}
return context;
};

View File

@@ -0,0 +1,81 @@
// ReminderWidget.jsx
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Bell, ChevronRight } from 'lucide-react';
import { supabase } from '../supabaseClient';
const ReminderWidget = () => {
const navigate = useNavigate();
const [reminders, setReminders] = useState([]);
useEffect(() => {
fetchReminders();
}, []);
const fetchReminders = async () => {
const today = new Date();
const thirtyDaysFromNow = new Date();
thirtyDaysFromNow.setDate(today.getDate() + 30);
const { data, error } = await supabase
.from('reminders')
.select('*')
.gte('due_date', today.toISOString().split('T')[0])
.lte('due_date', thirtyDaysFromNow.toISOString().split('T')[0])
.order('due_date', { ascending: true })
.limit(5);
if (data) setReminders(data);
};
const totalUpcoming = reminders.reduce((sum, reminder) => sum + parseFloat(reminder.amount), 0);
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Bell className="h-5 w-5 text-purple-500" />
<h3 className="text-lg font-semibold">Upcoming Payments</h3>
</div>
<div className="text-lg font-bold text-purple-600">
${totalUpcoming.toFixed(2)}
</div>
</div>
<div className="space-y-4">
{reminders.map((reminder) => (
<div
key={reminder.id}
className="flex items-center justify-between p-3 bg-purple-50 rounded-lg cursor-pointer hover:bg-purple-100"
onClick={() => navigate('/reminders')}
>
<div>
<div className="font-medium">{reminder.title}</div>
<div className="text-sm text-gray-600">
Due: {new Date(reminder.due_date).toLocaleDateString()}
</div>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">${parseFloat(reminder.amount).toFixed(2)}</span>
<ChevronRight className="h-4 w-4 text-gray-400" />
</div>
</div>
))}
{reminders.length === 0 && (
<div className="text-center text-gray-500 py-4">
No upcoming payments
</div>
)}
<button
onClick={() => navigate('/reminders')}
className="w-full mt-4 text-purple-600 hover:text-purple-700 font-medium flex items-center justify-center gap-2"
>
View All Reminders
<ChevronRight className="h-4 w-4" />
</button>
</div>
</div>
);
};
export default ReminderWidget;

5
frontend/src/index.css Normal file
View File

@@ -0,0 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";

10
frontend/src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,204 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { usePlaid } from '../PlaidProvider';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { DollarSign, ChevronLeft } from 'lucide-react';
const ExpenseTracker = () => {
const navigate = useNavigate();
const {
accessToken,
accounts,
transactions,
loading,
error,
initializePlaidLink,
fetchTransactions
} = usePlaid();
// Process transactions for chart data
const processTransactionsForChart = (transactions) => {
const sortedTransactions = [...transactions].sort((a, b) =>
new Date(a.date) - new Date(b.date)
).map(transaction => ({
date: new Date(transaction.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
}),
amount: transaction.amount,
name: transaction.merchant_name || transaction.name,
category: transaction.category ? transaction.category[0] : 'Uncategorized'
}));
return sortedTransactions;
};
const chartData = processTransactionsForChart(transactions);
const CustomTooltip = ({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-white p-4 shadow-lg rounded border">
<p className="text-sm text-gray-600">{data.date}</p>
<p className="font-bold">{data.name}</p>
<p className={`font-medium ${data.amount > 0 ? 'text-red-600' : 'text-green-600'}`}>
${Math.abs(data.amount).toFixed(2)}
</p>
<p className="text-sm text-gray-500">{data.category}</p>
</div>
);
}
return null;
};
if (loading) {
return (
<div className="flex flex-col items-center justify-center h-64 space-y-4">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<p className="text-gray-600">Loading your financial data...</p>
</div>
);
}
if (!accessToken) {
return (
<div className="flex flex-col items-center justify-center h-64 space-y-4">
<h2 className="text-2xl font-bold">Connect Your Bank Account</h2>
<button
onClick={initializePlaidLink}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Connect Account
</button>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation Bar */}
<div className="fixed top-0 left-0 right-0 bg-white shadow z-10">
<div className="max-w-7xl mx-auto px-4">
<div className="flex items-center h-16">
<button
onClick={() => navigate('/dashboard')}
className="p-2 hover:bg-gray-100 rounded-lg flex items-center gap-2 text-gray-600"
>
<ChevronLeft size={20} />
<span>Back</span>
</button>
<div className="flex items-center ml-4">
<DollarSign className="h-6 w-6 text-green-600" />
<span className="ml-2 text-xl font-bold text-gray-900">Expenses</span>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="container mx-auto px-4 pt-20">
{/* Account Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{accounts.map(account => (
<div key={account.account_id} className="bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold">{account.name}</h3>
<p className="text-gray-600">{account.subtype}</p>
<p className="text-xl mt-2">
${account.balances.current.toFixed(2)}
{account.balances.available && (
<span className="text-sm text-gray-500 ml-2">
(${account.balances.available.toFixed(2)} available)
</span>
)}
</p>
</div>
))}
</div>
{/* Transaction Chart */}
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h3 className="text-xl font-semibold mb-4">Transaction History</h3>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData} margin={{ bottom: 20 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="date"
tick={{ angle: -45 }}
textAnchor="end"
height={60}
interval="preserveStartEnd"
/>
<YAxis
tickFormatter={(value) => `$${Math.abs(value).toLocaleString()}`}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="amount"
stroke="#8884d8"
dot={{
stroke: (entry) => entry.amount > 0 ? '#ef4444' : '#22c55e',
fill: (entry) => entry.amount > 0 ? '#ef4444' : '#22c55e',
r: 4
}}
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* Transactions Table */}
<div className="bg-white rounded-lg shadow overflow-hidden mb-6">
<div className="flex justify-between items-center p-6 border-b">
<h3 className="text-xl font-semibold">Recent Transactions</h3>
<button
onClick={fetchTransactions}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Refresh
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Category</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Amount</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{transactions.map(transaction => (
<tr key={transaction.transaction_id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(transaction.date).toLocaleDateString()}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<p className="text-sm font-medium text-gray-900">
{transaction.merchant_name || transaction.name}
</p>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{transaction.category ? transaction.category[0] : 'Uncategorized'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<span className={`text-sm font-medium ${
transaction.amount > 0 ? 'text-red-600' : 'text-green-600'
}`}>
${Math.abs(transaction.amount).toFixed(2)}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
};
export default ExpenseTracker;

View File

@@ -0,0 +1,379 @@
// QuizPage.jsx
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { ChevronLeft, DollarSign, BookOpen, Award, Check, X } from 'lucide-react';
import { supabase } from '../supabaseClient';
const QuizPage = () => {
const navigate = useNavigate();
const [currentQuiz, setCurrentQuiz] = useState(null);
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [selectedAnswer, setSelectedAnswer] = useState(null);
const [score, setScore] = useState(0);
const [showResult, setShowResult] = useState(false);
const [quizStarted, setQuizStarted] = useState(false);
// Sample quiz data - Replace with Supabase data
const quizzes = [
{
id: 1,
title: "Banking & Finance Essentials",
description: "Understand the key concepts of personal finance and banking.",
questions: [
{
question: "What is compound interest?",
options: [
"Interest earned on the initial deposit only",
"Interest earned on both the initial deposit and previously earned interest",
"A fee charged by banks for loans",
"A one-time interest payment on savings"
],
correctAnswer: 1,
explanation: "Compound interest is calculated on both the principal amount and accumulated interest over time."
},
{
question: "Which of the following negatively impacts your credit score?",
options: [
"Paying credit card bills on time",
"Having multiple credit cards",
"Missing loan payments",
"Checking your credit score"
],
correctAnswer: 2,
explanation: "Missing loan or credit payments lowers your credit score as it reflects poor financial responsibility."
},
{
question: "What is the primary purpose of a credit score?",
options: [
"To determine your annual tax rate",
"To assess your trustworthiness as a borrower",
"To calculate your net worth",
"To set your bank account interest rate"
],
correctAnswer: 1,
explanation: "A credit score helps lenders assess your creditworthiness before approving loans or credit cards."
},
{
question: "Which financial tool helps you save money on unnecessary expenses?",
options: [
"A budgeting app",
"A credit card",
"A payday loan",
"A lottery ticket"
],
correctAnswer: 0,
explanation: "Budgeting apps help track expenses and reduce unnecessary spending."
},
{
question: "What is an overdraft fee?",
options: [
"A fee for depositing money in your bank",
"A penalty for withdrawing more money than you have in your account",
"A charge for opening a new bank account",
"A bonus for using your debit card frequently"
],
correctAnswer: 1,
explanation: "An overdraft fee is charged when you spend more than what is available in your account."
},
{
question: "What does APR stand for?",
options: [
"Annual Percentage Rate",
"Average Payment Ratio",
"Automated Payment Record",
"Account Processing Report"
],
correctAnswer: 0,
explanation: "APR (Annual Percentage Rate) represents the cost of borrowing, including interest and fees."
}
]
},
{
id: 2,
title: "Insurance & Healthcare Knowledge",
description: "Learn about insurance and healthcare essentials.",
questions: [
{
question: "Which type of insurance helps cover the cost of medical expenses?",
options: [
"Home insurance",
"Health insurance",
"Auto insurance",
"Travel insurance"
],
correctAnswer: 1,
explanation: "Health insurance covers medical expenses, including doctor visits, hospital stays, and medications."
},
{
question: "What is a deductible in an insurance policy?",
options: [
"A discount offered for early payments",
"The amount you pay out-of-pocket before insurance kicks in",
"The total coverage amount of an insurance policy",
"The fine print in an insurance contract"
],
correctAnswer: 1,
explanation: "A deductible is the amount you must pay before your insurance starts covering expenses."
},
{
question: "What does premium mean in an insurance policy?",
options: [
"The total amount covered by the policy",
"The amount you pay for insurance coverage, usually monthly or yearly",
"A special discount given to policyholders",
"A type of investment plan"
],
correctAnswer: 1,
explanation: "A premium is the regular payment you make to keep your insurance policy active."
},
{
question: "Which of the following is NOT typically covered by standard health insurance?",
options: [
"Doctor visits",
"Prescription medication",
"Cosmetic surgery",
"Emergency room visits"
],
correctAnswer: 2,
explanation: "Cosmetic surgery is usually not covered unless it is medically necessary."
},
{
question: "What does life insurance primarily provide?",
options: [
"Coverage for medical expenses",
"Financial support for dependents after the policyholders death",
"Investment opportunities",
"Legal protection against lawsuits"
],
correctAnswer: 1,
explanation: "Life insurance provides financial security to the policyholders beneficiaries in the event of death."
}
]
},
{
id: 3,
title: "Food Safety & Consumer Awareness",
description: "Stay informed about food safety and smart consumer choices.",
questions: [
{
question: "Which of the following should you NOT do to prevent food poisoning?",
options: [
"Wash your hands before preparing food",
"Leave perishable food at room temperature for hours",
"Store raw meat separately from cooked food",
"Cook meat to the proper internal temperature"
],
correctAnswer: 1,
explanation: "Leaving perishable food at room temperature can lead to bacterial growth and foodborne illnesses."
},
{
question: "What does the expiration date on food packaging indicate?",
options: [
"The last day the food should be sold",
"The date after which the food is unsafe to eat",
"The best quality before a certain date, but it may still be safe after",
"A suggestion for when to buy new food"
],
correctAnswer: 2,
explanation: "Expiration dates often indicate peak freshness, but many foods are still safe to consume after this date."
},
{
question: "Which of the following is the safest way to thaw frozen food?",
options: [
"Leaving it on the kitchen counter",
"Placing it in the refrigerator overnight",
"Soaking it in hot water",
"Leaving it under direct sunlight"
],
correctAnswer: 1,
explanation: "Refrigerator thawing is the safest method as it prevents bacterial growth."
},
{
question: "What temperature should poultry be cooked to in order to ensure food safety?",
options: [
"120°F (49°C)",
"145°F (63°C)",
"165°F (74°C)",
"200°F (93°C)"
],
correctAnswer: 2,
explanation: "Poultry should be cooked to at least 165°F (74°C) to kill harmful bacteria."
},
{
question: "Which of these common kitchen habits increases the risk of cross-contamination?",
options: [
"Using separate cutting boards for meat and vegetables",
"Washing hands after handling raw food",
"Using the same knife for raw chicken and fresh vegetables without washing it",
"Refrigerating leftovers within two hours"
],
correctAnswer: 2,
explanation: "Using the same knife for raw meat and vegetables without washing it can transfer harmful bacteria."
}
]
}
];
const handleStartQuiz = () => {
setCurrentQuiz(quizzes[0]);
setQuizStarted(true);
setCurrentQuestionIndex(0);
setScore(0);
setShowResult(false);
};
const handleAnswerSelect = (answerIndex) => {
setSelectedAnswer(answerIndex);
};
const handleNextQuestion = () => {
// Calculate score
if (selectedAnswer === currentQuiz.questions[currentQuestionIndex].correctAnswer) {
setScore(score + 1);
}
// Move to next question or show results
if (currentQuestionIndex + 1 < currentQuiz.questions.length) {
setCurrentQuestionIndex(currentQuestionIndex + 1);
setSelectedAnswer(null);
} else {
setShowResult(true);
}
};
return (
<div className="min-h-screen bg-gray-100">
{/* Navigation Bar */}
<div className="fixed top-0 left-0 right-0 bg-white shadow z-10">
<div className="max-w-7xl mx-auto px-4">
<div className="flex items-center h-16">
<button
onClick={() => navigate('/dashboard')}
className="p-2 hover:bg-gray-100 rounded-lg flex items-center gap-2 text-gray-600"
>
<ChevronLeft size={20} />
<span>Back</span>
</button>
<div className="flex items-center ml-4">
<BookOpen className="h-6 w-6 text-purple-600" />
<span className="ml-2 text-xl font-bold text-gray-900">Financial Education</span>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-4xl mx-auto px-4 pt-20">
{!quizStarted ? (
// Quiz Selection Screen
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold mb-2">Financial Independence Quiz</h1>
<p className="text-gray-600">Test your knowledge and learn about personal finance</p>
</div>
<div className="space-y-6">
<div className="flex items-start gap-4 p-4 bg-purple-50 rounded-lg">
<div className="bg-purple-100 p-3 rounded-lg">
<Award className="h-6 w-6 text-purple-600" />
</div>
<div>
<h3 className="font-semibold">Budgeting Basics</h3>
<p className="text-sm text-gray-600 mt-1">Learn the fundamentals of personal budgeting and financial planning</p>
</div>
</div>
<button
onClick={handleStartQuiz}
className="w-full bg-purple-600 text-white rounded-lg py-3 font-medium hover:bg-purple-700 transition-colors"
>
Start Quiz
</button>
</div>
</div>
) : showResult ? (
// Results Screen
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="text-center">
<div className="mb-4">
{score === currentQuiz.questions.length ? (
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<Award className="h-10 w-10 text-green-600" />
</div>
) : (
<div className="w-20 h-20 bg-purple-100 rounded-full flex items-center justify-center mx-auto">
<BookOpen className="h-10 w-10 text-purple-600" />
</div>
)}
</div>
<h2 className="text-2xl font-bold mb-2">Quiz Completed!</h2>
<p className="text-gray-600 mb-6">
You scored {score} out of {currentQuiz.questions.length}
</p>
<button
onClick={() => setQuizStarted(false)}
className="bg-purple-600 text-white rounded-lg px-6 py-2 font-medium hover:bg-purple-700 transition-colors"
>
Try Another Quiz
</button>
</div>
</div>
) : (
// Quiz Questions Screen
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="mb-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">{currentQuiz.title}</h2>
<span className="text-sm text-gray-600">
Question {currentQuestionIndex + 1} of {currentQuiz.questions.length}
</span>
</div>
<div className="h-2 bg-gray-200 rounded-full">
<div
className="h-full bg-purple-600 rounded-full transition-all duration-300"
style={{ width: `${((currentQuestionIndex + 1) / currentQuiz.questions.length) * 100}%` }}
></div>
</div>
</div>
<div className="space-y-6">
<h3 className="text-lg font-medium">
{currentQuiz.questions[currentQuestionIndex].question}
</h3>
<div className="space-y-3">
{currentQuiz.questions[currentQuestionIndex].options.map((option, index) => (
<button
key={index}
onClick={() => handleAnswerSelect(index)}
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
selectedAnswer === index
? 'border-purple-600 bg-purple-50'
: 'border-gray-200 hover:border-purple-300'
}`}
>
{option}
</button>
))}
</div>
<button
onClick={handleNextQuestion}
disabled={selectedAnswer === null}
className={`w-full py-3 rounded-lg font-medium transition-colors ${
selectedAnswer === null
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: 'bg-purple-600 text-white hover:bg-purple-700'
}`}
>
{currentQuestionIndex + 1 === currentQuiz.questions.length ? 'Finish Quiz' : 'Next Question'}
</button>
</div>
</div>
)}
</div>
</div>
);
};
export default QuizPage;

View File

@@ -0,0 +1,392 @@
// ReminderPage.jsx
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { DollarSign, Plus, X, Calendar, List, Pencil, Trash2, ChevronLeft } from 'lucide-react';
import { supabase } from '../supabaseClient';
const ReminderPage = () => {
const navigate = useNavigate();
const [reminders, setReminders] = useState([]);
const [showModal, setShowModal] = useState(false);
const [viewMode, setViewMode] = useState('list');
const [selectedDate, setSelectedDate] = useState(new Date());
const [editingReminder, setEditingReminder] = useState(null);
const [formData, setFormData] = useState({
title: '',
due_date: '',
amount: '',
description: '',
category: 'bill'
});
useEffect(() => {
fetchReminders();
}, []);
const fetchReminders = async () => {
const { data, error } = await supabase
.from('reminders')
.select('*')
.order('due_date', { ascending: true });
if (data) setReminders(data);
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (editingReminder) {
const { error } = await supabase
.from('reminders')
.update({
title: formData.title,
due_date: formData.due_date,
amount: parseFloat(formData.amount),
description: formData.description,
category: formData.category
})
.eq('id', editingReminder.id);
if (error) throw error;
setEditingReminder(null);
} else {
const { error } = await supabase
.from('reminders')
.insert([{
title: formData.title,
due_date: formData.due_date,
amount: parseFloat(formData.amount),
description: formData.description,
category: formData.category
}]);
if (error) throw error;
}
await fetchReminders();
setShowModal(false);
resetForm();
} catch (error) {
console.error('Error:', error);
}
};
const handleEdit = (reminder) => {
setEditingReminder(reminder);
setFormData({
title: reminder.title,
due_date: reminder.due_date.split('T')[0],
amount: reminder.amount,
description: reminder.description || '',
category: reminder.category || 'bill'
});
setShowModal(true);
};
const handleDelete = async (id) => {
const { error } = await supabase
.from('reminders')
.delete()
.eq('id', id);
if (!error) {
await fetchReminders();
}
};
const resetForm = () => {
setFormData({
title: '',
due_date: '',
amount: '',
description: '',
category: 'bill'
});
setEditingReminder(null);
};
const getDaysInMonth = (date) => {
const year = date.getFullYear();
const month = date.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstDayOfMonth = new Date(year, month, 1).getDay();
return { daysInMonth, firstDayOfMonth };
};
const getRemindersForDate = (date) => {
return reminders.filter(reminder => {
const reminderDate = new Date(reminder.due_date);
return reminderDate.toDateString() === date.toDateString();
});
};
return (
<div className="min-h-screen bg-gray-100">
{/* Navigation Bar */}
<div className="fixed top-0 left-0 right-0 bg-white shadow z-10">
<div className="max-w-7xl mx-auto px-4">
<div className="flex items-center h-16">
<button
onClick={() => navigate('/dashboard')}
className="p-2 hover:bg-gray-100 rounded-lg flex items-center gap-2 text-gray-600"
>
<ChevronLeft size={20} />
<span>Back</span>
</button>
<div className="flex items-center ml-4">
<DollarSign className="h-6 w-6 text-green-600" />
<span className="ml-2 text-xl font-bold text-gray-900">Money</span>
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="max-w-7xl mx-auto px-4 pt-20">
{/* Header */}
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Payment Reminders</h1>
<div className="flex gap-4">
<button
onClick={() => setViewMode(viewMode === 'list' ? 'calendar' : 'list')}
className="px-4 py-2 bg-white rounded-lg shadow flex items-center gap-2"
>
{viewMode === 'list' ? <Calendar size={20} /> : <List size={20} />}
{viewMode === 'list' ? 'Calendar View' : 'List View'}
</button>
<button
onClick={() => {
resetForm();
setShowModal(true);
}}
className="px-4 py-2 bg-purple-600 text-white rounded-lg shadow flex items-center gap-2"
>
<Plus size={20} />
Add Reminder
</button>
</div>
</div>
{/* List View */}
{viewMode === 'list' && (
<div className="bg-white rounded-lg shadow">
{reminders.map((reminder) => (
<div
key={reminder.id}
className="flex items-center justify-between p-4 border-b last:border-b-0"
>
<div className="flex-1">
<div className="flex items-center gap-2">
<span
className={`px-2 py-1 rounded text-sm ${
reminder.category === 'bill' ? 'bg-blue-100 text-blue-700' :
reminder.category === 'subscription' ? 'bg-green-100 text-green-700' :
'bg-gray-100 text-gray-700'
}`}
>
{reminder.category}
</span>
<h3 className="font-semibold">{reminder.title}</h3>
</div>
<p className="text-sm text-gray-600">
Due: {new Date(reminder.due_date).toLocaleDateString()}
</p>
{reminder.description && (
<p className="text-sm text-gray-500 mt-1">{reminder.description}</p>
)}
</div>
<div className="flex items-center gap-4">
<span className="font-semibold">${parseFloat(reminder.amount).toFixed(2)}</span>
<button
onClick={() => handleEdit(reminder)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-full"
>
<Pencil size={20} />
</button>
<button
onClick={() => handleDelete(reminder.id)}
className="p-2 text-red-600 hover:bg-red-50 rounded-full"
>
<Trash2 size={20} />
</button>
</div>
</div>
))}
{reminders.length === 0 && (
<div className="p-8 text-center text-gray-500">
No reminders yet. Add one to get started!
</div>
)}
</div>
)}
{/* Calendar View */}
{viewMode === 'calendar' && (
<div className="bg-white rounded-lg shadow p-4">
<div className="grid grid-cols-7 gap-2">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
<div key={day} className="text-center font-semibold py-2">
{day}
</div>
))}
{(() => {
const { daysInMonth, firstDayOfMonth } = getDaysInMonth(selectedDate);
const days = [];
for (let i = 0; i < firstDayOfMonth; i++) {
days.push(
<div key={`empty-${i}`} className="p-2 min-h-[100px]" />
);
}
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), day);
const dayReminders = getRemindersForDate(date);
days.push(
<div
key={day}
className={`p-2 border rounded-lg min-h-[100px] ${
date.toDateString() === new Date().toDateString()
? 'bg-purple-50 border-purple-200'
: 'border-gray-200'
}`}
>
<div className="font-semibold mb-2">{day}</div>
<div className="space-y-1">
{dayReminders.map(reminder => (
<div
key={reminder.id}
className="text-sm p-1 rounded bg-purple-100 text-purple-700 cursor-pointer"
onClick={() => handleEdit(reminder)}
>
{reminder.title} - ${parseFloat(reminder.amount).toFixed(2)}
</div>
))}
</div>
</div>
);
}
return days;
})()}
</div>
</div>
)}
{/* Add/Edit Modal */}
{showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-md w-full p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">
{editingReminder ? 'Edit Reminder' : 'New Reminder'}
</h2>
<button
onClick={() => {
setShowModal(false);
resetForm();
}}
className="text-gray-500 hover:text-gray-700"
>
<X size={24} />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Title
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full p-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Category
</label>
<select
value={formData.category}
onChange={(e) => setFormData({ ...formData, category: e.target.value })}
className="w-full p-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="bill">Bill</option>
<option value="subscription">Subscription</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Due Date
</label>
<input
type="date"
value={formData.due_date}
onChange={(e) => setFormData({ ...formData, due_date: e.target.value })}
className="w-full p-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Amount
</label>
<input
type="number"
step="0.01"
value={formData.amount}
onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
className="w-full p-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description (Optional)
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full p-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
rows="3"
/>
</div>
<div className="flex justify-end gap-4 mt-6">
<button
type="button"
onClick={() => {
setShowModal(false);
resetForm();
}}
className="px-4 py-2 text-gray-600 hover:text-gray-700"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
{editingReminder ? 'Save Changes' : 'Add Reminder'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
</div>
);
};
export default ReminderPage;

View File

@@ -0,0 +1,6 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = 'https://qtjqzwqaoribjbqapybe.supabase.co';
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InF0anF6d3Fhb3JpYmpicWFweWJlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Mzk2NDc3MTEsImV4cCI6MjA1NTIyMzcxMX0.UBSC6ylMp2x8dI_-aAo6u_-uhIq5rAVwUalDD-ykYH0';
export const supabase = createClient(supabaseUrl, supabaseAnonKey);

10
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [tailwindcss(),
react()],
})