292 lines
16 KiB
HTML
292 lines
16 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ 'settings.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||
{% block content %}
|
||
<div class="section-header">
|
||
<h1>{{ 'settings.title'|t(user.language) }}</h1>
|
||
</div>
|
||
|
||
<div class="tabs" id="tabs">
|
||
<button class="tab {% if active_tab != 'showcase' and active_tab != 'apikeys' %}active{% endif %}" onclick="switchTab('account')">{{ 'settings.tab_account'|t(user.language) }}</button>
|
||
<button class="tab {% if active_tab == 'showcase' %}active{% endif %}" onclick="switchTab('showcase')">{{ 'settings.tab_showcase'|t(user.language) }}</button>
|
||
<button class="tab {% if active_tab == 'apikeys' %}active{% endif %}" onclick="switchTab('apikeys')">{{ 'settings.tab_apikeys'|t(user.language) }}</button>
|
||
</div>
|
||
|
||
{% if success %}
|
||
<div class="alert alert-success">{{ success }}</div>
|
||
{% endif %}
|
||
{% if error %}
|
||
<div class="alert alert-error">{{ error }}</div>
|
||
{% endif %}
|
||
|
||
<div id="tab-account" class="tab-panel" {% if active_tab == 'showcase' or active_tab == 'apikeys' %}style="display:none"{% endif %}>
|
||
<div class="form-card">
|
||
<h3 style="margin-bottom:.75rem">{{ 'settings.change_username'|t(user.language) }}</h3>
|
||
<form method="post" action="/settings/change-username">
|
||
<label>{{ 'settings.current_username'|t(user.language) }}</label>
|
||
<input type="text" value="{{ user.username }}" disabled>
|
||
<label>{{ 'settings.new_username'|t(user.language) }}</label>
|
||
<input type="text" name="new_username" required maxlength="64">
|
||
<div class="form-actions">
|
||
<button type="submit" class="btn btn-primary">{{ 'settings.change_username'|t(user.language) }}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="form-card" style="margin-top:1.5rem">
|
||
<h3 style="margin-bottom:.75rem">{{ 'settings.change_password'|t(user.language) }}</h3>
|
||
<form method="post" action="/settings/change-password">
|
||
<label>{{ 'settings.current_password'|t(user.language) }}</label>
|
||
<input type="password" name="old_password" required>
|
||
<label>{{ 'settings.new_password'|t(user.language) }}</label>
|
||
<input type="password" name="new_password" required>
|
||
<label>{{ 'settings.confirm_password'|t(user.language) }}</label>
|
||
<input type="password" name="confirm_password" required>
|
||
<div class="form-actions">
|
||
<button type="submit" class="btn btn-primary">{{ 'settings.change_password'|t(user.language) }}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="form-card" style="margin-top:1.5rem">
|
||
<h3 style="margin-bottom:.75rem">{{ 'settings.language'|t(user.language) }}</h3>
|
||
<form method="post" action="/settings/language" style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap">
|
||
<select name="language" style="width:auto">
|
||
{% for code, name in available_languages.items() %}
|
||
<option value="{{ code }}" {% if user.language == code %}selected{% endif %}>{{ name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<button type="submit" class="btn btn-primary btn-sm">{{ 'settings.save'|t(user.language) }}</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tab-showcase" class="tab-panel" {% if active_tab != 'showcase' %}style="display:none"{% endif %}>
|
||
<div class="form-card">
|
||
<h3 style="margin-bottom:.75rem">{{ 'settings.showcase_global'|t(user.language) }}</h3>
|
||
<form method="post" action="/showcase/manage">
|
||
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||
<label style="margin:0;white-space:nowrap">{{ 'settings.showcase_mode'|t(user.language) }}</label>
|
||
<select name="showcase_mode" style="width:auto">
|
||
<option value="profile" {% if user.showcase_mode == 'profile' %}selected{% endif %}>{{ 'settings.mode_profile'|t(user.language) }}</option>
|
||
<option value="flat" {% if user.showcase_mode == 'flat' %}selected{% endif %}>{{ 'settings.mode_flat'|t(user.language) }}</option>
|
||
</select>
|
||
<button type="submit" class="btn btn-primary btn-sm">{{ 'settings.save'|t(user.language) }}</button>
|
||
</div>
|
||
</form>
|
||
<p style="margin-top:.75rem;font-size:.8rem;color:var(--text-muted)">
|
||
{{ 'settings.public_url_label'|t(user.language) }}<a href="/u/{{ user.username }}" target="_blank">/u/{{ user.username }}</a>
|
||
</p>
|
||
</div>
|
||
|
||
<div style="margin-top:1.5rem">
|
||
<h2 style="font-size:1rem;margin-bottom:1rem">{{ 'settings.profiles_showcase'|t(user.language) }}</h2>
|
||
{% if profiles %}
|
||
{% for p in profiles %}
|
||
{% set sent_cards = [] %}
|
||
{% set received_cards = [] %}
|
||
{% if profile_cards and p.id in profile_cards %}
|
||
{% for pc in profile_cards[p.id] %}
|
||
{% if pc.status in ('sent', 'delivered') %}
|
||
{% if sent_cards.append(pc) %}{% endif %}
|
||
{% else %}
|
||
{% if received_cards.append(pc) %}{% endif %}
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% endif %}
|
||
<div class="showcase-profile-card" style="background:var(--card-bg);border:1px solid var(--border);border-radius:10px;padding:1rem;margin-bottom:.75rem">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem">
|
||
<strong>{{ p.nickname }}</strong>
|
||
<span class="badge" style="{% if p.showcase_enabled %}background:var(--sent){% else %}background:var(--text-muted){% endif %}">{% if p.showcase_enabled %}{{ 'settings.showcase_on'|t(user.language) }}{% else %}{{ 'settings.showcase_off'|t(user.language) }}{% endif %}</span>
|
||
</div>
|
||
<form method="post" action="/profiles/{{ p.id }}/showcase">
|
||
<div style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center">
|
||
<div style="display:flex;align-items:center;gap:.5rem">
|
||
<label style="margin:0;font-size:.85rem">{{ 'settings.enable_showcase'|t(user.language) }}</label>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:.5rem">
|
||
<label style="margin:0;font-size:.85rem">{{ 'settings.showcase_content'|t(user.language) }}</label>
|
||
<select name="showcase_visible" onchange="this.form.submit()" style="width:auto">
|
||
<option value="both" {% if p.showcase_visible == 'both' %}selected{% endif %}>{{ 'settings.content_both'|t(user.language) }}</option>
|
||
<option value="sent" {% if p.showcase_visible == 'sent' %}selected{% endif %}>{{ 'settings.content_sent'|t(user.language) }}</option>
|
||
<option value="received" {% if p.showcase_visible == 'received' %}selected{% endif %}>{{ 'settings.content_received'|t(user.language) }}</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
{% if p.showcase_enabled and (sent_cards or received_cards) %}
|
||
<div style="margin-top:.75rem;border-top:1px solid var(--border);padding-top:.75rem">
|
||
<p style="font-size:.8rem;color:var(--text-muted);margin-bottom:.5rem">{{ 'profile_showcase.single_control'|t(user.language) }}:</p>
|
||
{% if sent_cards %}
|
||
<div style="margin-bottom:.4rem">
|
||
<span style="font-size:.78rem;color:var(--text-muted);margin-right:.5rem">{{ 'showcase.tab_sent'|t(user.language) }}</span>
|
||
{% for pc in sent_cards %}
|
||
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.3rem;margin-right:.75rem;margin-bottom:.25rem;font-size:.85rem">
|
||
<input type="checkbox" name="showcase_visible" value="1" {% if not pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()" style="margin:0">
|
||
<span>{{ pc.card_number }}</span>
|
||
</form>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
{% if received_cards %}
|
||
<div>
|
||
<span style="font-size:.78rem;color:var(--text-muted);margin-right:.5rem">{{ 'showcase.tab_received'|t(user.language) }}</span>
|
||
{% for pc in received_cards %}
|
||
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.3rem;margin-right:.75rem;margin-bottom:.25rem;font-size:.85rem">
|
||
<input type="checkbox" name="showcase_visible" value="1" {% if not pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()" style="margin:0">
|
||
<span>{{ pc.card_number }}</span>
|
||
</form>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% elif p.showcase_enabled %}
|
||
<p style="margin-top:.75rem;font-size:.8rem;color:var(--text-muted)">{{ 'showcase.empty'|t(user.language) }}</p>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
<div class="empty-state">
|
||
<p>{{ 'profiles.new_ph'|t(user.language) }}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tab-apikeys" class="tab-panel" {% if active_tab != 'apikeys' %}style="display:none"{% endif %}>
|
||
<div class="form-card">
|
||
<h3 style="margin-bottom:.75rem">{{ 'apikey.title'|t(user.language) }}</h3>
|
||
<p style="color:var(--text-muted);font-size:.85rem;margin-bottom:1rem">{{ 'apikey.hint'|t(user.language) }}</p>
|
||
|
||
{% if new_key %}
|
||
<div class="alert alert-success" style="margin-bottom:1rem">
|
||
<strong>{{ 'apikey.created'|t(user.language) }}</strong><br>
|
||
<code style="word-break:break-all;font-size:.85rem">{{ new_key }}</code><br>
|
||
<small style="color:var(--text-muted)">{{ 'apikey.copy_now'|t(user.language) }}</small>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<form method="post" action="/settings/apikeys/create" style="margin-bottom:1rem">
|
||
<div style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-bottom:.75rem">
|
||
<input type="text" name="name" placeholder="{{ 'apikey.name_ph'|t(user.language) }}" style="max-width:200px">
|
||
<button type="submit" class="btn btn-primary btn-sm">{{ 'apikey.create'|t(user.language) }}</button>
|
||
</div>
|
||
<details>
|
||
<summary style="font-size:.85rem;color:var(--text-muted);cursor:pointer;margin-bottom:.5rem">{{ 'apikey.permissions'|t(user.language) }}</summary>
|
||
<div style="display:flex;flex-wrap:wrap;gap:.75rem 1.5rem">
|
||
{% for scope, label in [
|
||
('profiles:read', 'apikey.perm.profiles_read'),
|
||
('profiles:write', 'apikey.perm.profiles_write'),
|
||
('postcards:read', 'apikey.perm.postcards_read'),
|
||
('postcards:write', 'apikey.perm.postcards_write'),
|
||
('images:upload', 'apikey.perm.images_upload')
|
||
] %}
|
||
<label style="display:flex;align-items:center;gap:.3rem;font-size:.85rem;white-space:nowrap">
|
||
<input type="checkbox" name="permissions" value="{{ scope }}">
|
||
{{ label|t(user.language) }}
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
<p style="font-size:.78rem;color:var(--text-muted);margin-top:.5rem">{{ 'apikey.perm.hint'|t(user.language) }}</p>
|
||
</details>
|
||
</form>
|
||
|
||
{% if api_keys %}
|
||
<div style="overflow-x:auto;-webkit-overflow-scrolling:touch">
|
||
<table class="table" style="min-width:700px;white-space:nowrap">
|
||
<thead>
|
||
<tr>
|
||
<th>{{ 'apikey.name'|t(user.language) }}</th>
|
||
<th>{{ 'apikey.key'|t(user.language) }}</th>
|
||
<th>{{ 'apikey.permissions'|t(user.language) }}</th>
|
||
<th>{{ 'apikey.created_at'|t(user.language) }}</th>
|
||
<th>{{ 'apikey.last_used'|t(user.language) }}</th>
|
||
<th style="white-space:nowrap"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for ak in api_keys %}
|
||
{% set perms = ak.permissions|default('[]', true)|json_loads %}
|
||
<tr>
|
||
<td style="white-space:nowrap"><strong>{{ ak.name or '—' }}</strong></td>
|
||
<td><code style="font-size:.78rem">{{ ak.key[:8] }}****</code></td>
|
||
<td style="white-space:nowrap">
|
||
{% if not perms %}
|
||
<span class="badge" style="background:var(--sent);font-size:.75rem">{{ 'apikey.perm_full_access'|t(user.language) }}</span>
|
||
{% else %}
|
||
{% for p in perms %}
|
||
<span class="badge" style="font-size:.7rem;margin:1px">{{ p }}</span>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</td>
|
||
<td style="white-space:nowrap;font-size:.82rem">{{ ak.created_at|to_local|strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||
<td style="white-space:nowrap;font-size:.82rem">{% if ak.last_used_at %}{{ ak.last_used_at|to_local|strftime('%Y-%m-%d %H:%M:%S') }}{% else %}—{% endif %}</td>
|
||
<td style="white-space:nowrap">
|
||
<button type="button" class="btn btn-sm" onclick="toggleAkEdit({{ ak.id }})">{{ 'apikey.edit'|t(user.language) }}</button>
|
||
<form method="post" action="/settings/apikeys/{{ ak.id }}/delete" style="display:inline" onsubmit="return confirm('{{ 'apikey.confirm_delete'|t(user.language) }}')">
|
||
<button type="submit" class="btn btn-danger btn-sm">{{ 'apikey.delete'|t(user.language) }}</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% for ak in api_keys %}
|
||
{% set perms = ak.permissions|default('[]', true)|json_loads %}
|
||
<div id="ak-edit-{{ ak.id }}" style="display:none;margin-top:.5rem;background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:1rem">
|
||
<h4 style="font-size:.85rem;margin-bottom:.75rem;color:var(--text-muted)">{{ 'apikey.edit'|t(user.language) }}: {{ ak.name or '—' }}</h4>
|
||
<form method="post" action="/settings/apikeys/{{ ak.id }}/edit" style="display:flex;flex-direction:column;gap:.5rem">
|
||
<div style="display:flex;align-items:center;gap:.5rem;flex-wrap:wrap">
|
||
<label style="font-size:.85rem;white-space:nowrap">{{ 'apikey.name'|t(user.language) }}</label>
|
||
<input type="text" name="name" value="{{ ak.name }}" style="max-width:200px">
|
||
</div>
|
||
<div style="display:flex;flex-wrap:wrap;gap:.6rem 1.25rem">
|
||
{% for scope, label in [
|
||
('profiles:read', 'apikey.perm.profiles_read'),
|
||
('profiles:write', 'apikey.perm.profiles_write'),
|
||
('postcards:read', 'apikey.perm.postcards_read'),
|
||
('postcards:write', 'apikey.perm.postcards_write'),
|
||
('images:upload', 'apikey.perm.images_upload')
|
||
] %}
|
||
<label style="display:flex;align-items:center;gap:.3rem;font-size:.85rem;white-space:nowrap">
|
||
<input type="checkbox" name="permissions" value="{{ scope }}" {% if scope in perms %}checked{% endif %}>
|
||
{{ label|t(user.language) }}
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
<div style="display:flex;gap:.5rem;margin-top:.25rem">
|
||
<button type="submit" class="btn btn-primary btn-sm">{{ 'apikey.save'|t(user.language) }}</button>
|
||
<button type="button" class="btn btn-sm" onclick="toggleAkEdit({{ ak.id }})">{{ 'apikey.cancel'|t(user.language) }}</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
{% else %}
|
||
<div class="empty-state">
|
||
<p>{{ 'apikey.empty'|t(user.language) }}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function switchTab(name) {
|
||
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
||
document.querySelectorAll('.tab-panel').forEach(function(p) { p.style.display = 'none'; });
|
||
event.currentTarget.classList.add('active');
|
||
document.getElementById('tab-' + name).style.display = '';
|
||
}
|
||
function toggleAkEdit(id) {
|
||
var el = document.getElementById('ak-edit-' + id);
|
||
if (el) el.style.display = el.style.display === 'none' ? '' : 'none';
|
||
}
|
||
</script>
|
||
{% endblock %}
|