diff --git a/7project/frontend/src/App.tsx b/7project/frontend/src/App.tsx index 93626c2..3f3cec6 100644 --- a/7project/frontend/src/App.tsx +++ b/7project/frontend/src/App.tsx @@ -8,6 +8,18 @@ function App() { const [hasToken, setHasToken] = useState(!!localStorage.getItem('token')); useEffect(() => { + // Handle OAuth callback: /oauth-callback?access_token=...&token_type=... + if (window.location.pathname === '/oauth-callback') { + const params = new URLSearchParams(window.location.search); + const token = params.get('access_token'); + if (token) { + localStorage.setItem('token', token); + setHasToken(true); + } + // Clean URL and redirect to home + window.history.replaceState({}, '', '/'); + } + const onStorage = (e: StorageEvent) => { if (e.key === 'token') setHasToken(!!e.newValue); }; diff --git a/7project/frontend/src/api.ts b/7project/frontend/src/api.ts index 95933d4..2b7a2cb 100644 --- a/7project/frontend/src/api.ts +++ b/7project/frontend/src/api.ts @@ -95,6 +95,49 @@ export async function getTransactions(): Promise { return res.json(); } +export type User = { + id: string; + email: string; + first_name?: string | null; + last_name?: string | null; + is_active: boolean; + is_superuser: boolean; + is_verified: boolean; +}; + +export async function getMe(): Promise { + const res = await fetch(`${getBaseUrl()}/users/me`, { + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + }); + if (!res.ok) throw new Error('Failed to load user'); + return res.json(); +} + +export type UpdateMeInput = Partial> & { password?: string }; +export async function updateMe(input: UpdateMeInput): Promise { + const res = await fetch(`${getBaseUrl()}/users/me`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + body: JSON.stringify(input), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || 'Failed to update user'); + } + return res.json(); +} + +export async function deleteMe(): Promise { + const res = await fetch(`${getBaseUrl()}/users/me`, { + method: 'DELETE', + headers: { ...authHeaders() }, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || 'Failed to delete account'); + } +} + export function logout() { localStorage.removeItem('token'); } diff --git a/7project/frontend/src/main.tsx b/7project/frontend/src/main.tsx index d8e0921..9d33e35 100644 --- a/7project/frontend/src/main.tsx +++ b/7project/frontend/src/main.tsx @@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client' import './index.css' import './ui.css' import App from './App.tsx' +import { applyAppearanceFromStorage } from './appearance' + +applyAppearanceFromStorage() createRoot(document.getElementById('root')!).render( diff --git a/7project/frontend/src/pages/Dashboard.tsx b/7project/frontend/src/pages/Dashboard.tsx index 2c6b05f..4f78f76 100644 --- a/7project/frontend/src/pages/Dashboard.tsx +++ b/7project/frontend/src/pages/Dashboard.tsx @@ -1,11 +1,14 @@ import { useEffect, useMemo, useState } from 'react'; import { type Category, type Transaction, createTransaction, getCategories, getTransactions } from '../api'; +import AccountPage from './AccountPage'; +import AppearancePage from './AppearancePage'; function formatAmount(n: number) { return new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n); } export default function Dashboard({ onLogout }: { onLogout: () => void }) { + const [current, setCurrent] = useState<'home' | 'account' | 'appearance'>('home'); const [transactions, setTransactions] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); @@ -78,77 +81,90 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
-

Dashboard

+

{current === 'home' ? 'Dashboard' : current === 'account' ? 'Account' : 'Appearance'}

Signed in
-
-

Add Transaction

-
- setAmount(e.target.value)} required /> - setDescription(e.target.value)} /> - - -
-
+ {current === 'home' && ( + <> +
+

Add Transaction

+
+ setAmount(e.target.value)} required /> + setDescription(e.target.value)} /> + + +
+
-
-

Filters

-
- setMinAmount(e.target.value)} /> - setMaxAmount(e.target.value)} /> - - setSearchText(e.target.value)} /> -
-
+
+

Filters

+
+ setMinAmount(e.target.value)} /> + setMaxAmount(e.target.value)} /> + + setSearchText(e.target.value)} /> +
+
-
-

Latest Transactions (last 10)

- {loading ? ( -
Loading…
- ) : error ? ( -
{error}
- ) : filtered.length === 0 ? ( -
No transactions
- ) : ( - - - - - - - - - - - {filtered.map(t => ( - - - - - - - ))} - -
IDAmountDescriptionCategories
{t.id}{formatAmount(t.amount)}{t.description || ''}{t.category_ids.map(id => categoryNameById(id)).join(', ')}
- )} -
+
+

Latest Transactions (last 10)

+ {loading ? ( +
Loading…
+ ) : error ? ( +
{error}
+ ) : filtered.length === 0 ? ( +
No transactions
+ ) : ( + + + + + + + + + + + {filtered.map(t => ( + + + + + + + ))} + +
IDAmountDescriptionCategories
{t.id}{formatAmount(t.amount)}{t.description || ''}{t.category_ids.map(id => categoryNameById(id)).join(', ')}
+ )} +
+ + )} + + {current === 'account' && ( + // lazy import avoided for simplicity + + )} + + {current === 'appearance' && ( + + )}
diff --git a/7project/frontend/src/pages/LoginRegisterPage.tsx b/7project/frontend/src/pages/LoginRegisterPage.tsx index ce333d0..50896fe 100644 --- a/7project/frontend/src/pages/LoginRegisterPage.tsx +++ b/7project/frontend/src/pages/LoginRegisterPage.tsx @@ -1,6 +1,12 @@ import { useState, useEffect } from 'react'; import { login, register } from '../api'; +import { BACKEND_URL } from '../config'; +function oauthUrl(provider: 'mojeid' | 'bankid') { + const base = BACKEND_URL.replace(/\/$/, ''); + const redirect = encodeURIComponent(window.location.origin + '/oauth-callback'); + return `${base}/auth/${provider}/authorize?redirect_url=${redirect}`; +} export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => void }) { const [mode, setMode] = useState<'login' | 'register'>('login'); @@ -75,8 +81,13 @@ export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => vo )} {error &&
{error}
} -
- +
+
Or continue with
+
+ MojeID + BankID + +