Adds a full translation layer (English + European Portuguese) using
BurntSushi/toml with go:embed. Locale detection reads the lang cookie,
falls back to Accept-Language, then defaults to "en". A language switcher
in the nav writes the cookie and redirects back. All 20 personal finance
templates now use {{.T.Get "key"}} for every UI string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
193 lines
9.5 KiB
HTML
193 lines
9.5 KiB
HTML
{{template "base" .}}
|
|
|
|
{{define "content"}}
|
|
{{$d := .}}
|
|
<style>
|
|
.tab-bar { display:flex; gap:4px; border-bottom:2px solid var(--border); margin-bottom:24px; }
|
|
.tab-bar a { padding:8px 18px; font-size:0.9rem; font-weight:600; color:var(--muted); text-decoration:none; border-bottom:2px solid transparent; margin-bottom:-2px; transition:all 0.15s; }
|
|
.tab-bar a.active { color:var(--accent); border-bottom-color:var(--accent); }
|
|
.tab-bar a:hover:not(.active) { color:var(--text); }
|
|
.s-card { background:var(--card); border:1px solid var(--border); border-radius:12px; padding:22px 24px; margin-bottom:16px; }
|
|
.s-card h3 { margin:0 0 14px; font-size:0.95rem; font-weight:700; }
|
|
.form-row { display:flex; gap:10px; flex-wrap:wrap; }
|
|
.form-row input, .form-row select { flex:1; min-width:120px; padding:8px 12px; border:1px solid var(--border); border-radius:8px; background:var(--bg); color:var(--text); font-size:0.875rem; }
|
|
.btn { padding:8px 18px; border:none; border-radius:8px; font-size:0.875rem; font-weight:600; cursor:pointer; }
|
|
.btn-primary { background:var(--accent); color:#fff; }
|
|
.btn-danger { background:transparent; border:1px solid #f4433660; color:#f44336; font-size:0.8rem; padding:4px 10px; border-radius:6px; }
|
|
.btn-danger:hover { background:#f4433615; }
|
|
table { width:100%; border-collapse:collapse; font-size:0.875rem; }
|
|
th { text-align:left; padding:8px 12px; color:var(--muted); font-weight:600; border-bottom:2px solid var(--border); }
|
|
td { padding:10px 12px; border-bottom:1px solid var(--border); vertical-align:middle; }
|
|
tr:last-child td { border-bottom:none; }
|
|
.color-dot { display:inline-block; width:12px; height:12px; border-radius:50%; margin-right:6px; vertical-align:middle; }
|
|
.type-badge { display:inline-block; padding:2px 8px; border-radius:20px; font-size:0.72rem; font-weight:600; background:var(--bg); border:1px solid var(--border); color:var(--muted); }
|
|
.empty-state { padding:32px; text-align:center; color:var(--muted); font-size:0.875rem; }
|
|
</style>
|
|
|
|
<h1 style="margin:0 0 20px;">{{$d.T.Get "settings.title"}}</h1>
|
|
|
|
<div class="tab-bar">
|
|
<a href="/settings?tab=accounts" class="{{if eq $d.Tab "accounts"}}active{{end}}">{{$d.T.Get "settings.tab_accounts"}}</a>
|
|
<a href="/settings?tab=categories" class="{{if eq $d.Tab "categories"}}active{{end}}">{{$d.T.Get "settings.tab_categories"}}</a>
|
|
</div>
|
|
|
|
{{if eq $d.Tab "accounts"}}
|
|
|
|
<div class="s-card">
|
|
<h3>{{$d.T.Get "settings.accounts.card_add_title"}}</h3>
|
|
<form method="post" action="/accounts" id="add-account-form">
|
|
<div class="form-row">
|
|
<input type="text" name="name" placeholder="{{$d.T.Get "settings.accounts.placeholder_name"}}" required>
|
|
<select name="type">
|
|
<option value="checking">{{$d.T.Get "settings.accounts.type_checking"}}</option>
|
|
<option value="savings">{{$d.T.Get "settings.accounts.type_savings"}}</option>
|
|
<option value="credit">{{$d.T.Get "settings.accounts.type_credit"}}</option>
|
|
<option value="securities">{{$d.T.Get "settings.accounts.type_securities"}}</option>
|
|
</select>
|
|
<button type="submit" class="btn btn-primary">{{$d.T.Get "settings.accounts.btn_add"}}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="s-card">
|
|
{{if $d.Accounts}}
|
|
<table>
|
|
<thead><tr><th>{{$d.T.Get "settings.accounts.col_name"}}</th><th>{{$d.T.Get "settings.accounts.col_type"}}</th><th></th></tr></thead>
|
|
<tbody>
|
|
{{range $d.Accounts}}
|
|
<tr>
|
|
<td style="font-weight:500;">{{.Name}}</td>
|
|
<td><span class="type-badge">{{.Type}}</span></td>
|
|
<td style="text-align:right;">
|
|
<button class="btn btn-danger" onclick="deleteAccount('{{.ID}}')">{{$d.T.Get "settings.accounts.btn_delete"}}</button>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{else}}
|
|
<div class="empty-state">{{$d.T.Get "settings.accounts.empty_msg"}}</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{else}}{{/* categories tab */}}
|
|
|
|
<div class="s-card">
|
|
<h3>{{$d.T.Get "settings.categories.card_add_title"}}</h3>
|
|
<form method="post" action="/categories" id="add-cat-form">
|
|
<div class="form-row">
|
|
<input type="text" name="name" placeholder="{{$d.T.Get "settings.categories.placeholder_name"}}" required>
|
|
<input type="number" name="budget_cents" placeholder="{{$d.T.Get "settings.categories.placeholder_budget"}}" min="0">
|
|
<input type="color" name="color" value="#6366f1" style="flex:0; width:44px; padding:4px; border-radius:8px; cursor:pointer;">
|
|
<button type="submit" class="btn btn-primary">{{$d.T.Get "settings.categories.btn_add"}}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="s-card">
|
|
{{if $d.Categories}}
|
|
<table>
|
|
<thead><tr><th>{{$d.T.Get "settings.categories.col_category"}}</th><th style="text-align:right;">{{$d.T.Get "settings.categories.col_budget"}}</th><th></th></tr></thead>
|
|
<tbody>
|
|
{{range $d.Categories}}
|
|
<tr id="cat-row-{{.ID}}">
|
|
<td>
|
|
<span class="color-dot" style="background:{{if .Color}}{{.Color}}{{else}}#9e9e9e{{end}};"></span>
|
|
<span id="cat-name-{{.ID}}">{{.Name}}</span>
|
|
</td>
|
|
<td style="text-align:right;">
|
|
{{if .BudgetCents}}
|
|
<span id="cat-budget-{{.ID}}">€{{cents .BudgetCents}}</span>
|
|
{{else}}
|
|
<span id="cat-budget-{{.ID}}" style="color:var(--muted);">—</span>
|
|
{{end}}
|
|
<button onclick="editCat('{{.ID}}','{{.Name}}',{{.BudgetCents}},'{{.Color}}')"
|
|
style="margin-left:8px; background:none; border:none; cursor:pointer; color:var(--muted); font-size:0.85rem;">✎</button>
|
|
</td>
|
|
<td style="text-align:right;">
|
|
<button class="btn btn-danger" onclick="deleteCat('{{.ID}}')">{{$d.T.Get "settings.categories.btn_delete"}}</button>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{else}}
|
|
<div class="empty-state">{{$d.T.Get "settings.categories.empty_msg"}}</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{end}}
|
|
|
|
<!-- Edit category modal -->
|
|
<div id="edit-cat-modal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); backdrop-filter:blur(4px); z-index:300; align-items:center; justify-content:center;">
|
|
<div class="s-card" style="width:380px; max-width:95vw; margin:0; box-shadow:var(--shadow-lg);">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
|
<span style="font-weight:700;">{{$d.T.Get "settings.categories.modal_edit.title"}}</span>
|
|
<button onclick="closeEditCat()" style="background:none; border:none; cursor:pointer; color:var(--muted); font-size:1.1rem;">✕</button>
|
|
</div>
|
|
<div style="display:flex; flex-direction:column; gap:10px;">
|
|
<input id="edit-cat-name" type="text" placeholder="{{$d.T.Get "settings.categories.modal_edit.placeholder_name"}}" style="padding:8px 12px; border:1px solid var(--border); border-radius:8px; background:var(--bg); color:var(--text);">
|
|
<input id="edit-cat-budget" type="number" placeholder="{{$d.T.Get "settings.categories.modal_edit.placeholder_budget"}}" min="0" style="padding:8px 12px; border:1px solid var(--border); border-radius:8px; background:var(--bg); color:var(--text);">
|
|
<input id="edit-cat-color" type="color" style="width:100%; height:38px; padding:4px; border:1px solid var(--border); border-radius:8px; cursor:pointer;">
|
|
</div>
|
|
<div style="display:flex; gap:8px; justify-content:flex-end; margin-top:16px;">
|
|
<button onclick="closeEditCat()" class="btn" style="background:var(--bg); border:1px solid var(--border); color:var(--text);">{{$d.T.Get "settings.categories.modal_edit.btn_cancel"}}</button>
|
|
<button onclick="saveEditCat()" class="btn btn-primary">{{$d.T.Get "settings.categories.modal_edit.btn_save"}}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const CONFIRM_DELETE_ACCOUNT = {{$d.T.Get "settings.accounts.confirm_delete" | printf "%q"}};
|
|
const CONFIRM_DELETE_CAT = {{$d.T.Get "settings.categories.confirm_delete" | printf "%q"}};
|
|
let editingCatID = null;
|
|
|
|
function deleteAccount(id) {
|
|
if (!confirm(CONFIRM_DELETE_ACCOUNT)) return;
|
|
fetch('/accounts/' + id, { method: 'DELETE' })
|
|
.then(r => { if (r.ok) location.reload(); });
|
|
}
|
|
|
|
function deleteCat(id) {
|
|
if (!confirm(CONFIRM_DELETE_CAT)) return;
|
|
fetch('/categories/' + id, { method: 'DELETE' })
|
|
.then(r => { if (r.ok) location.reload(); });
|
|
}
|
|
|
|
function editCat(id, name, budgetCents, color) {
|
|
editingCatID = id;
|
|
document.getElementById('edit-cat-name').value = name;
|
|
document.getElementById('edit-cat-budget').value = budgetCents || '';
|
|
document.getElementById('edit-cat-color').value = color || '#6366f1';
|
|
document.getElementById('edit-cat-modal').style.display = 'flex';
|
|
}
|
|
|
|
function closeEditCat() {
|
|
document.getElementById('edit-cat-modal').style.display = 'none';
|
|
editingCatID = null;
|
|
}
|
|
|
|
function saveEditCat() {
|
|
if (!editingCatID) return;
|
|
const body = new URLSearchParams({
|
|
name: document.getElementById('edit-cat-name').value,
|
|
budget_cents: document.getElementById('edit-cat-budget').value || '0',
|
|
color: document.getElementById('edit-cat-color').value,
|
|
});
|
|
fetch('/categories/' + editingCatID, { method: 'PUT', body })
|
|
.then(r => { if (r.ok) { closeEditCat(); location.reload(); } });
|
|
}
|
|
|
|
document.getElementById('add-account-form')?.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
fetch(this.action, { method: 'POST', body: new FormData(this) })
|
|
.then(r => { if (r.ok || r.redirected) location.reload(); });
|
|
});
|
|
document.getElementById('add-cat-form')?.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
fetch(this.action, { method: 'POST', body: new FormData(this) })
|
|
.then(r => { if (r.ok || r.redirected) location.reload(); });
|
|
});
|
|
</script>
|
|
{{end}}
|