feat(frontend): improved and centered UI

This commit is contained in:
ribardej
2025-10-15 10:06:22 +02:00
parent 89d032dd69
commit eb087e457c
5 changed files with 127 additions and 152 deletions

View File

@@ -36,9 +36,7 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
}
}
useEffect(() => {
loadAll();
}, []);
useEffect(() => { loadAll(); }, []);
const last10 = useMemo(() => {
const sorted = [...transactions].sort((a, b) => b.id - a.id);
@@ -56,9 +54,7 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
return arr;
}, [last10, minAmount, maxAmount, filterCategoryId, searchText]);
function categoryNameById(id: number) {
return categories.find(c => c.id === id)?.name || `#${id}`;
}
function categoryNameById(id: number) { return categories.find(c => c.id === id)?.name || `#${id}`; }
async function handleCreate(e: React.FormEvent) {
e.preventDefault();
@@ -71,85 +67,90 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
try {
const created = await createTransaction(payload);
setTransactions(prev => [created, ...prev]);
// reset form
setAmount('');
setDescription('');
setSelectedCategoryId('');
setAmount(''); setDescription(''); setSelectedCategoryId('');
} catch (err: any) {
alert(err?.message || 'Failed to create transaction');
}
}
return (
<div style={{ maxWidth: 900, margin: '2rem auto', padding: 16 }}>
<header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<h2 style={{ margin: 0 }}>Dashboard</h2>
<button onClick={onLogout}>Logout</button>
</header>
<section style={{ border: '1px solid #eee', padding: 12, borderRadius: 8, marginBottom: 16 }}>
<h3 style={{ marginTop: 0 }}>Add Transaction</h3>
<form onSubmit={handleCreate} style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<input type="number" step="0.01" placeholder="Amount" value={amount} onChange={(e) => setAmount(e.target.value)} required />
<input type="text" placeholder="Description (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
<select value={selectedCategoryId} onChange={(e) => setSelectedCategoryId(e.target.value ? Number(e.target.value) : '')}>
<option value="">No category</option>
{categories.map(c => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
<button type="submit">Add</button>
</form>
</section>
<section style={{ border: '1px solid #eee', padding: 12, borderRadius: 8, marginBottom: 16 }}>
<h3 style={{ marginTop: 0 }}>Filters</h3>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<input type="number" step="0.01" placeholder="Min amount" value={minAmount} onChange={(e) => setMinAmount(e.target.value)} />
<input type="number" step="0.01" placeholder="Max amount" value={maxAmount} onChange={(e) => setMaxAmount(e.target.value)} />
<select value={filterCategoryId} onChange={(e) => setFilterCategoryId(e.target.value ? Number(e.target.value) : '')}>
<option value="">All categories</option>
{categories.map(c => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
<input type="text" placeholder="Search in description" value={searchText} onChange={(e) => setSearchText(e.target.value)} />
<div className="app-layout">
<aside className="sidebar">
<div className="logo">7Project</div>
<nav className="nav">
<button className="active">Home</button>
<button>Account</button>
<button>Appearance</button>
</nav>
</aside>
<div className="content">
<div className="topbar">
<h2 style={{ margin: 0 }}>Dashboard</h2>
<div className="actions">
<span className="user muted">Signed in</span>
<button className="btn" onClick={onLogout}>Logout</button>
</div>
</div>
</section>
<main className="page space-y">
<section className="card">
<h3>Add Transaction</h3>
<form onSubmit={handleCreate} className="form-row">
<input className="input" type="number" step="0.01" placeholder="Amount" value={amount} onChange={(e) => setAmount(e.target.value)} required />
<input className="input" type="text" placeholder="Description (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
<select className="input" value={selectedCategoryId} onChange={(e) => setSelectedCategoryId(e.target.value ? Number(e.target.value) : '')}>
<option value="">No category</option>
{categories.map(c => (<option key={c.id} value={c.id}>{c.name}</option>))}
</select>
<button className="btn primary" type="submit">Add</button>
</form>
</section>
<section style={{ border: '1px solid #eee', padding: 12, borderRadius: 8 }}>
<h3 style={{ marginTop: 0 }}>Latest Transactions (last 10)</h3>
{loading ? (
<div>Loading</div>
) : error ? (
<div style={{ color: 'crimson' }}>{error}</div>
) : filtered.length === 0 ? (
<div>No transactions</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={{ textAlign: 'left', borderBottom: '1px solid #ddd' }}>ID</th>
<th style={{ textAlign: 'right', borderBottom: '1px solid #ddd' }}>Amount</th>
<th style={{ textAlign: 'left', borderBottom: '1px solid #ddd' }}>Description</th>
<th style={{ textAlign: 'left', borderBottom: '1px solid #ddd' }}>Categories</th>
</tr>
</thead>
<tbody>
{filtered.map(t => (
<tr key={t.id}>
<td style={{ padding: '6px 4px' }}>{t.id}</td>
<td style={{ padding: '6px 4px', textAlign: 'right' }}>{formatAmount(t.amount)}</td>
<td style={{ padding: '6px 4px' }}>{t.description || ''}</td>
<td style={{ padding: '6px 4px' }}>
{t.category_ids.map(id => categoryNameById(id)).join(', ')}
</td>
</tr>
))}
</tbody>
</table>
)}
</section>
<section className="card">
<h3>Filters</h3>
<div className="form-row">
<input className="input" type="number" step="0.01" placeholder="Min amount" value={minAmount} onChange={(e) => setMinAmount(e.target.value)} />
<input className="input" type="number" step="0.01" placeholder="Max amount" value={maxAmount} onChange={(e) => setMaxAmount(e.target.value)} />
<select className="input" value={filterCategoryId} onChange={(e) => setFilterCategoryId(e.target.value ? Number(e.target.value) : '')}>
<option value="">All categories</option>
{categories.map(c => (<option key={c.id} value={c.id}>{c.name}</option>))}
</select>
<input className="input" type="text" placeholder="Search in description" value={searchText} onChange={(e) => setSearchText(e.target.value)} />
</div>
</section>
<section className="card">
<h3>Latest Transactions (last 10)</h3>
{loading ? (
<div>Loading</div>
) : error ? (
<div style={{ color: 'crimson' }}>{error}</div>
) : filtered.length === 0 ? (
<div>No transactions</div>
) : (
<table className="table">
<thead>
<tr>
<th>ID</th>
<th style={{ textAlign: 'right' }}>Amount</th>
<th>Description</th>
<th>Categories</th>
</tr>
</thead>
<tbody>
{filtered.map(t => (
<tr key={t.id}>
<td>{t.id}</td>
<td className="amount">{formatAmount(t.amount)}</td>
<td>{t.description || ''}</td>
<td>{t.category_ids.map(id => categoryNameById(id)).join(', ')}</td>
</tr>
))}
</tbody>
</table>
)}
</section>
</main>
</div>
</div>
);
}