fix: portfolio value formula, Yahoo UA header, mobile nav hamburger

- Remove erroneous /100 in currentValue calculation (portfolio values
  were 100x too small, causing net worth card to show ~€0)
- Add User-Agent header to Yahoo Finance requests (avoids 429s)
- Wrap ES module chart body in if(total>0){} block (return at top
  level of a module is a SyntaxError — chart was silently broken)
- Add mobile hamburger menu: full-screen drawer at ≤720px with
  animated open/close, all nav links, scroll lock while open

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gonçalo Rodrigues 2026-06-13 19:23:11 +01:00
parent 981b832d7f
commit a66941160a

View File

@ -413,17 +413,67 @@
.animate-on-scroll:nth-child(4) { transition-delay: 0.21s; } .animate-on-scroll:nth-child(4) { transition-delay: 0.21s; }
.animate-on-scroll:nth-child(5) { transition-delay: 0.28s; } .animate-on-scroll:nth-child(5) { transition-delay: 0.28s; }
/* ── Hamburger / mobile drawer ────────────────────────────────────── */
.nav-hamburger {
display: none;
width: 36px; height: 36px;
border: none; background: none; cursor: pointer;
color: var(--text2);
flex-direction: column; justify-content: center; align-items: center; gap: 5px;
border-radius: var(--radius-sm);
transition: background 0.18s;
}
.nav-hamburger:hover { background: var(--surface2); }
.nav-hamburger span {
display: block; width: 20px; height: 2px;
background: currentColor; border-radius: 2px;
transition: transform 0.25s ease, opacity 0.25s ease;
}
.nav-hamburger.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-hamburger.open span:nth-child(2) { opacity: 0; }
.nav-hamburger.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.nav-drawer {
display: none;
position: fixed; inset: var(--nav-h) 0 0 0;
background: var(--bg);
z-index: 199;
overflow-y: auto;
padding: 12px 16px 32px;
flex-direction: column; gap: 4px;
animation: slideIn 0.2s ease-out both;
}
.nav-drawer.open { display: flex; }
.nav-drawer a, .nav-drawer-section-label {
display: block;
padding: 11px 14px;
border-radius: var(--radius-sm);
font-size: 15px; font-weight: 500;
color: var(--text2); text-decoration: none;
transition: all 0.15s;
}
.nav-drawer a:hover { color: var(--text); background: var(--surface2); }
.nav-drawer a.active { color: var(--accent2); background: var(--accent-glow); }
.nav-drawer-section-label {
font-size: 11px; font-weight: 600; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--text3);
padding-top: 16px; padding-bottom: 4px;
}
.nav-drawer hr { border: none; border-top: 1px solid var(--border); margin: 8px 0; }
/* ── Responsive ──────────────────────────────────────────────────── */ /* ── Responsive ──────────────────────────────────────────────────── */
@media (max-width: 900px) { @media (max-width: 900px) {
.grid-2 { grid-template-columns: 1fr; } .grid-2 { grid-template-columns: 1fr; }
} }
@media (max-width: 600px) { @media (max-width: 720px) {
.nav { padding: 0 12px; gap: 2px; overflow-x: auto; } .nav-hamburger { display: flex; }
.nav a:not(.nav-brand) { padding: 6px 7px; font-size: 12px; } .nav > a:not(.nav-brand),
.nav > .nav-group,
.nav > .nav-spacer,
.nav-email { display: none; }
.container { padding: 16px 12px 32px; } .container { padding: 16px 12px 32px; }
.grid { grid-template-columns: 1fr 1fr; } .grid { grid-template-columns: 1fr 1fr; }
.card { padding: 16px; } .card { padding: 16px; }
.nav-email { display: none; }
} }
</style> </style>
</head> </head>
@ -469,8 +519,32 @@
<div class="nav-spacer"></div> <div class="nav-spacer"></div>
<span class="nav-email">{{.Email}}</span> <span class="nav-email">{{.Email}}</span>
<button class="theme-btn" id="theme-toggle" title="Toggle dark/light mode">🌙</button> <button class="theme-btn" id="theme-toggle" title="Toggle dark/light mode">🌙</button>
<button class="nav-hamburger" id="nav-hamburger" aria-label="Menu">
<span></span><span></span><span></span>
</button>
</nav> </nav>
<!-- Mobile drawer -->
<div class="nav-drawer" id="nav-drawer">
<a href="/" class="{{if eq .Route "dashboard"}}active{{end}}">Dashboard</a>
<a href="/transactions" class="{{if eq .Route "transactions"}}active{{end}}">Transactions</a>
<a href="/portfolio" class="{{if eq .Route "portfolio"}}active{{end}}">Portfolio</a>
<a href="/goals" class="{{if eq .Route "goals"}}active{{end}}">Goals</a>
<a href="/people" class="{{if eq .Route "people"}}active{{end}}">People</a>
<hr>
<span class="nav-drawer-section-label">Analysis</span>
<a href="/reports" class="{{if eq .Route "reports"}}active{{end}}">Reports</a>
<a href="/projections" class="{{if eq .Route "projections"}}active{{end}}">Projections</a>
<a href="/tax" class="{{if eq .Route "tax"}}active{{end}}">Tax</a>
<a href="/networth" class="{{if eq .Route "networth"}}active{{end}}">Net Worth</a>
<a href="/simulator" class="{{if eq .Route "simulator"}}active{{end}}">What If</a>
<hr>
<span class="nav-drawer-section-label">Settings</span>
<a href="/settings?tab=accounts" class="{{if eq .Route "settings"}}active{{end}}">Accounts &amp; Categories</a>
<a href="/import" class="{{if eq .Route "import"}}active{{end}}">Import CSV</a>
<a href="/auto-import" class="{{if eq .Route "auto-import"}}active{{end}}">Import Guide</a>
</div>
<div class="container"> <div class="container">
{{block "content" .}}{{end}} {{block "content" .}}{{end}}
</div> </div>
@ -493,6 +567,22 @@
btn.addEventListener('click', () => btn.addEventListener('click', () =>
applyTheme(html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark')); applyTheme(html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark'));
/* ── Mobile hamburger ─────────────────────────────────────────── */
const hamburger = document.getElementById('nav-hamburger');
const drawer = document.getElementById('nav-drawer');
hamburger.addEventListener('click', () => {
const open = drawer.classList.toggle('open');
hamburger.classList.toggle('open', open);
document.body.style.overflow = open ? 'hidden' : '';
});
drawer.querySelectorAll('a').forEach(a =>
a.addEventListener('click', () => {
drawer.classList.remove('open');
hamburger.classList.remove('open');
document.body.style.overflow = '';
})
);
/* ── Animated counter ─────────────────────────────────────────── */ /* ── Animated counter ─────────────────────────────────────────── */
function animateCounter(el) { function animateCounter(el) {
const target = parseFloat(el.getAttribute('data-target')); const target = parseFloat(el.getAttribute('data-target'));