feat(i18n): add English localization with per-user language preference
This commit is contained in:
@@ -33,6 +33,14 @@ _translations: dict[str, dict[str, str]] = {
|
|||||||
"dash.countries": {"zh": "国家/地区", "en": "Countries"},
|
"dash.countries": {"zh": "国家/地区", "en": "Countries"},
|
||||||
"dash.chart_sent": {"zh": "📤 寄出", "en": "📤 Sent"},
|
"dash.chart_sent": {"zh": "📤 寄出", "en": "📤 Sent"},
|
||||||
"dash.chart_received": {"zh": "📬 收到", "en": "📬 Received"},
|
"dash.chart_received": {"zh": "📬 收到", "en": "📬 Received"},
|
||||||
|
"dash.recent": {"zh": "最近动态", "en": "Recent activity"},
|
||||||
|
"dash.empty": {"zh": "暂无明信片", "en": "No postcards yet"},
|
||||||
|
"dash.welcome_title": {"zh": "欢迎使用星笺!", "en": "Welcome to Mailova!"},
|
||||||
|
"dash.welcome_hint": {"zh": "创建一个", "en": "Create a"},
|
||||||
|
"dash.welcome_hint2": {"zh": " 开始管理你的明信片", "en": " to start managing your postcards"},
|
||||||
|
"dash.today": {"zh": "今天", "en": "Today"},
|
||||||
|
"dash.yesterday": {"zh": "昨天", "en": "Yesterday"},
|
||||||
|
"dash.days_ago": {"zh": "天前", "en": " days ago"},
|
||||||
|
|
||||||
# ── Profiles ──
|
# ── Profiles ──
|
||||||
"profiles.title": {"zh": "Profile", "en": "Profiles"},
|
"profiles.title": {"zh": "Profile", "en": "Profiles"},
|
||||||
@@ -159,6 +167,7 @@ _translations: dict[str, dict[str, str]] = {
|
|||||||
"invites.max_uses": {"zh": "最大使用次数", "en": "Max uses"},
|
"invites.max_uses": {"zh": "最大使用次数", "en": "Max uses"},
|
||||||
"invites.expires_time": {"zh": "过期时间", "en": "Expiration"},
|
"invites.expires_time": {"zh": "过期时间", "en": "Expiration"},
|
||||||
"invites.registered_users": {"zh": "注册用户", "en": "Registered users"},
|
"invites.registered_users": {"zh": "注册用户", "en": "Registered users"},
|
||||||
|
"invites.empty": {"zh": "暂无邀请码", "en": "No invite codes yet"},
|
||||||
|
|
||||||
# ── Showcase (public) ──
|
# ── Showcase (public) ──
|
||||||
"showcase.title": {"zh": " 的明信片", "en": "'s postcards"},
|
"showcase.title": {"zh": " 的明信片", "en": "'s postcards"},
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">🔑</div>
|
<div class="empty-icon">🔑</div>
|
||||||
<p>{{ 'invites.title'|t(user.language) }}</p>
|
<p>{{ 'invites.empty'|t(user.language) }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -76,45 +76,97 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
{% if sent_country_stats %}
|
||||||
var labels = {{ sent_country_stats|map(attribute=0)|list|tojson }};
|
new Chart(document.getElementById('chart-sent'), {
|
||||||
var data = {{ sent_country_stats|map(attribute=1)|list|tojson }};
|
|
||||||
if (labels.length) {
|
|
||||||
new Chart(document.getElementById('chart-sent'), {
|
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: { labels: labels, datasets: [{ data: data, backgroundColor: '#f59e0b', borderRadius: 4 }] },
|
data: {
|
||||||
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
|
labels: [{% for code, _ in sent_country_stats %}'{{ code|flag }} {{ code }}',{% endfor %}],
|
||||||
});
|
datasets: [{ data: [{% for _, count in sent_country_stats %}{{ count }},{% endfor %}], backgroundColor: '#3b82f6', borderRadius: 4, maxBarThickness: 40 }]
|
||||||
}
|
},
|
||||||
var rlabels = {{ received_country_stats|map(attribute=0)|list|tojson }};
|
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } }, x: { grid: { display: false } } } }
|
||||||
var rdata = {{ received_country_stats|map(attribute=1)|list|tojson }};
|
});
|
||||||
if (rlabels.length) {
|
{% endif %}
|
||||||
new Chart(document.getElementById('chart-received'), {
|
{% if received_country_stats %}
|
||||||
|
new Chart(document.getElementById('chart-received'), {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: { labels: rlabels, datasets: [{ data: rdata, backgroundColor: '#8b5cf6', borderRadius: 4 }] },
|
data: {
|
||||||
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
|
labels: [{% for code, _ in received_country_stats %}'{{ code|flag }} {{ code }}',{% endfor %}],
|
||||||
});
|
datasets: [{ data: [{% for _, count in received_country_stats %}{{ count }},{% endfor %}], backgroundColor: '#8b5cf6', borderRadius: 4, maxBarThickness: 40 }]
|
||||||
}
|
},
|
||||||
})();
|
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } }, x: { grid: { display: false } } } }
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if profiles %}
|
|
||||||
<div class="dash-sections">
|
<div class="dash-sections">
|
||||||
{% for p in profiles %}
|
<div class="dash-section">
|
||||||
<a href="/profiles/{{ p.id }}/postcards" class="card dashboard-card">
|
<h2>{{ 'dash.recent'|t(user.language) }}</h2>
|
||||||
<h3>{{ p.nickname }}</h3>
|
{% if recent %}
|
||||||
<div class="stats">
|
<div class="recent-list">
|
||||||
<span class="stat">{{ p.postcards|length }} {{ 'profiles.total'|t(user.language) }}</span>
|
{% for pc in recent %}
|
||||||
|
<a href="/postcards/{{ pc.id }}" class="recent-item">
|
||||||
|
<div class="recent-img">
|
||||||
|
{% if pc.image_front %}
|
||||||
|
<img src="{{ pc.image_front }}" alt="" loading="lazy">
|
||||||
|
{% else %}
|
||||||
|
<span>📷</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="recent-info">
|
||||||
|
<strong>{{ pc.card_number }}</strong>
|
||||||
|
<span class="badge badge-{{ pc.status }}">{{ {'sent':'已寄出','delivered':'已送达','received':'已收到'}.get(pc.status) if user.language=='zh' else {'sent':'Sent','delivered':'Delivered','received':'Received'}.get(pc.status) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="recent-meta">
|
||||||
|
{% if pc.status in ('sent','delivered') %}
|
||||||
|
<span>{{ pc.country|flag }} {{ pc.country or '' }}</span>
|
||||||
|
<span>→ {{ pc.recipient_name or '-' }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span>← {{ pc.sender_name or '-' }}</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="recent-time">
|
||||||
|
{% if pc.status == 'sent' and pc.send_time %}
|
||||||
|
{% set delta = (now.date() - pc.send_time.date()).days %}
|
||||||
|
{% if delta == 0 %}{{ 'dash.today'|t(user.language) }}{% elif delta == 1 %}{{ 'dash.yesterday'|t(user.language) }}{% else %}{{ delta }}{{ 'dash.days_ago'|t(user.language) }}{% endif %}
|
||||||
|
{% elif pc.status == 'delivered' and pc.arrival_time %}
|
||||||
|
{% set delta = (now.date() - pc.arrival_time.date()).days %}
|
||||||
|
{% if delta == 0 %}{{ 'dash.today'|t(user.language) }}{% elif delta == 1 %}{{ 'dash.yesterday'|t(user.language) }}{% else %}{{ delta }}{{ 'dash.days_ago'|t(user.language) }}{% endif %}
|
||||||
|
{% elif pc.status == 'received' and pc.receive_time %}
|
||||||
|
{% set delta = (now.date() - pc.receive_time.date()).days %}
|
||||||
|
{% if delta == 0 %}{{ 'dash.today'|t(user.language) }}{% elif delta == 1 %}{{ 'dash.yesterday'|t(user.language) }}{% else %}{{ delta }}{{ 'dash.days_ago'|t(user.language) }}{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="empty">{{ 'dash.empty'|t(user.language) }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dash-section">
|
||||||
|
<h2>Profile</h2>
|
||||||
|
<div class="dash-profiles">
|
||||||
|
{% for p in profiles %}
|
||||||
|
<a href="/profiles/{{ p.id }}/postcards" class="dash-profile-link">
|
||||||
|
<span class="dash-profile-name">{{ p.nickname }}</span>
|
||||||
|
<span class="dash-profile-arrow">→</span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
<a href="/profiles" class="dash-profile-link dash-profile-more">
|
||||||
|
<span class="dash-profile-name">{{ 'profiles.new_btn'|t(user.language) }}</span>
|
||||||
|
<span class="dash-profile-arrow">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>{{ 'dash.welcome'|t(user.language) }}{{ user.username }} 👋</p>
|
<div class="empty-icon">📮</div>
|
||||||
|
<p>{{ 'dash.welcome_title'|t(user.language) }}</p>
|
||||||
|
<p class="empty-sub">{{ 'dash.welcome_hint'|t(user.language) }} <a href="/profiles">{{ 'nav.profiles'|t(user.language) }}</a></p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ function toggleFields() {
|
|||||||
country.required = false;
|
country.required = false;
|
||||||
sendTime.required = false;
|
sendTime.required = false;
|
||||||
arrival.required = false;
|
arrival.required = false;
|
||||||
|
arrival.disabled = false;
|
||||||
sender.required = true;
|
sender.required = true;
|
||||||
receive.required = true;
|
receive.required = true;
|
||||||
} else if (s === 'sent') {
|
} else if (s === 'sent') {
|
||||||
@@ -71,6 +72,7 @@ function toggleFields() {
|
|||||||
arrival.required = false;
|
arrival.required = false;
|
||||||
arrival.disabled = true;
|
arrival.disabled = true;
|
||||||
arrival.value = '';
|
arrival.value = '';
|
||||||
|
labelArrival.textContent = '到达时间';
|
||||||
sender.required = false;
|
sender.required = false;
|
||||||
receive.required = false;
|
receive.required = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -81,19 +83,23 @@ function toggleFields() {
|
|||||||
sendTime.required = true;
|
sendTime.required = true;
|
||||||
arrival.disabled = false;
|
arrival.disabled = false;
|
||||||
arrival.required = true;
|
arrival.required = true;
|
||||||
|
labelArrival.textContent = '到达时间 *';
|
||||||
sender.required = false;
|
sender.required = false;
|
||||||
receive.required = false;
|
receive.required = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toggleFields();
|
toggleFields();
|
||||||
{% if not postcard %}
|
{% if not postcard %}
|
||||||
|
// Auto-fill date fields with today on new postcards
|
||||||
(function() {
|
(function() {
|
||||||
var today = new Date().toISOString().slice(0, 10);
|
var today = new Date().toISOString().slice(0, 10);
|
||||||
var sendTime = document.getElementById('input-send-time');
|
var sendTime = document.getElementById('input-send-time');
|
||||||
var receiveTime = document.getElementById('input-receive');
|
var receiveTime = document.getElementById('input-receive');
|
||||||
var statusSel = document.getElementById('status-select');
|
var statusSel = document.getElementById('status-select');
|
||||||
|
// Fill on page load
|
||||||
if (statusSel.value === 'sent' && !sendTime.value) sendTime.value = today;
|
if (statusSel.value === 'sent' && !sendTime.value) sendTime.value = today;
|
||||||
if (statusSel.value === 'received' && !receiveTime.value) receiveTime.value = today;
|
if (statusSel.value === 'received' && !receiveTime.value) receiveTime.value = today;
|
||||||
|
// Fill on status change
|
||||||
statusSel.addEventListener('change', function() {
|
statusSel.addEventListener('change', function() {
|
||||||
if (this.value === 'sent' && !sendTime.value) sendTime.value = today;
|
if (this.value === 'sent' && !sendTime.value) sendTime.value = today;
|
||||||
if (this.value === 'received' && !receiveTime.value) receiveTime.value = today;
|
if (this.value === 'received' && !receiveTime.value) receiveTime.value = today;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% block title %}{{ 'nav.profiles'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
{% block title %}{{ 'nav.profiles'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h1>{{ 'nav.profiles'|t(user.language) }}</h1>
|
<h1>Profile</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-form">
|
<div class="inline-form">
|
||||||
<form method="post" action="/profiles" class="form-row">
|
<form method="post" action="/profiles" class="form-row">
|
||||||
@@ -56,27 +56,34 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="profile-card-footer">
|
<div class="profile-card-footer">
|
||||||
<span class="profile-card-date">{{ 'profiles.created_at'|t(user.language) }} {{ p.created_at.strftime('%Y-%m-%d') }}</span>
|
<span class="profile-card-date">{{ 'profiles.created_at'|t(user.language) }} {{ p.created_at.strftime('%Y-%m-%d') }}</span>
|
||||||
<a href="/profiles/{{ p.id }}/postcards" class="btn btn-sm">{{ 'profiles.view'|t(user.language) }}</a>
|
<div class="profile-card-actions">
|
||||||
|
<a href="/profiles/{{ p.id }}/postcards" class="btn btn-primary btn-sm">{{ 'profiles.view'|t(user.language) }}</a>
|
||||||
<form method="post" action="/profiles/{{ p.id }}/delete" class="inline">
|
<form method="post" action="/profiles/{{ p.id }}/delete" class="inline">
|
||||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('{{ 'profiles.confirm_delete'|t(user.language) }}')">{{ 'profiles.delete'|t(user.language) }}</button>
|
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('{{ 'profiles.confirm_delete'|t(user.language) }}')">{{ 'profiles.delete'|t(user.language) }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="empty">{{ 'profiles.new_ph'|t(user.language) }}</p>
|
<div class="empty-state">
|
||||||
|
<div class="empty-icon">📮</div>
|
||||||
|
<p>还没有 Profile</p>
|
||||||
|
<p class="empty-sub">{{ 'profiles.new_ph'|t(user.language) }}</p>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function startRename(id) {
|
function startRename(id) {
|
||||||
document.getElementById('name-display-' + id).style.display = 'none';
|
document.getElementById('name-display-' + id).style.display = 'none';
|
||||||
document.getElementById('name-edit-' + id).style.display = '';
|
var form = document.getElementById('name-edit-' + id);
|
||||||
document.getElementById('name-edit-' + id).querySelector('input').focus();
|
form.style.display = 'inline-flex';
|
||||||
|
form.querySelector('input').focus();
|
||||||
}
|
}
|
||||||
function cancelRename(id) {
|
function cancelRename(id) {
|
||||||
|
var form = document.getElementById('name-edit-' + id);
|
||||||
|
form.style.display = 'none';
|
||||||
document.getElementById('name-display-' + id).style.display = '';
|
document.getElementById('name-display-' + id).style.display = '';
|
||||||
document.getElementById('name-edit-' + id).style.display = 'none';
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -76,37 +76,83 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if profiles %}
|
|
||||||
<div style="margin-top:1.5rem">
|
<div style="margin-top:1.5rem">
|
||||||
<h2 style="font-size:1rem;margin-bottom:1rem">{{ 'settings.profiles_showcase'|t(user.language) }}</h2>
|
<h2 style="font-size:1rem;margin-bottom:1rem">{{ 'settings.profiles_showcase'|t(user.language) }}</h2>
|
||||||
|
{% if profiles %}
|
||||||
{% for p in profiles %}
|
{% for p in profiles %}
|
||||||
<div class="form-card" style="margin-bottom:.75rem">
|
{% set sent_cards = [] %}
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem">
|
{% 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>
|
<strong>{{ p.nickname }}</strong>
|
||||||
<span class="badge">{% if p.showcase_enabled %}{{ 'settings.showcase_on'|t(user.language) }}{% else %}{{ 'settings.showcase_off'|t(user.language) }}{% endif %}</span>
|
<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>
|
</div>
|
||||||
<form method="post" action="/profiles/{{ p.id }}/showcase" style="display:flex;flex-wrap:wrap;gap:.75rem;align-items:flex-end">
|
<form method="post" action="/profiles/{{ p.id }}/showcase">
|
||||||
<div style="display:flex;flex-direction:column;gap:.25rem">
|
<div style="display:flex;flex-wrap:wrap;gap:1rem;align-items:center">
|
||||||
<label style="margin:0;font-size:.78rem">{{ 'settings.enable_showcase'|t(user.language) }}</label>
|
<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">
|
<label class="toggle-switch">
|
||||||
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
|
<input type="checkbox" name="showcase_enabled" value="1" {% if p.showcase_enabled %}checked{% endif %} onchange="this.form.submit()">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;flex-direction:column;gap:.25rem">
|
<div style="display:flex;align-items:center;gap:.5rem">
|
||||||
<label style="margin:0;font-size:.78rem">{{ 'settings.showcase_content'|t(user.language) }}</label>
|
<label style="margin:0;font-size:.85rem">{{ 'settings.showcase_content'|t(user.language) }}</label>
|
||||||
<select name="showcase_visible" onchange="this.form.submit()">
|
<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="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="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>
|
<option value="received" {% if p.showcase_visible == 'received' %}selected{% endif %}>{{ 'settings.content_received'|t(user.language) }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<a href="/profiles/{{ p.id }}/showcase" class="btn btn-sm">{{ 'settings.save'|t(user.language) }}</a>
|
</div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>{{ 'profiles.new_ph'|t(user.language) }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -116,8 +162,5 @@ function switchTab(name) {
|
|||||||
event.currentTarget.classList.add('active');
|
event.currentTarget.classList.add('active');
|
||||||
document.getElementById('tab-' + name).style.display = '';
|
document.getElementById('tab-' + name).style.display = '';
|
||||||
}
|
}
|
||||||
{% if active_tab == 'showcase' %}
|
|
||||||
document.querySelector('[onclick*="showcase"]').click();
|
|
||||||
{% endif %}
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set lang = lang if lang is defined else 'zh' %}
|
|
||||||
{% block title %}{{ profile_user.username }} {{ 'showcase.title'|t(lang) }}{% endblock %}
|
{% block title %}{{ profile_user.username }} {{ 'showcase.title'|t(lang) }}{% endblock %}
|
||||||
{% block nav %}{% endblock %}
|
{% block nav %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@@ -114,47 +113,76 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Lightbox -->
|
<div id="lightbox" class="lightbox" onclick="closeLightbox(event)">
|
||||||
<div id="lightbox" class="lightbox" onclick="closeLightbox()">
|
<button class="lightbox-close" onclick="closeLightbox(event)">×</button>
|
||||||
<button class="lightbox-close" onclick="closeLightbox()">✕</button>
|
<button class="lightbox-prev" onclick="navLightbox(-1,event)">❮</button>
|
||||||
<button class="lightbox-prev" onclick="event.stopPropagation();navLightbox(-1)">‹</button>
|
<button class="lightbox-next" onclick="navLightbox(1,event)">❯</button>
|
||||||
<button class="lightbox-next" onclick="event.stopPropagation();navLightbox(1)">›</button>
|
|
||||||
<div class="lightbox-content" onclick="event.stopPropagation()">
|
<div class="lightbox-content" onclick="event.stopPropagation()">
|
||||||
<img id="lightbox-img" src="" alt="">
|
<img id="lightbox-img" src="" alt="">
|
||||||
<div id="lightbox-number" class="lightbox-number"></div>
|
<div id="lightbox-caption" class="lightbox-caption"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var cards = [];
|
|
||||||
document.querySelectorAll('.showcase-card').forEach(function(el) {
|
|
||||||
cards.push({ front: el.dataset.front, number: el.dataset.number });
|
|
||||||
});
|
|
||||||
var currentIndex = 0;
|
|
||||||
|
|
||||||
function openLightbox(el) {
|
|
||||||
currentIndex = cards.findIndex(function(c) { return c.number === el.dataset.number; });
|
|
||||||
showLightbox();
|
|
||||||
document.getElementById('lightbox').style.display = 'flex';
|
|
||||||
}
|
|
||||||
function showLightbox() {
|
|
||||||
var c = cards[currentIndex];
|
|
||||||
document.getElementById('lightbox-img').src = c.front;
|
|
||||||
document.getElementById('lightbox-number').textContent = c.number;
|
|
||||||
}
|
|
||||||
function closeLightbox() { document.getElementById('lightbox').style.display = 'none'; }
|
|
||||||
function navLightbox(dir) { currentIndex = (currentIndex + dir + cards.length) % cards.length; showLightbox(); }
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (document.getElementById('lightbox').style.display !== 'flex') return;
|
|
||||||
if (e.key === 'Escape') closeLightbox();
|
|
||||||
if (e.key === 'ArrowLeft') navLightbox(-1);
|
|
||||||
if (e.key === 'ArrowRight') navLightbox(1);
|
|
||||||
});
|
|
||||||
function switchTab(name) {
|
function switchTab(name) {
|
||||||
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
|
||||||
document.querySelectorAll('.tab-panel').forEach(function(p) { p.style.display = 'none'; });
|
document.querySelectorAll('.tab-panel').forEach(function(p) { p.style.display = 'none'; });
|
||||||
event.currentTarget.classList.add('active');
|
event.currentTarget.classList.add('active');
|
||||||
document.getElementById('tab-' + name).style.display = '';
|
document.getElementById('tab-' + name).style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lightboxItems = [];
|
||||||
|
var lightboxIdx = 0;
|
||||||
|
|
||||||
|
function buildLightboxItems() {
|
||||||
|
lightboxItems = [];
|
||||||
|
var visible = document.querySelectorAll('.tab-panel:not([style*="display:none"]) .showcase-card[data-front]');
|
||||||
|
visible.forEach(function(card) {
|
||||||
|
var front = card.getAttribute('data-front');
|
||||||
|
var number = card.getAttribute('data-number');
|
||||||
|
if (front) lightboxItems.push({src: front, number: number});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLightbox(el) {
|
||||||
|
buildLightboxItems();
|
||||||
|
var front = el.getAttribute('data-front');
|
||||||
|
if (!front) return;
|
||||||
|
var number = el.getAttribute('data-number');
|
||||||
|
for (var i = 0; i < lightboxItems.length; i++) {
|
||||||
|
if (lightboxItems[i].number === number) { lightboxIdx = i; break; }
|
||||||
|
}
|
||||||
|
showLightboxItem();
|
||||||
|
document.getElementById('lightbox').classList.add('active');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLightboxItem() {
|
||||||
|
if (!lightboxItems.length) return;
|
||||||
|
var item = lightboxItems[lightboxIdx];
|
||||||
|
document.getElementById('lightbox-img').src = item.src;
|
||||||
|
document.getElementById('lightbox-caption').textContent = item.number + ' (' + (lightboxIdx+1) + '/' + lightboxItems.length + ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLightbox(e) {
|
||||||
|
if (e) e.stopPropagation();
|
||||||
|
document.getElementById('lightbox').classList.remove('active');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function navLightbox(dir, e) {
|
||||||
|
if (e) e.stopPropagation();
|
||||||
|
if (!lightboxItems.length) return;
|
||||||
|
lightboxIdx = (lightboxIdx + dir + lightboxItems.length) % lightboxItems.length;
|
||||||
|
showLightboxItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
var lb = document.getElementById('lightbox');
|
||||||
|
if (!lb || !lb.classList.contains('active')) return;
|
||||||
|
if (e.key === 'Escape') closeLightbox();
|
||||||
|
else if (e.key === 'ArrowLeft') navLightbox(-1);
|
||||||
|
else if (e.key === 'ArrowRight') navLightbox(1);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% if p.showcase_enabled and pc_list %}
|
{% if p.showcase_enabled and pc_list %}
|
||||||
<div style="margin-top:.75rem;border-top:1px solid var(--border);padding-top:.75rem">
|
<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>
|
<p style="font-size:.8rem;color:var(--text-muted);margin-bottom:.5rem">单张明信片展示控制(关闭展示后可单独隐藏):</p>
|
||||||
{% for pc in pc_list %}
|
{% for pc in pc_list %}
|
||||||
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.3rem;margin-right:.75rem;margin-bottom:.3rem;font-size:.8rem">
|
<form method="post" action="/postcards/{{ pc.id }}/showcase-toggle" style="display:inline-flex;align-items:center;gap:.3rem;margin-right:.75rem;margin-bottom:.3rem;font-size:.8rem">
|
||||||
<span>{{ pc.card_number }}</span>
|
<span>{{ pc.card_number }}</span>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<input type="checkbox" name="showcase_hidden" value="1" {% if pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()">
|
<input type="checkbox" name="showcase_hidden" value="1" {% if pc.showcase_hidden %}checked{% endif %} onchange="this.form.submit()">
|
||||||
<span class="toggle-slider"></span>
|
<span class="toggle-slider"></span>
|
||||||
</label>
|
</label>
|
||||||
{% if pc.showcase_hidden %}<span style="color:var(--text-muted);font-size:.75rem">{{ 'profile_showcase.hidden'|t(user.language) }}</span>{% endif %}
|
{% if pc.showcase_hidden %}<span style="color:var(--text-muted);font-size:.75rem">隐藏</span>{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user