- vault.hidrago.click: Vaultwarden (password manager) - git.hidrago.click: Gitea (git service) - All static sites (suma, weather, combien-jour, tokyo-trip, etc.) - Nginx configs for all subdomains - Systemd user service for Vaultwarden
354 lines
10 KiB
Python
354 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the Tokyo Trip itinerary page (FR/EN/JA)."""
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
# ─── Trip data (easily editable) ───
|
|
TRIP = {
|
|
"title": {"fr": "Tokyo Trip", "en": "Tokyo Trip", "ja": "東京旅行"},
|
|
"date_label": {"fr": "Date du voyage", "en": "Trip date", "ja": "旅行日"},
|
|
"date": "", # à remplir plus tard
|
|
"steps": [
|
|
{
|
|
"time": "07:30",
|
|
"city": "Kuwana",
|
|
"station": {"fr": "Gare de Kuwana", "en": "Kuwana Station", "ja": "桑名駅"},
|
|
"action": {"fr": "Départ", "en": "Departure", "ja": "出発"},
|
|
"icon": "🚉",
|
|
"note": {"fr": "Train depuis Kuwana", "en": "Train from Kuwana", "ja": "桑名から電車で出発"},
|
|
},
|
|
{
|
|
"time": "08:35",
|
|
"city": "Nagoya",
|
|
"station": {"fr": "Gare de Nagoya", "en": "Nagoya Station", "ja": "名古屋駅"},
|
|
"action": {"fr": "Arrivée à Nagoya", "en": "Arrival at Nagoya", "ja": "名古屋到着"},
|
|
"icon": "🛬",
|
|
"note": {"fr": "22 minutes avant le Shinkansen", "en": "22 minutes before Shinkansen", "ja": "新幹線まで22分"},
|
|
},
|
|
{
|
|
"time": "08:57",
|
|
"city": "Nagoya",
|
|
"station": {"fr": "Gare de Nagoya", "en": "Nagoya Station", "ja": "名古屋駅"},
|
|
"action": {"fr": "Shinkansen pour Tokyo", "en": "Shinkansen to Tokyo", "ja": "新幹線で東京へ"},
|
|
"icon": "🚄",
|
|
"note": {"fr": "Départ en Shinkansen", "en": "Shinkansen departure", "ja": "新幹線出発"},
|
|
},
|
|
{
|
|
"time": "—",
|
|
"city": "Tokyo",
|
|
"station": {"fr": "Gare de Tokyo", "en": "Tokyo Station", "ja": "東京駅"},
|
|
"action": {"fr": "Arrivée à Tokyo", "en": "Arrival at Tokyo", "ja": "東京到着"},
|
|
"icon": "🏁",
|
|
"note": {"fr": "", "en": "", "ja": ""},
|
|
},
|
|
],
|
|
}
|
|
|
|
# ─── UI translations ───
|
|
I18N = {
|
|
"fr": {
|
|
"title": "Tokyo Trip — Itinéraire",
|
|
"heading": "🚄 Tokyo Trip",
|
|
"subtitle": "Itinéraire de voyage",
|
|
"step": "Étape",
|
|
"time": "Heure",
|
|
"station": "Gare",
|
|
"action": "Action",
|
|
"note": "Note",
|
|
"no_date": "Date à définir",
|
|
"updated": "Mis à jour le",
|
|
},
|
|
"en": {
|
|
"title": "Tokyo Trip — Itinerary",
|
|
"heading": "🚄 Tokyo Trip",
|
|
"subtitle": "Travel itinerary",
|
|
"step": "Step",
|
|
"time": "Time",
|
|
"station": "Station",
|
|
"action": "Action",
|
|
"note": "Note",
|
|
"no_date": "Date TBD",
|
|
"updated": "Updated on",
|
|
},
|
|
"ja": {
|
|
"title": "東京旅行 — 旅程",
|
|
"heading": "🚄 東京旅行",
|
|
"subtitle": "旅程表",
|
|
"step": "ステップ",
|
|
"time": "時刻",
|
|
"station": "駅",
|
|
"action": "行動",
|
|
"note": "メモ",
|
|
"no_date": "日付未定",
|
|
"updated": "更新日時",
|
|
},
|
|
}
|
|
|
|
REMINDER = {
|
|
"fr": "📅 Rappel : le 30 juillet, départ pour Saitama ! Chichi-bouh ! 🎉",
|
|
"en": "📅 Reminder: July 30th, departure for Saitama! Chichi-bouh! 🎉",
|
|
"ja": "📅 リマインダー:7月30日、埼玉へ出発!ちちぶー!🎉",
|
|
}
|
|
|
|
I18N_JSON = json.dumps(I18N, ensure_ascii=False)
|
|
TRIP_JSON = json.dumps(TRIP, ensure_ascii=False)
|
|
REMINDER_JSON = json.dumps(REMINDER, ensure_ascii=False)
|
|
|
|
|
|
def generate_html():
|
|
now = datetime.now(timezone.utc)
|
|
|
|
# Build step cards server-side (time, city, icon are language-independent)
|
|
steps_html = []
|
|
for i, step in enumerate(TRIP["steps"]):
|
|
note_html = (
|
|
f'<div class="step-note" data-step-note="{i}"></div>'
|
|
if step["note"].get("fr")
|
|
else ""
|
|
)
|
|
steps_html.append(f"""
|
|
<div class="step-card">
|
|
<div class="step-time">{step['time']}</div>
|
|
<div class="step-icon">{step['icon']}</div>
|
|
<div class="step-body">
|
|
<div class="step-city">{step['city']}</div>
|
|
<div class="step-station" data-step-station="{i}"></div>
|
|
<div class="step-action" data-step-action="{i}"></div>
|
|
{note_html}
|
|
</div>
|
|
</div>""")
|
|
|
|
steps = "\n".join(steps_html)
|
|
# Connector arrows between steps
|
|
n_steps = len(TRIP["steps"])
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Tokyo Trip — Itinéraire</title>
|
|
<style>
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 2rem 1rem;
|
|
}}
|
|
.lang-switch {{
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}}
|
|
.lang-btn {{
|
|
padding: 0.4rem 1rem;
|
|
border: 2px solid rgba(255,255,255,0.4);
|
|
background: rgba(255,255,255,0.15);
|
|
color: #fff;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
}}
|
|
.lang-btn:hover {{ background: rgba(255,255,255,0.3); }}
|
|
.lang-btn.active {{
|
|
background: #fff;
|
|
color: #764ba2;
|
|
border-color: #fff;
|
|
}}
|
|
h1 {{
|
|
color: #fff;
|
|
font-size: 2rem;
|
|
margin-bottom: 0.3rem;
|
|
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
}}
|
|
.subtitle {{
|
|
color: rgba(255,255,255,0.8);
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0.5rem;
|
|
}}
|
|
.trip-date {{
|
|
color: rgba(255,255,255,0.6);
|
|
font-size: 0.85rem;
|
|
margin-bottom: 0.5rem;
|
|
}}
|
|
.reminder {{
|
|
background: rgba(255,255,255,0.95);
|
|
color: #e8893b;
|
|
font-weight: 700;
|
|
font-size: 1rem;
|
|
padding: 0.8rem 1.2rem;
|
|
border-radius: 12px;
|
|
margin-bottom: 2rem;
|
|
max-width: 580px;
|
|
width: 100%;
|
|
text-align: center;
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
|
border: 2px solid #e8893b;
|
|
}}
|
|
.timeline {{
|
|
max-width: 580px;
|
|
width: 100%;
|
|
position: relative;
|
|
}}
|
|
.step-card {{
|
|
background: rgba(255,255,255,0.95);
|
|
border-radius: 16px;
|
|
padding: 1.2rem 1.5rem;
|
|
margin-bottom: 0.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
|
transition: transform 0.2s;
|
|
}}
|
|
.step-card:hover {{ transform: translateX(4px); }}
|
|
.step-time {{
|
|
font-size: 1.4rem;
|
|
font-weight: 800;
|
|
color: #764ba2;
|
|
min-width: 60px;
|
|
text-align: center;
|
|
}}
|
|
.step-icon {{
|
|
font-size: 2rem;
|
|
min-width: 40px;
|
|
text-align: center;
|
|
}}
|
|
.step-body {{
|
|
flex: 1;
|
|
}}
|
|
.step-city {{
|
|
font-size: 1.2rem;
|
|
font-weight: 700;
|
|
color: #333;
|
|
}}
|
|
.step-station {{
|
|
font-size: 0.85rem;
|
|
color: #888;
|
|
}}
|
|
.step-action {{
|
|
font-size: 0.95rem;
|
|
color: #555;
|
|
margin-top: 0.2rem;
|
|
}}
|
|
.step-note {{
|
|
font-size: 0.8rem;
|
|
color: #e8893b;
|
|
margin-top: 0.3rem;
|
|
font-style: italic;
|
|
}}
|
|
.connector {{
|
|
text-align: center;
|
|
color: rgba(255,255,255,0.5);
|
|
font-size: 1.5rem;
|
|
margin: 0.1rem 0;
|
|
line-height: 1;
|
|
}}
|
|
footer {{
|
|
margin-top: 2rem;
|
|
color: rgba(255,255,255,0.6);
|
|
font-size: 0.8rem;
|
|
text-align: center;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="lang-switch">
|
|
<button class="lang-btn active" data-lang="fr">FR</button>
|
|
<button class="lang-btn" data-lang="en">EN</button>
|
|
<button class="lang-btn" data-lang="ja">日本語</button>
|
|
</div>
|
|
<h1 id="heading">🚄 Tokyo Trip</h1>
|
|
<p class="subtitle" id="subtitle">Itinéraire de voyage</p>
|
|
<p class="trip-date" id="trip-date"></p>
|
|
<div class="reminder" id="reminder"></div>
|
|
<div class="timeline">
|
|
{steps}
|
|
</div>
|
|
<footer>
|
|
<span data-i18n="updated">Mis à jour le</span> {now.strftime('%d/%m/%Y à %H:%M UTC')}
|
|
</footer>
|
|
|
|
<script>
|
|
const I18N = {I18N_JSON};
|
|
const TRIP = {TRIP_JSON};
|
|
const REMINDER = {REMINDER_JSON};
|
|
let currentLang = 'fr';
|
|
|
|
function applyLang(lang) {{
|
|
currentLang = lang;
|
|
const t = I18N[lang];
|
|
document.title = t.title;
|
|
document.getElementById('heading').textContent = t.heading;
|
|
document.getElementById('subtitle').textContent = t.subtitle;
|
|
|
|
// Reminder banner
|
|
const remEl = document.getElementById('reminder');
|
|
if (remEl && REMINDER[lang]) remEl.textContent = REMINDER[lang];
|
|
|
|
// Trip date
|
|
const dateEl = document.getElementById('trip-date');
|
|
if (TRIP.date) {{
|
|
dateEl.textContent = t.date_label + ' : ' + TRIP.date;
|
|
}} else {{
|
|
dateEl.textContent = t.no_date;
|
|
}}
|
|
|
|
// Step stations, actions, notes
|
|
TRIP.steps.forEach((step, i) => {{
|
|
const stEl = document.querySelector('[data-step-station="' + i + '"]');
|
|
if (stEl) stEl.textContent = step.station[lang];
|
|
const actEl = document.querySelector('[data-step-action="' + i + '"]');
|
|
if (actEl) actEl.textContent = step.action[lang];
|
|
const noteEl = document.querySelector('[data-step-note="' + i + '"]');
|
|
if (noteEl && step.note[lang]) noteEl.textContent = step.note[lang];
|
|
}});
|
|
|
|
// Generic i18n labels
|
|
document.querySelectorAll('[data-i18n]').forEach(el => {{
|
|
const key = el.getAttribute('data-i18n');
|
|
if (t[key]) el.textContent = t[key];
|
|
}});
|
|
|
|
// Active button
|
|
document.querySelectorAll('.lang-btn').forEach(btn => {{
|
|
btn.classList.toggle('active', btn.getAttribute('data-lang') === lang);
|
|
}});
|
|
document.documentElement.lang = lang;
|
|
}}
|
|
|
|
document.querySelectorAll('.lang-btn').forEach(btn => {{
|
|
btn.addEventListener('click', () => {{
|
|
applyLang(btn.getAttribute('data-lang'));
|
|
localStorage.setItem('trip-lang', btn.getAttribute('data-lang'));
|
|
}});
|
|
}});
|
|
|
|
const saved = localStorage.getItem('trip-lang');
|
|
if (saved && I18N[saved]) {{
|
|
applyLang(saved);
|
|
}} else {{
|
|
const browserLang = navigator.language.slice(0, 2);
|
|
if (I18N[browserLang]) applyLang(browserLang);
|
|
}}
|
|
</script>
|
|
</body>
|
|
</html>"""
|
|
|
|
return html
|
|
|
|
|
|
if __name__ == "__main__":
|
|
html = generate_html()
|
|
output_path = "/home/jerome/hermes-sites/tokyo-trip.hidrago.click/index.html"
|
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
f.write(html)
|
|
print(f"Tokyo Trip page generated at {output_path}")
|