feat(ui): add landing page with random public postcards for unauthenticated visitors

This commit is contained in:
2026-07-07 12:05:44 +08:00
parent ba56fc6e6b
commit bd664d23be
5 changed files with 127 additions and 3 deletions

View File

@@ -3,6 +3,10 @@
"name": "English"
},
"brand": "📮 Mailova",
"index.title": "Mailova",
"index.subtitle": "Postcards from around the world",
"index.go_login": "Login",
"index.empty": "No public postcards yet",
"nav.profiles": "Profiles",
"nav.settings": "Settings",
"nav.invites": "Invites",

View File

@@ -3,6 +3,10 @@
"name": "中文"
},
"brand": "📮 星笺",
"index.title": "星笺",
"index.subtitle": "来自世界各地的明信片",
"index.go_login": "登录",
"index.empty": "暂无公开明信片",
"nav.profiles": "Profile",
"nav.settings": "设置",
"nav.invites": "邀请码管理",

View File

@@ -1,5 +1,6 @@
from datetime import datetime, timezone, timedelta
from pathlib import Path
from random import shuffle
from uuid import uuid4
from fastapi import APIRouter, Depends, File, Form, Query, Request, UploadFile
@@ -17,12 +18,11 @@ from app.auth import (
)
from app.config import SECRET_KEY, SESSION_COOKIE_NAME, UPLOAD_DIR
from app.database import get_db
from datetime import datetime
from random import choice
from string import ascii_letters, digits
from app.models import InviteCode, Postcard, Profile, User
from app.i18n import t as _t
from app.i18n import t as _t, get_languages as _get_languages
router = APIRouter(tags=["web"])
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent / "templates"))
@@ -109,6 +109,48 @@ def _redirect(url: str) -> RedirectResponse:
return RedirectResponse(url, status_code=303)
# ---------- Root / Index ----------
@router.get("/", response_class=HTMLResponse)
def index_page(request: Request, db: Session = Depends(get_db)):
user = redirect_if_not_logged_in(request, db)
if user:
return _redirect("/dashboard")
# Collect all public postcards from all users with showcase enabled
all_postcards: list[Postcard] = []
users = db.query(User).filter(User.showcase_mode.isnot(None)).all()
for u in users:
profiles = db.query(Profile).filter(
Profile.user_id == u.id, Profile.showcase_enabled == True
).all()
for p in profiles:
cards = db.query(Postcard).filter(
Postcard.profile_id == p.id, Postcard.showcase_hidden == False
).all()
for c in cards:
visible = True
if c.status in ("sent", "delivered") and p.showcase_visible not in ("sent", "both"):
visible = False
if c.status == "received" and p.showcase_visible not in ("received", "both"):
visible = False
if visible:
all_postcards.append(c)
shuffle(all_postcards)
lang = request.query_params.get("lang")
avail = _get_languages()
if not lang or lang not in avail:
lang = request.cookies.get("showcase_lang")
if not lang or lang not in avail:
lang = "zh"
return templates.TemplateResponse("index.html", {
"request": request, "postcards": all_postcards, "lang": lang,
})
# ---------- Auth ----------
@router.get("/login", response_class=HTMLResponse)

View File

@@ -9,7 +9,7 @@
<body>
{% block nav %}
<nav class="navbar">
<a href="/dashboard" class="nav-brand">{{ 'brand'|t(user.language if user else 'zh') }}</a>
<a href="/" class="nav-brand">{{ 'brand'|t(user.language if user else 'zh') }}</a>
{% if user is defined and user %}
<button class="nav-hamburger" onclick="document.querySelector('.nav-links').classList.toggle('open')"></button>
<div class="nav-links">

74
app/templates/index.html Normal file
View File

@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block title %}{{ 'brand'|t(lang) }}{% endblock %}
{% block nav %}
<nav class="navbar">
<a href="/" class="nav-brand">{{ 'brand'|t(lang) }}</a>
<div class="nav-links">
<div class="lang-switcher" id="langSwitcher">
<button type="button" class="lang-globe" onclick="document.getElementById('langDropdown').classList.toggle('show')">🌐</button>
<div class="lang-dropdown" id="langDropdown">
{% for code, name in available_languages.items() %}
<a href="/?lang={{ code }}" class="lang-option {% if lang == code %}active{% endif %}">{{ name }}</a>
{% endfor %}
</div>
</div>
<a href="/login" class="btn btn-primary" style="margin-left:.5rem">{{ 'index.go_login'|t(lang) }}</a>
</div>
</nav>
{% endblock %}
{% block content %}
<div class="section-header">
<h1>📮 {{ 'index.title'|t(lang) }}</h1>
<p style="color:var(--text-muted);margin-top:.25rem">{{ 'index.subtitle'|t(lang) }}</p>
</div>
{% if postcards %}
<div class="showcase-grid">
{% for pc in postcards %}
<div class="showcase-card" data-front="{{ pc.image_front or '' }}" data-number="{{ pc.card_number }}" onclick="openLightbox(this)">
{% if pc.image_front %}
<img src="{{ pc.image_front }}" alt="" class="showcase-card-img" loading="lazy">
{% else %}
<div class="showcase-card-img placeholder-img">📷</div>
{% endif %}
<div class="showcase-card-body">
<strong>{{ pc.card_number }}</strong>
<div class="showcase-card-meta">
<span>{{ pc.country|flag }} {{ pc.country or '' }}</span>
{% if pc.send_time %}<span>{{ pc.send_time.strftime('%Y-%m-%d') }}</span>{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<p>{{ 'index.empty'|t(lang) }}</p>
</div>
{% endif %}
<!-- Lightbox -->
<div id="lightbox" class="lightbox" onclick="closeLightbox()" style="display:none">
<div class="lightbox-inner" onclick="event.stopPropagation()">
<button class="lightbox-close" onclick="closeLightbox()">&times;</button>
<img id="lightbox-img" src="" alt="">
<div id="lightbox-number" class="lightbox-number"></div>
</div>
</div>
<script>
function openLightbox(el) {
var src = el.getAttribute('data-front');
if (!src) return;
document.getElementById('lightbox-img').src = src;
document.getElementById('lightbox-number').textContent = el.getAttribute('data-number');
document.getElementById('lightbox').style.display = 'flex';
}
function closeLightbox() {
document.getElementById('lightbox').style.display = 'none';
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeLightbox();
});
</script>
{% endblock %}