mirror of
https://github.com/dat515-2025/Group-8.git
synced 2026-03-22 15:12:08 +01:00
feat(frontend): added account and appearance tabs
This commit is contained in:
@@ -8,6 +8,18 @@ function App() {
|
|||||||
const [hasToken, setHasToken] = useState<boolean>(!!localStorage.getItem('token'));
|
const [hasToken, setHasToken] = useState<boolean>(!!localStorage.getItem('token'));
|
||||||
|
|
||||||
useEffect(() => {
|
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) => {
|
const onStorage = (e: StorageEvent) => {
|
||||||
if (e.key === 'token') setHasToken(!!e.newValue);
|
if (e.key === 'token') setHasToken(!!e.newValue);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -95,6 +95,49 @@ export async function getTransactions(): Promise<Transaction[]> {
|
|||||||
return res.json();
|
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<User> {
|
||||||
|
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<Pick<User, 'first_name' | 'last_name'>> & { password?: string };
|
||||||
|
export async function updateMe(input: UpdateMeInput): Promise<User> {
|
||||||
|
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<void> {
|
||||||
|
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() {
|
export function logout() {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import './ui.css'
|
import './ui.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
import { applyAppearanceFromStorage } from './appearance'
|
||||||
|
|
||||||
|
applyAppearanceFromStorage()
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { type Category, type Transaction, createTransaction, getCategories, getTransactions } from '../api';
|
import { type Category, type Transaction, createTransaction, getCategories, getTransactions } from '../api';
|
||||||
|
import AccountPage from './AccountPage';
|
||||||
|
import AppearancePage from './AppearancePage';
|
||||||
|
|
||||||
function formatAmount(n: number) {
|
function formatAmount(n: number) {
|
||||||
return new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n);
|
return new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||||
|
const [current, setCurrent] = useState<'home' | 'account' | 'appearance'>('home');
|
||||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -78,20 +81,22 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
|||||||
<aside className="sidebar">
|
<aside className="sidebar">
|
||||||
<div className="logo">7Project</div>
|
<div className="logo">7Project</div>
|
||||||
<nav className="nav">
|
<nav className="nav">
|
||||||
<button className="active">Home</button>
|
<button className={current === 'home' ? 'active' : ''} onClick={() => setCurrent('home')}>Home</button>
|
||||||
<button>Account</button>
|
<button className={current === 'account' ? 'active' : ''} onClick={() => setCurrent('account')}>Account</button>
|
||||||
<button>Appearance</button>
|
<button className={current === 'appearance' ? 'active' : ''} onClick={() => setCurrent('appearance')}>Appearance</button>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="topbar">
|
<div className="topbar">
|
||||||
<h2 style={{ margin: 0 }}>Dashboard</h2>
|
<h2 style={{ margin: 0 }}>{current === 'home' ? 'Dashboard' : current === 'account' ? 'Account' : 'Appearance'}</h2>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<span className="user muted">Signed in</span>
|
<span className="user muted">Signed in</span>
|
||||||
<button className="btn" onClick={onLogout}>Logout</button>
|
<button className="btn" onClick={onLogout}>Logout</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<main className="page space-y">
|
<main className="page space-y">
|
||||||
|
{current === 'home' && (
|
||||||
|
<>
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<h3>Add Transaction</h3>
|
<h3>Add Transaction</h3>
|
||||||
<form onSubmit={handleCreate} className="form-row">
|
<form onSubmit={handleCreate} className="form-row">
|
||||||
@@ -149,6 +154,17 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
|||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{current === 'account' && (
|
||||||
|
// lazy import avoided for simplicity
|
||||||
|
<AccountPage onDeleted={onLogout} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{current === 'appearance' && (
|
||||||
|
<AppearancePage />
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { login, register } from '../api';
|
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 }) {
|
export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => void }) {
|
||||||
const [mode, setMode] = useState<'login' | 'register'>('login');
|
const [mode, setMode] = useState<'login' | 'register'>('login');
|
||||||
@@ -75,9 +81,14 @@ export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => vo
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && <div style={{ color: 'crimson' }}>{error}</div>}
|
{error && <div style={{ color: 'crimson' }}>{error}</div>}
|
||||||
<div className="actions" style={{ justifyContent: 'flex-end' }}>
|
<div className="actions" style={{ justifyContent: 'space-between' }}>
|
||||||
|
<div className="muted">Or continue with</div>
|
||||||
|
<div className="actions">
|
||||||
|
<a className="btn" href={oauthUrl('mojeid')}>MojeID</a>
|
||||||
|
<a className="btn" href={oauthUrl('bankid')}>BankID</a>
|
||||||
<button className="btn primary" type="submit" disabled={loading}>{loading ? 'Please wait…' : (mode === 'login' ? 'Login' : 'Register')}</button>
|
<button className="btn primary" type="submit" disabled={loading}>{loading ? 'Please wait…' : (mode === 'login' ? 'Login' : 'Register')}</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user