Skip to content

Commit 5f48368

Browse files
committed
Redesign frontend as an Apple-Health-style bento mosaic
Drop the sidebar + uniform-card layout. The dashboards now live on a warm stone-50 background with a sticky top navigation (pill-style links, current page in a black chip), and content is composed of varied bento tiles instead of identical panels. Each section of the data has its own tint: savings is violet, mortgage amber, investments cyan, income emerald, expenses rose, generic accents indigo. Hero numbers are large tabular Inter, panel headers became small uppercase captions. The Overview becomes the showpiece — a 12-column mosaic combining the net-worth hero with sparkline, six section-tinted KPIs, a 12-month cashflow strip, a category doughnut, recent transactions, the accounts table and a counts row. The other pages now share the same tile aesthetic with a coloured hero tile up top and matching content cards. Chart.js retuned for the light theme: zinc-500 labels, zinc-200 grid, dark tooltips, no point dots, larger bar radii.
1 parent b456b8e commit 5f48368

10 files changed

Lines changed: 502 additions & 251 deletions

File tree

finance/static/finance/dashboard.js

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,25 @@ window.dashboard = (function () {
1818
});
1919
const intFmt = new Intl.NumberFormat("en-IE");
2020

21-
// Palette tuned for dark backgrounds: brighter, slightly less saturated
21+
// Section accent colours — pair with the .tile.* tints
22+
const accents = {
23+
income: "#10b981", // emerald-500
24+
expense: "#f43f5e", // rose-500
25+
expenses: "#f43f5e",
26+
savings: "#8b5cf6", // violet-500
27+
mortgage: "#f59e0b", // amber-500
28+
investment: "#06b6d4", // cyan-500
29+
investments:"#06b6d4",
30+
accent: "#6366f1", // indigo-500
31+
neutral: "#71717a", // zinc-500
32+
};
33+
34+
// Palette for varied category buckets (good contrast on white)
2235
const palette = [
23-
"#818cf8", "#34d399", "#fb7185", "#fbbf24", "#a78bfa",
24-
"#38bdf8", "#fb923c", "#22d3ee", "#c084fc", "#60a5fa",
25-
"#a3e635", "#f472b6", "#2dd4bf", "#f87171", "#4ade80",
26-
"#facc15", "#d4d4d8", "#94a3b8", "#5eead4", "#d8b4fe",
36+
"#6366f1", "#10b981", "#f43f5e", "#f59e0b", "#8b5cf6",
37+
"#06b6d4", "#f97316", "#0ea5e9", "#a855f7", "#3b82f6",
38+
"#84cc16", "#ec4899", "#14b8a6", "#fb7185", "#22c55e",
39+
"#eab308", "#ea580c", "#475569", "#0d9488", "#9333ea",
2740
];
2841

2942
function colourFor(key) {
@@ -52,37 +65,32 @@ window.dashboard = (function () {
5265
container.innerHTML = `<p class="empty">${message}</p>`;
5366
}
5467

55-
// Apply project-wide chart defaults once Chart.js is on the page
68+
// Light theme defaults for Chart.js — zinc-500 labels, zinc-200 grid,
69+
// dark tooltips with white text
5670
function themeChartJs() {
5771
if (!window.Chart) return;
5872
Chart.defaults.font.family =
5973
'"Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
6074
Chart.defaults.font.size = 12;
61-
// Dark theme: muted axis labels in zinc-400, gridlines in zinc-800
62-
Chart.defaults.color = "#a1a1aa";
63-
Chart.defaults.borderColor = "#27272a";
75+
Chart.defaults.color = "#71717a";
76+
Chart.defaults.borderColor = "#e4e4e7";
6477
Chart.defaults.plugins.tooltip.padding = 10;
65-
Chart.defaults.plugins.tooltip.cornerRadius = 10;
66-
// Slightly lifted zinc-900 with a hairline ring for the tooltip
67-
Chart.defaults.plugins.tooltip.backgroundColor = "rgba(24, 24, 27, 0.95)";
68-
Chart.defaults.plugins.tooltip.borderColor = "rgba(63, 63, 70, 0.8)";
69-
Chart.defaults.plugins.tooltip.borderWidth = 1;
70-
Chart.defaults.plugins.tooltip.titleColor = "#fafafa";
78+
Chart.defaults.plugins.tooltip.cornerRadius = 12;
79+
Chart.defaults.plugins.tooltip.backgroundColor = "#18181b";
80+
Chart.defaults.plugins.tooltip.titleColor = "#ffffff";
7181
Chart.defaults.plugins.tooltip.bodyColor = "#e4e4e7";
7282
Chart.defaults.plugins.tooltip.titleFont = { weight: "600" };
7383
Chart.defaults.plugins.tooltip.bodySpacing = 4;
7484
Chart.defaults.plugins.legend.labels.usePointStyle = true;
7585
Chart.defaults.plugins.legend.labels.boxWidth = 8;
7686
Chart.defaults.plugins.legend.labels.padding = 16;
77-
Chart.defaults.elements.bar.borderRadius = 6;
78-
Chart.defaults.elements.line.borderWidth = 2;
79-
Chart.defaults.elements.point.radius = 3;
87+
Chart.defaults.elements.bar.borderRadius = 8;
88+
Chart.defaults.elements.line.borderWidth = 2.5;
89+
Chart.defaults.elements.point.radius = 0;
8090
Chart.defaults.elements.point.hoverRadius = 5;
8191
}
8292

83-
// The dashboard.js script is deferred and runs after chart.umd.min.js,
84-
// so Chart is defined when this IIFE executes
8593
themeChartJs();
8694

87-
return { fetchJson, formatCurrency, formatInt, colourFor, palette, emptyMessage };
95+
return { fetchJson, formatCurrency, formatInt, colourFor, palette, accents, emptyMessage };
8896
})();

finance/templates/finance/accounts.html

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
{% block title %}Accounts{% endblock %}
44

55
{% block content %}
6-
<h1 class="text-2xl font-bold tracking-tight text-zinc-100">Accounts</h1>
6+
<h1 class="text-3xl font-semibold tracking-tight text-zinc-900 mb-6">Accounts</h1>
77

8-
<section class="panel">
8+
<div id="account-cards" class="grid grid-cols-12 gap-3 mb-3">
9+
<div class="tile col-span-12 empty">Loading…</div>
10+
</div>
11+
12+
<div class="tile col-span-12">
13+
<div class="tile-label mb-3">All accounts</div>
914
<table class="table-data">
1015
<thead>
1116
<tr>
@@ -15,12 +20,34 @@ <h1 class="text-2xl font-bold tracking-tight text-zinc-100">Accounts</h1>
1520
</thead>
1621
<tbody id="accounts-tbody"><tr><td colspan="7" class="empty">Loading…</td></tr></tbody>
1722
</table>
18-
</section>
23+
</div>
1924

2025
<script>
2126
window.addEventListener("load", async () => {
2227
const d = window.dashboard;
2328
const data = await d.fetchJson("/api/dashboard/accounts/");
29+
30+
// Per-account hero cards
31+
const cards = document.getElementById("account-cards");
32+
if (!data.length) {
33+
cards.innerHTML = '<div class="tile col-span-12 empty">No accounts yet.</div>';
34+
} else {
35+
cards.innerHTML = data.map(a => {
36+
const tone = a.scope === "business" ? "investment" : "accent";
37+
return `
38+
<div class="tile ${tone} col-span-12 md:col-span-4">
39+
<div class="flex items-baseline justify-between">
40+
<div class="tile-label">${a.scope}</div>
41+
<div class="text-xs text-zinc-500">${a.bank}</div>
42+
</div>
43+
<div class="font-medium mt-2">${a.name}</div>
44+
<div class="stat-num mt-1">${a.balance === null ? "—" : d.formatCurrency(a.balance, true)}</div>
45+
<div class="text-xs text-zinc-500 mt-2">Last movement ${a.last_movement || "—"}</div>
46+
</div>`;
47+
}).join("");
48+
}
49+
50+
// Full table
2451
const tbody = document.getElementById("accounts-tbody");
2552
if (!data.length) {
2653
tbody.innerHTML = '<tr><td colspan="7" class="empty">No accounts yet.</td></tr>';
@@ -29,9 +56,9 @@ <h1 class="text-2xl font-bold tracking-tight text-zinc-100">Accounts</h1>
2956
tbody.innerHTML = data.map(a => `
3057
<tr>
3158
<td class="font-medium">${a.name}</td>
32-
<td class="text-zinc-400">${a.bank}</td>
59+
<td class="text-zinc-500">${a.bank}</td>
3360
<td><span class="tag ${a.scope}">${a.scope}</span></td>
34-
<td class="text-zinc-400">${a.kind}</td>
61+
<td class="text-zinc-500">${a.kind}</td>
3562
<td class="text-xs tabular-nums text-zinc-500">${a.iban}</td>
3663
<td class="num">${a.balance === null ? "—" : d.formatCurrency(a.balance, true)}</td>
3764
<td class="text-zinc-500">${a.last_movement || "—"}</td>

finance/templates/finance/base.html

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,42 @@
88

99
<link rel="preconnect" href="https://fonts.googleapis.com">
1010
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
1212

1313
<link rel="stylesheet" href="{% static 'finance/tailwind.css' %}">
1414

1515
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js" defer></script>
1616
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js" defer></script>
1717
<script src="{% static 'finance/dashboard.js' %}" defer></script>
1818
</head>
19-
<body class="text-zinc-100 antialiased font-sans">
20-
<div class="grid min-h-screen md:grid-cols-[240px_1fr]">
21-
22-
<aside class="md:sticky md:top-0 md:h-screen bg-zinc-950/70 backdrop-blur border-b md:border-b-0 md:border-r border-zinc-800 p-4 md:p-6 flex md:flex-col gap-3 items-center md:items-stretch">
23-
<a class="flex items-center gap-2 md:pb-4 font-semibold tracking-tight text-zinc-100" href="{% url 'pages:overview' %}">
24-
<span class="w-8 h-8 rounded-lg bg-gradient-to-br from-indigo-500 to-violet-500 text-white grid place-items-center font-bold shadow-lg shadow-indigo-500/30"></span>
25-
<span>FinancialAudit</span>
26-
</a>
27-
<nav class="flex md:flex-col gap-1 overflow-x-auto md:overflow-visible flex-1">
28-
<a href="{% url 'pages:overview' %}" class="nav-link {% if request.resolver_match.url_name == 'overview' %}is-active{% endif %}">Overview</a>
29-
<a href="{% url 'pages:income' %}" class="nav-link {% if request.resolver_match.url_name == 'income' %}is-active{% endif %}">Income</a>
30-
<a href="{% url 'pages:expenses' %}" class="nav-link {% if request.resolver_match.url_name == 'expenses' %}is-active{% endif %}">Expenses</a>
31-
<a href="{% url 'pages:cashflow' %}" class="nav-link {% if request.resolver_match.url_name == 'cashflow' %}is-active{% endif %}">Cashflow</a>
32-
<a href="{% url 'pages:net-worth' %}" class="nav-link {% if request.resolver_match.url_name == 'net-worth' %}is-active{% endif %}">Net worth</a>
33-
<a href="{% url 'pages:accounts' %}" class="nav-link {% if request.resolver_match.url_name == 'accounts' %}is-active{% endif %}">Accounts</a>
34-
<a href="{% url 'pages:transactions' %}" class="nav-link {% if request.resolver_match.url_name == 'transactions' %}is-active{% endif %}">Transactions</a>
35-
</nav>
36-
<div class="hidden md:block md:pt-4 md:border-t md:border-zinc-800 text-xs text-zinc-500">
37-
<div class="flex items-center gap-2 mb-1.5 text-zinc-100 font-medium">
38-
<span class="w-6 h-6 rounded-full bg-indigo-500/20 text-indigo-300 grid place-items-center text-[0.7rem] font-bold">{{ request.user.username|slice:":1"|upper }}</span>
39-
<span>{{ request.user.username }}</span>
19+
<body class="font-sans antialiased">
20+
<div class="min-h-screen">
21+
<header class="topbar">
22+
<div class="topbar-inner">
23+
<a class="brand" href="{% url 'pages:overview' %}">
24+
<span class="brand-mark"></span>
25+
<span>FinancialAudit</span>
26+
</a>
27+
<nav class="topnav">
28+
<a href="{% url 'pages:overview' %}" class="{% if request.resolver_match.url_name == 'overview' %}is-active{% endif %}">Overview</a>
29+
<a href="{% url 'pages:income' %}" class="{% if request.resolver_match.url_name == 'income' %}is-active{% endif %}">Income</a>
30+
<a href="{% url 'pages:expenses' %}" class="{% if request.resolver_match.url_name == 'expenses' %}is-active{% endif %}">Expenses</a>
31+
<a href="{% url 'pages:cashflow' %}" class="{% if request.resolver_match.url_name == 'cashflow' %}is-active{% endif %}">Cashflow</a>
32+
<a href="{% url 'pages:net-worth' %}" class="{% if request.resolver_match.url_name == 'net-worth' %}is-active{% endif %}">Net worth</a>
33+
<a href="{% url 'pages:accounts' %}" class="{% if request.resolver_match.url_name == 'accounts' %}is-active{% endif %}">Accounts</a>
34+
<a href="{% url 'pages:transactions' %}" class="{% if request.resolver_match.url_name == 'transactions' %}is-active{% endif %}">Transactions</a>
35+
</nav>
36+
<div class="hidden md:flex items-center gap-3 text-sm text-zinc-500 shrink-0">
37+
<span class="text-zinc-700 font-medium">{{ request.user.username }}</span>
38+
<span class="text-zinc-300">·</span>
39+
<a href="/admin/" class="hover:text-zinc-900">Admin</a>
40+
<a href="/admin/logout/" class="hover:text-zinc-900">Sign out</a>
4041
</div>
41-
<a href="/admin/" class="text-indigo-400 hover:underline">Admin</a>
42-
<span class="text-zinc-700">·</span>
43-
<a href="/admin/logout/" class="text-indigo-400 hover:underline">Sign out</a>
44-
</div>
45-
</aside>
46-
47-
<main class="px-6 py-8 md:px-10 md:py-10 max-w-7xl w-full">
48-
<div class="space-y-6">
49-
{% block content %}{% endblock %}
5042
</div>
43+
</header>
44+
<main class="max-w-7xl mx-auto px-4 md:px-8 py-6 md:py-10">
45+
{% block content %}{% endblock %}
5146
</main>
52-
5347
</div>
5448
</body>
5549
</html>

finance/templates/finance/cashflow.html

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,59 @@
33
{% block title %}Cashflow{% endblock %}
44

55
{% block content %}
6-
<h1 class="text-2xl font-bold tracking-tight text-zinc-100">Cashflow</h1>
6+
<h1 class="text-3xl font-semibold tracking-tight text-zinc-900 mb-6">Cashflow</h1>
77

8-
<section class="panel">
9-
<h2>Monthly income vs expenses</h2>
10-
<div class="chart-wrap tall"><canvas id="cashflow-chart"></canvas></div>
11-
</section>
8+
<div class="grid grid-cols-12 gap-3">
129

13-
<section class="panel">
14-
<h2>Monthly totals</h2>
15-
<table class="table-data">
16-
<thead><tr><th>Month</th><th class="num">Income</th><th class="num">Expenses</th><th class="num">Net</th></tr></thead>
17-
<tbody id="cashflow-tbody"><tr><td colspan="4" class="empty">Loading…</td></tr></tbody>
18-
</table>
19-
</section>
10+
<div class="tile income col-span-12 md:col-span-4">
11+
<div class="tile-label">Total income</div>
12+
<div class="stat-num" id="kpi-income"></div>
13+
</div>
14+
<div class="tile expenses col-span-12 md:col-span-4">
15+
<div class="tile-label">Total expenses</div>
16+
<div class="stat-num" id="kpi-expenses"></div>
17+
</div>
18+
<div class="tile accent col-span-12 md:col-span-4">
19+
<div class="tile-label">Net cashflow</div>
20+
<div class="stat-num" id="kpi-net"></div>
21+
</div>
22+
23+
<div class="tile col-span-12">
24+
<div class="tile-label mb-3">Monthly income vs expenses</div>
25+
<div class="chart-wrap tall"><canvas id="cashflow-chart"></canvas></div>
26+
</div>
27+
28+
<div class="tile col-span-12">
29+
<div class="tile-label mb-3">Monthly totals</div>
30+
<table class="table-data">
31+
<thead><tr><th>Month</th><th class="num">Income</th><th class="num">Expenses</th><th class="num">Net</th></tr></thead>
32+
<tbody id="cashflow-tbody"><tr><td colspan="4" class="empty">Loading…</td></tr></tbody>
33+
</table>
34+
</div>
35+
36+
</div>
2037

2138
<script>
2239
window.addEventListener("load", async () => {
2340
const d = window.dashboard;
2441
const data = await d.fetchJson("/api/dashboard/cashflow/");
42+
const totalIn = data.reduce((s, r) => s + r.income, 0);
43+
const totalOut = data.reduce((s, r) => s + r.expenses, 0);
44+
document.getElementById("kpi-income").textContent = d.formatCurrency(totalIn);
45+
document.getElementById("kpi-expenses").textContent = d.formatCurrency(-totalOut);
46+
const net = totalIn - totalOut;
47+
const netEl = document.getElementById("kpi-net");
48+
netEl.textContent = d.formatCurrency(net);
49+
netEl.classList.add(net >= 0 ? "text-emerald-700" : "text-rose-700");
2550

2651
new Chart(document.getElementById("cashflow-chart"), {
2752
type: "bar",
2853
data: {
2954
labels: data.map(r => r.period),
3055
datasets: [
31-
{ label: "Income", data: data.map(r => r.income), backgroundColor: "#34d399" },
32-
{ label: "Expenses", data: data.map(r => -r.expenses), backgroundColor: "#fb7185" },
33-
{ label: "Net", data: data.map(r => r.net), type: "line", borderColor: "#818cf8", backgroundColor: "#818cf8", tension: 0.25 },
56+
{ label: "Income", data: data.map(r => r.income), backgroundColor: d.accents.income },
57+
{ label: "Expenses", data: data.map(r => -r.expenses), backgroundColor: d.accents.expense },
58+
{ label: "Net", data: data.map(r => r.net), type: "line", borderColor: d.accents.accent, pointRadius: 0, tension: 0.25 },
3459
],
3560
},
3661
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom" } } },
@@ -43,9 +68,9 @@ <h2>Monthly totals</h2>
4368
tbody.innerHTML = data.map(r => `
4469
<tr>
4570
<td>${r.period}</td>
46-
<td class="num text-emerald-400">${d.formatCurrency(r.income, true)}</td>
47-
<td class="num text-rose-400">${d.formatCurrency(-r.expenses, true)}</td>
48-
<td class="num font-semibold ${r.net >= 0 ? "text-emerald-400" : "text-rose-400"}">${d.formatCurrency(r.net, true)}</td>
71+
<td class="num text-emerald-600">${d.formatCurrency(r.income, true)}</td>
72+
<td class="num text-rose-600">${d.formatCurrency(-r.expenses, true)}</td>
73+
<td class="num font-semibold ${r.net >= 0 ? "text-emerald-600" : "text-rose-600"}">${d.formatCurrency(r.net, true)}</td>
4974
</tr>`).join("");
5075
}
5176
});

0 commit comments

Comments
 (0)