243 lines
12 KiB
HTML
243 lines
12 KiB
HTML
{{define "content"}}
|
|
{{$d := .}}
|
|
|
|
<div style="display:flex; align-items:baseline; justify-content:space-between; margin-bottom:24px; flex-wrap:wrap; gap:8px;">
|
|
<h1>Goals</h1>
|
|
<button onclick="document.getElementById('new-goal-modal').style.display='flex'"
|
|
class="btn btn-primary btn-sm">+ New goal</button>
|
|
</div>
|
|
|
|
{{if $d.AvgMonthlySavings}}
|
|
<div style="display:flex; gap:10px; margin-bottom:20px; flex-wrap:wrap;">
|
|
<div class="card value-card animate-on-scroll" style="flex:1; min-width:160px;">
|
|
<h2>Avg monthly savings</h2>
|
|
<div class="value positive animate-counter" data-target="{{$d.AvgMonthlySavings}}" data-prefix="€">€0</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:4px;">last 3 months</p>
|
|
</div>
|
|
<div class="card value-card animate-on-scroll" style="flex:1; min-width:160px;">
|
|
<h2>Disposable income</h2>
|
|
<div class="value animate-counter" data-target="{{$d.DisposableIncome}}" data-prefix="€" style="color:var(--text);">€0</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:4px;">this month</p>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if $d.Goals}}
|
|
<div style="display:flex; flex-direction:column; gap:14px;">
|
|
{{range $d.Goals}}
|
|
<div class="card animate-on-scroll">
|
|
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap;">
|
|
|
|
<!-- left: name + type + progress -->
|
|
<div style="flex:1; min-width:200px;">
|
|
<div style="display:flex; align-items:center; gap:10px; margin-bottom:4px;">
|
|
<span style="font-size:18px;">{{if eq .Type "once"}}🎯{{else if eq .Type "deposit"}}🏠{{else if eq .Type "emergency"}}🛡️{{else}}📈{{end}}</span>
|
|
<div>
|
|
<div style="font-size:15px; font-weight:600; color:var(--text);">{{.Name}}</div>
|
|
<div style="font-size:11px; color:var(--text3); text-transform:uppercase; letter-spacing:.4px;">
|
|
{{if eq .Type "once"}}One-off purchase{{else if eq .Type "deposit"}}Deposit / down-payment{{else if eq .Type "emergency"}}Emergency fund{{else}}Recurring investment{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top:12px;">
|
|
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
|
|
<span style="font-size:12px; color:var(--text2);">€{{cents .SavedCents}} saved of €{{cents .TargetCents}}</span>
|
|
<span style="font-size:12px; color:var(--text2);">{{.ProgressPct}}%</span>
|
|
</div>
|
|
<div style="background:var(--bg3); border-radius:99px; height:6px; overflow:hidden;">
|
|
<div style="height:100%; border-radius:99px; width:{{.ProgressPct}}%;
|
|
background:{{if .Feasible}}var(--green){{else}}var(--accent){{end}};
|
|
transition:width 1s ease;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- right: projection numbers -->
|
|
<div style="display:flex; gap:24px; flex-wrap:wrap; align-items:flex-start;">
|
|
|
|
<div style="text-align:center;">
|
|
<div style="font-size:11px; color:var(--text3); margin-bottom:3px;">Need per month</div>
|
|
<div class="animate-counter" style="font-size:18px; font-weight:600; color:var(--text);"
|
|
data-target="{{.MonthlyCents}}" data-prefix="€">€0</div>
|
|
</div>
|
|
|
|
<div style="text-align:center;">
|
|
<div style="font-size:11px; color:var(--text3); margin-bottom:3px;">Months left</div>
|
|
<div style="font-size:18px; font-weight:600; color:var(--text);">{{.MonthsLeft}}</div>
|
|
</div>
|
|
|
|
<div style="text-align:center;">
|
|
<div style="font-size:11px; color:var(--text3); margin-bottom:3px;">At current rate</div>
|
|
{{if gt .MonthsAtCurrentRate 0}}
|
|
<div style="font-size:18px; font-weight:600;
|
|
{{if .Feasible}}color:var(--green){{else}}color:var(--red){{end}};">
|
|
{{.MonthsAtCurrentRate}}mo
|
|
</div>
|
|
{{else}}
|
|
<div style="font-size:14px; color:var(--text3);">—</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div style="text-align:center;">
|
|
<div style="font-size:11px; color:var(--text3); margin-bottom:3px;">Disposable after</div>
|
|
<div class="animate-counter" style="font-size:18px; font-weight:600;
|
|
{{if ge .ImpactOnDisposable 0}}color:var(--green){{else}}color:var(--red){{end}};"
|
|
data-target="{{.ImpactOnDisposable}}" data-prefix="€">€0</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- feasibility banner -->
|
|
<div style="margin-top:14px; padding:10px 14px; border-radius:8px; font-size:13px;
|
|
background:{{if .Feasible}}rgba(74,222,128,0.08){{else}}rgba(248,113,113,0.08){{end}};
|
|
border:1px solid {{if .Feasible}}rgba(74,222,128,0.2){{else}}rgba(248,113,113,0.2){{end}};
|
|
color:{{if .Feasible}}var(--green){{else}}var(--red){{end}};">
|
|
{{if .Feasible}}
|
|
✓ On track — your current savings rate covers the required €{{cents .MonthlyCents}}/month with {{.MonthsLeft}} months to go.
|
|
{{else}}
|
|
⚠ At your current savings rate (€{{cents $d.AvgMonthlySavings}}/mo) you'd reach this in {{.MonthsAtCurrentRate}} months, missing the deadline by {{sub .MonthsAtCurrentRate .MonthsLeft}} months.
|
|
{{end}}
|
|
</div>
|
|
|
|
<!-- delete -->
|
|
<form method="POST" action="/goals" style="margin-top:10px; text-align:right;">
|
|
<input type="hidden" name="action" value="delete">
|
|
<input type="hidden" name="id" value="{{.ID}}">
|
|
<button type="submit" class="btn btn-outline btn-sm" style="color:var(--red); border-color:var(--red)33;"
|
|
onclick="return confirm('Remove this goal?')">Remove</button>
|
|
</form>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{else}}
|
|
<div class="card empty-state animate-on-scroll">
|
|
<div style="font-size:48px; margin-bottom:16px;">🎯</div>
|
|
<h3>No goals yet</h3>
|
|
<p style="margin-bottom:20px;">Plan a purchase, a house deposit, or an emergency fund.<br>See exactly how much you need to save each month to get there.</p>
|
|
<button onclick="document.getElementById('new-goal-modal').style.display='flex'" class="btn btn-primary">
|
|
Add your first goal
|
|
</button>
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- New goal modal -->
|
|
<div id="new-goal-modal" style="display:none; position:fixed; inset:0; z-index:1000;
|
|
background:rgba(0,0,0,0.6); backdrop-filter:blur(4px);
|
|
align-items:center; justify-content:center; padding:20px;">
|
|
<div style="background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius);
|
|
padding:28px; width:100%; max-width:460px; position:relative;">
|
|
<h2 style="margin-bottom:20px;">New goal</h2>
|
|
|
|
<form method="POST" action="/goals" style="display:flex; flex-direction:column; gap:16px;">
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text2); display:block; margin-bottom:6px;">Goal name</label>
|
|
<input name="name" required placeholder="e.g. Nintendo Switch, House deposit…"
|
|
style="width:100%; padding:9px 12px; background:var(--bg3); border:1px solid var(--border);
|
|
border-radius:8px; color:var(--text); font-size:14px;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text2); display:block; margin-bottom:6px;">Type</label>
|
|
<select name="type" id="goal-type" onchange="updateTypeHint(this.value)"
|
|
style="width:100%; padding:9px 12px; background:var(--bg3); border:1px solid var(--border);
|
|
border-radius:8px; color:var(--text); font-size:14px;">
|
|
<option value="once">🎯 One-off purchase</option>
|
|
<option value="deposit">🏠 Deposit / down-payment</option>
|
|
<option value="emergency">🛡️ Emergency fund</option>
|
|
<option value="investment">📈 Recurring investment</option>
|
|
</select>
|
|
<p id="type-hint" style="font-size:12px; color:var(--text3); margin-top:5px;">
|
|
A specific purchase you want to save up for.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text2); display:block; margin-bottom:6px;">Target amount (€)</label>
|
|
<input name="target_euros" type="number" min="1" step="0.01" required placeholder="500"
|
|
style="width:100%; padding:9px 12px; background:var(--bg3); border:1px solid var(--border);
|
|
border-radius:8px; color:var(--text); font-size:14px;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text2); display:block; margin-bottom:6px;">Target date</label>
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
|
|
<select id="deadline-month"
|
|
style="padding:9px 12px; background:var(--bg3); border:1px solid var(--border);
|
|
border-radius:8px; color:var(--text); font-size:14px;">
|
|
<option value="01">January</option>
|
|
<option value="02">February</option>
|
|
<option value="03">March</option>
|
|
<option value="04">April</option>
|
|
<option value="05">May</option>
|
|
<option value="06">June</option>
|
|
<option value="07">July</option>
|
|
<option value="08">August</option>
|
|
<option value="09">September</option>
|
|
<option value="10">October</option>
|
|
<option value="11">November</option>
|
|
<option value="12">December</option>
|
|
</select>
|
|
<select id="deadline-year"
|
|
style="padding:9px 12px; background:var(--bg3); border:1px solid var(--border);
|
|
border-radius:8px; color:var(--text); font-size:14px;">
|
|
</select>
|
|
</div>
|
|
<!-- hidden field submitted to server -->
|
|
<input type="hidden" name="deadline" id="deadline-value">
|
|
</div>
|
|
|
|
<div style="display:flex; gap:10px; justify-content:flex-end; margin-top:4px;">
|
|
<button type="button" onclick="document.getElementById('new-goal-modal').style.display='none'"
|
|
class="btn btn-outline">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save goal</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const typeHints = {
|
|
once: 'A specific purchase you want to save up for.',
|
|
deposit: 'A lump sum needed for a house deposit or down-payment.',
|
|
emergency: 'A safety net covering several months of expenses.',
|
|
investment: 'A recurring investment target (e.g. ETF monthly contribution).',
|
|
};
|
|
function updateTypeHint(v) {
|
|
document.getElementById('type-hint').textContent = typeHints[v] || '';
|
|
}
|
|
|
|
// populate year dropdown: current year + 10 years ahead
|
|
(function() {
|
|
const now = new Date();
|
|
const yearSel = document.getElementById('deadline-year');
|
|
const monthSel = document.getElementById('deadline-month');
|
|
for (let y = now.getFullYear(); y <= now.getFullYear() + 10; y++) {
|
|
const opt = document.createElement('option');
|
|
opt.value = String(y);
|
|
opt.textContent = String(y);
|
|
yearSel.appendChild(opt);
|
|
}
|
|
// default month to next month
|
|
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
monthSel.value = String(nextMonth.getMonth() + 1).padStart(2, '0');
|
|
yearSel.value = String(nextMonth.getFullYear());
|
|
})();
|
|
|
|
// wire hidden field before submit
|
|
document.querySelector('#new-goal-modal form').addEventListener('submit', function() {
|
|
const m = document.getElementById('deadline-month').value;
|
|
const y = document.getElementById('deadline-year').value;
|
|
document.getElementById('deadline-value').value = y + '-' + m;
|
|
});
|
|
|
|
// close modal on backdrop click
|
|
document.getElementById('new-goal-modal').addEventListener('click', function(e) {
|
|
if (e.target === this) this.style.display = 'none';
|
|
});
|
|
</script>
|
|
{{end}}
|