Gonçalo Rodrigues 541a1c3556 feat: animated homepage + split personal/business navigation (#25)
- New animated homepage at / with 3D card tilt, particle canvas,
  floating ring decorations, and gradient title shimmer
- Personal finance pages move to /dashboard (base.html shows only
  personal nav + a subtle Business link)
- Business/org inner pages use base_org.html with a purple theme,
  org breadcrumb, Year/Team dropdowns, and a ← Hub back link
- org home/teams/members/invite/events/requests/ledger/analysis/report
  all switched to renderOrg() + base_org.html
- Route strings updated to org-home, org-teams, org-events, etc.
  so active nav highlighting works correctly in the business shell

Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 17:02:37 +01:00

735 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Finance Hub</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
:root {
--bg: #050810;
--text: #e8f4f2;
--text2: #8ab4b0;
--text3: #3d6e6a;
--teal: #00c9b8;
--teal2: #33d9ca;
--teal-glow:rgba(0,201,184,0.25);
--purple: #7c3aed;
--purple2: #a855f7;
--purple-glow:rgba(124,58,237,0.25);
--border: rgba(255,255,255,0.06);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
/* ── Canvas background ─────────────────────────────────────────── */
#bg-canvas {
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
}
/* ── Gradient orbs ─────────────────────────────────────────────── */
.orb {
position: fixed;
border-radius: 50%;
filter: blur(100px);
pointer-events: none;
z-index: 0;
animation: orb-drift 12s ease-in-out infinite alternate;
}
.orb-1 {
width: 600px; height: 600px;
background: radial-gradient(circle, rgba(0,201,184,0.12) 0%, transparent 70%);
top: -200px; left: -100px;
animation-delay: 0s;
}
.orb-2 {
width: 800px; height: 800px;
background: radial-gradient(circle, rgba(124,58,237,0.1) 0%, transparent 70%);
bottom: -300px; right: -200px;
animation-delay: -4s;
}
.orb-3 {
width: 400px; height: 400px;
background: radial-gradient(circle, rgba(168,85,247,0.08) 0%, transparent 70%);
top: 40%; left: 40%;
animation-delay: -8s;
}
@keyframes orb-drift {
from { transform: translate(0, 0) scale(1); }
to { transform: translate(40px, 30px) scale(1.08); }
}
/* ── Page wrapper ──────────────────────────────────────────────── */
.page {
position: relative;
z-index: 1;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 80px;
}
/* ── Nav bar ───────────────────────────────────────────────────── */
.topbar {
width: 100%;
max-width: 1200px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 24px 0;
}
.topbar-logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 700;
color: var(--text);
text-decoration: none;
letter-spacing: -0.3px;
}
.topbar-logo-icon {
width: 36px; height: 36px;
background: linear-gradient(135deg, var(--teal), var(--purple2));
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
box-shadow: 0 0 20px rgba(0,201,184,0.3), 0 0 40px rgba(124,58,237,0.2);
animation: logo-pulse 3s ease-in-out infinite;
}
@keyframes logo-pulse {
0%,100% { box-shadow: 0 0 20px rgba(0,201,184,0.3), 0 0 40px rgba(124,58,237,0.2); }
50% { box-shadow: 0 0 30px rgba(0,201,184,0.5), 0 0 60px rgba(124,58,237,0.35); }
}
.topbar-email {
font-size: 13px;
color: var(--text3);
letter-spacing: 0.02em;
}
/* ── Hero ──────────────────────────────────────────────────────── */
.hero {
text-align: center;
padding: 80px 0 60px;
max-width: 700px;
animation: fadeUp 0.8s ease-out both;
}
.hero-badge {
display: inline-block;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--teal);
background: rgba(0,201,184,0.08);
border: 1px solid rgba(0,201,184,0.2);
padding: 5px 14px;
border-radius: 100px;
margin-bottom: 24px;
animation: badge-glow 2.5s ease-in-out infinite;
}
@keyframes badge-glow {
0%,100% { box-shadow: 0 0 0 rgba(0,201,184,0); }
50% { box-shadow: 0 0 16px rgba(0,201,184,0.3); }
}
.hero-title {
font-size: clamp(48px, 8vw, 80px);
font-weight: 800;
letter-spacing: -2px;
line-height: 1.0;
margin-bottom: 20px;
background: linear-gradient(135deg, #fff 0%, var(--teal) 45%, var(--purple2) 80%, #fff 100%);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: title-shimmer 6s linear infinite;
}
@keyframes title-shimmer {
0% { background-position: 0% center; }
100% { background-position: 200% center; }
}
.hero-sub {
font-size: 18px;
color: var(--text2);
line-height: 1.6;
max-width: 480px;
margin: 0 auto;
}
/* ── Stats row ─────────────────────────────────────────────────── */
.stats-row {
display: flex;
gap: 48px;
justify-content: center;
padding: 0 0 60px;
animation: fadeUp 0.8s 0.15s ease-out both;
}
.stat {
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
color: var(--text);
}
.stat-label {
font-size: 12px;
color: var(--text3);
font-weight: 500;
letter-spacing: 0.05em;
text-transform: uppercase;
margin-top: 3px;
}
/* ── Products grid ─────────────────────────────────────────────── */
.products {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
width: 100%;
max-width: 900px;
animation: fadeUp 0.8s 0.25s ease-out both;
perspective: 1200px;
}
/* ── Product card ──────────────────────────────────────────────── */
.product-card {
position: relative;
background: rgba(13,18,28,0.7);
border: 1px solid var(--border);
border-radius: 24px;
padding: 36px 32px 32px;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
overflow: hidden;
cursor: pointer;
transition: transform 0.08s ease-out, box-shadow 0.3s ease;
transform-style: preserve-3d;
will-change: transform;
text-decoration: none;
color: inherit;
display: flex;
flex-direction: column;
gap: 16px;
}
.product-card:hover {
box-shadow: 0 30px 80px rgba(0,0,0,0.6);
}
/* animated corner glow */
.product-card::before {
content: '';
position: absolute;
inset: -1px;
border-radius: 24px;
padding: 1px;
background: var(--card-gradient, linear-gradient(135deg, transparent, transparent));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
transition: opacity 0.3s ease;
}
.product-card:hover::before { opacity: 1; }
/* inner glow blob */
.card-glow {
position: absolute;
width: 200px; height: 200px;
border-radius: 50%;
filter: blur(60px);
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
top: -60px; left: -60px;
}
.product-card:hover .card-glow { opacity: 1; }
/* teal card */
.card-personal {
--card-gradient: linear-gradient(135deg, var(--teal), transparent, var(--teal2));
animation: float-a 6s ease-in-out infinite;
}
.card-personal .card-glow { background: radial-gradient(circle, var(--teal-glow), transparent); }
.card-personal::after {
content: '';
position: absolute;
bottom: 0; right: 0;
width: 120px; height: 120px;
background: radial-gradient(circle, rgba(0,201,184,0.08), transparent);
border-radius: 50%;
transform: translate(30%, 30%);
}
/* purple card */
.card-business {
--card-gradient: linear-gradient(135deg, var(--purple), transparent, var(--purple2));
animation: float-b 6s ease-in-out infinite;
}
.card-business .card-glow { background: radial-gradient(circle, var(--purple-glow), transparent); }
.card-business::after {
content: '';
position: absolute;
bottom: 0; right: 0;
width: 120px; height: 120px;
background: radial-gradient(circle, rgba(124,58,237,0.08), transparent);
border-radius: 50%;
transform: translate(30%, 30%);
}
@keyframes float-a {
0%,100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
@keyframes float-b {
0%,100% { transform: translateY(-6px); }
50% { transform: translateY(6px); }
}
.card-header {
display: flex;
align-items: center;
gap: 14px;
}
.card-icon {
width: 52px; height: 52px;
border-radius: 14px;
display: flex; align-items: center; justify-content: center;
font-size: 24px;
flex-shrink: 0;
position: relative;
}
.card-personal .card-icon {
background: rgba(0,201,184,0.12);
border: 1px solid rgba(0,201,184,0.2);
box-shadow: 0 0 20px rgba(0,201,184,0.15);
}
.card-business .card-icon {
background: rgba(124,58,237,0.12);
border: 1px solid rgba(124,58,237,0.2);
box-shadow: 0 0 20px rgba(124,58,237,0.15);
}
.card-title {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.4px;
color: var(--text);
}
.card-label {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.card-personal .card-label { color: var(--teal); }
.card-business .card-label { color: var(--purple2); }
.card-desc {
font-size: 14px;
color: var(--text2);
line-height: 1.6;
}
/* feature list */
.feature-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
}
.feature-list li {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: var(--text2);
}
.feature-list li::before {
content: '';
width: 5px; height: 5px;
border-radius: 50%;
flex-shrink: 0;
}
.card-personal .feature-list li::before { background: var(--teal); }
.card-business .feature-list li::before { background: var(--purple2); }
/* CTA button */
.card-cta {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s ease;
margin-top: 4px;
align-self: flex-start;
position: relative;
overflow: hidden;
}
.card-cta::before {
content: '';
position: absolute;
inset: 0;
background: rgba(255,255,255,0.08);
opacity: 0;
transition: opacity 0.2s ease;
}
.card-cta:hover::before { opacity: 1; }
.card-personal .card-cta {
background: linear-gradient(135deg, var(--teal), var(--teal2));
color: #050810;
box-shadow: 0 4px 20px rgba(0,201,184,0.35);
}
.card-personal .card-cta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0,201,184,0.5);
}
.card-business .card-cta {
background: linear-gradient(135deg, var(--purple), var(--purple2));
color: #fff;
box-shadow: 0 4px 20px rgba(124,58,237,0.35);
}
.card-business .card-cta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(124,58,237,0.5);
}
.cta-arrow {
transition: transform 0.2s ease;
}
.card-cta:hover .cta-arrow { transform: translateX(4px); }
/* ── Divider ───────────────────────────────────────────────────── */
.divider {
width: 100%;
max-width: 900px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--border), transparent);
margin: 64px 0 48px;
}
/* ── Features row ──────────────────────────────────────────────── */
.features {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
width: 100%;
max-width: 900px;
animation: fadeUp 0.8s 0.35s ease-out both;
}
.feature-chip {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 18px;
background: rgba(255,255,255,0.025);
border: 1px solid var(--border);
border-radius: 14px;
font-size: 13px;
color: var(--text2);
transition: all 0.2s ease;
}
.feature-chip:hover {
background: rgba(255,255,255,0.04);
border-color: rgba(255,255,255,0.1);
color: var(--text);
}
.feature-chip-icon {
font-size: 18px;
}
/* ── Footer ────────────────────────────────────────────────────── */
.footer {
margin-top: 80px;
font-size: 12px;
color: var(--text3);
text-align: center;
}
/* ── Animations ────────────────────────────────────────────────── */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── Responsive ────────────────────────────────────────────────── */
@media (max-width: 640px) {
.products { grid-template-columns: 1fr; }
.features { grid-template-columns: 1fr 1fr; }
.stats-row { gap: 24px; }
.hero { padding: 50px 0 40px; }
}
</style>
</head>
<body>
<!-- Particle canvas background -->
<canvas id="bg-canvas"></canvas>
<!-- Gradient orbs -->
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="orb orb-3"></div>
<div class="page">
<!-- Top bar -->
<div class="topbar">
<a href="/" class="topbar-logo">
<div class="topbar-logo-icon"></div>
Finance Hub
</a>
{{if .Email}}<span class="topbar-email">{{.Email}}</span>{{end}}
</div>
<!-- Hero -->
<section class="hero">
<div class="hero-badge">Your Financial Universe</div>
<h1 class="hero-title">Finance Hub</h1>
<p class="hero-sub">One platform for managing personal wealth and business finances — beautifully unified.</p>
</section>
<!-- Stats -->
<div class="stats-row">
<div class="stat">
<div class="stat-value">2</div>
<div class="stat-label">Products</div>
</div>
<div class="stat">
<div class="stat-value"></div>
<div class="stat-label">Organisations</div>
</div>
<div class="stat">
<div class="stat-value">100%</div>
<div class="stat-label">Self-hosted</div>
</div>
</div>
<!-- Product cards -->
<div class="products" id="products">
<a href="/dashboard" class="product-card card-personal" id="card-personal">
<div class="card-glow"></div>
<div class="card-header">
<div class="card-icon">🏦</div>
<div>
<div class="card-label">Personal</div>
<div class="card-title">My Finance</div>
</div>
</div>
<p class="card-desc">Track spending, grow wealth, plan your future with full visibility into your personal finances.</p>
<ul class="feature-list">
<li>Dashboard &amp; spending analytics</li>
<li>Transactions &amp; auto-import</li>
<li>Investment portfolio</li>
<li>Financial goals</li>
<li>Net worth &amp; projections</li>
<li>Tax reports</li>
</ul>
<span class="card-cta">Open Personal <span class="cta-arrow"></span></span>
</a>
<a href="/orgs" class="product-card card-business" id="card-business">
<div class="card-glow"></div>
<div class="card-header">
<div class="card-icon">🏢</div>
<div>
<div class="card-label">Business</div>
<div class="card-title">Organisations</div>
</div>
</div>
<p class="card-desc">Manage organisations, plan events, control budgets, and keep your teams aligned financially.</p>
<ul class="feature-list">
<li>Multi-org &amp; teams</li>
<li>Events &amp; budget planning</li>
<li>Purchase requests &amp; approvals</li>
<li>Ledger &amp; bank imports</li>
<li>Variance analysis</li>
<li>End-of-year reports</li>
</ul>
<span class="card-cta">Open Business <span class="cta-arrow"></span></span>
</a>
</div>
<div class="divider"></div>
<!-- Feature chips -->
<div class="features">
<div class="feature-chip"><span class="feature-chip-icon">🔒</span> Self-hosted &amp; private</div>
<div class="feature-chip"><span class="feature-chip-icon">🌙</span> Dark &amp; light themes</div>
<div class="feature-chip"><span class="feature-chip-icon">📊</span> Real-time analytics</div>
<div class="feature-chip"><span class="feature-chip-icon">🏦</span> Multi-bank support</div>
<div class="feature-chip"><span class="feature-chip-icon">👥</span> Role-based access</div>
<div class="feature-chip"><span class="feature-chip-icon">📅</span> Fiscal year lifecycle</div>
</div>
<div class="footer">Finance Hub · Self-hosted homelab</div>
</div>
<script>
/* ── Particle canvas ─────────────────────────────────────────── */
(function() {
const canvas = document.getElementById('bg-canvas');
const ctx = canvas.getContext('2d');
let W, H, pts;
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
}
resize();
window.addEventListener('resize', () => { resize(); });
const N = 60;
function initPts() {
pts = Array.from({ length: N }, () => ({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 0.35,
vy: (Math.random() - 0.5) * 0.35,
r: Math.random() * 1.8 + 0.6,
}));
}
initPts();
let mouse = { x: W / 2, y: H / 2 };
window.addEventListener('mousemove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
function draw() {
ctx.clearRect(0, 0, W, H);
for (let p of pts) {
p.x += p.vx;
p.y += p.vy;
if (p.x < 0 || p.x > W) p.vx *= -1;
if (p.y < 0 || p.y > H) p.vy *= -1;
}
const LINK = 140;
for (let i = 0; i < pts.length; i++) {
for (let j = i + 1; j < pts.length; j++) {
const dx = pts[i].x - pts[j].x;
const dy = pts[i].y - pts[j].y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < LINK) {
ctx.beginPath();
ctx.moveTo(pts[i].x, pts[i].y);
ctx.lineTo(pts[j].x, pts[j].y);
ctx.strokeStyle = `rgba(0,201,184,${0.06 * (1 - d / LINK)})`;
ctx.lineWidth = 0.8;
ctx.stroke();
}
}
// mouse proximity glow
const mdx = pts[i].x - mouse.x;
const mdy = pts[i].y - mouse.y;
const md = Math.sqrt(mdx * mdx + mdy * mdy);
if (md < 160) {
ctx.beginPath();
ctx.arc(pts[i].x, pts[i].y, pts[i].r * 2.5, 0, Math.PI * 2);
ctx.fillStyle = `rgba(124,58,237,${0.7 * (1 - md / 160)})`;
ctx.fill();
}
ctx.beginPath();
ctx.arc(pts[i].x, pts[i].y, pts[i].r, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,201,184,0.35)';
ctx.fill();
}
requestAnimationFrame(draw);
}
draw();
})();
/* ── 3D card tilt on mousemove ───────────────────────────────── */
(function() {
const cards = document.querySelectorAll('.product-card');
const MAX_TILT = 12;
cards.forEach(card => {
card.addEventListener('mousemove', e => {
const rect = card.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = (e.clientX - cx) / (rect.width / 2);
const dy = (e.clientY - cy) / (rect.height / 2);
const rx = -dy * MAX_TILT;
const ry = dx * MAX_TILT;
card.style.transform = `perspective(900px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(8px)`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
card.style.transition = 'transform 0.5s ease, box-shadow 0.3s ease';
setTimeout(() => card.style.transition = '', 500);
});
card.addEventListener('mouseenter', () => {
card.style.transition = 'transform 0.08s ease-out, box-shadow 0.3s ease';
});
});
})();
/* ── 3D rotating ring decoration ────────────────────────────── */
(function() {
// subtle CSS 3D ring animation injected dynamically
const style = document.createElement('style');
style.textContent = `
@keyframes ring-spin {
from { transform: rotateX(70deg) rotateZ(0deg); }
to { transform: rotateX(70deg) rotateZ(360deg); }
}
.ring {
position: fixed;
width: 400px; height: 400px;
border-radius: 50%;
border: 1px solid rgba(0,201,184,0.08);
top: 50%; left: 50%;
margin: -200px 0 0 -200px;
animation: ring-spin 20s linear infinite;
pointer-events: none;
z-index: 0;
}
.ring:nth-child(2) {
width: 600px; height: 600px;
margin: -300px 0 0 -300px;
border-color: rgba(124,58,237,0.05);
animation-duration: 32s;
animation-direction: reverse;
}
.ring:nth-child(3) {
width: 240px; height: 240px;
margin: -120px 0 0 -120px;
border-color: rgba(168,85,247,0.1);
animation-duration: 14s;
}
`;
document.head.appendChild(style);
const container = document.createElement('div');
container.style.cssText = 'position:fixed;inset:0;z-index:0;pointer-events:none;perspective:800px;';
for (let i = 0; i < 3; i++) {
const r = document.createElement('div');
r.className = 'ring';
container.appendChild(r);
}
document.body.appendChild(container);
})();
</script>
</body>
</html>