Bei neuem Service Worker wird die Seite automatisch neu geladen. Kein Popup mehr für die User.
1246 lines
39 KiB
HTML
1246 lines
39 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>JGA Memory-Buch — Prag 2026</title>
|
|
<link rel="manifest" href="manifest.json">
|
|
<meta name="theme-color" content="#3b82f6">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<meta name="apple-mobile-web-app-title" content="JGA Prag">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<script>
|
|
(function() {
|
|
const saved = localStorage.getItem('jga-theme');
|
|
document.documentElement.setAttribute('data-theme', saved || 'dark');
|
|
})();
|
|
</script>
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📖</text></svg>">
|
|
<style>
|
|
:root, [data-theme="dark"] {
|
|
--bg: #1c1c1f;
|
|
--card-bg: #26262a;
|
|
--card-bg-2: #2e2e33;
|
|
--card-border: #353539;
|
|
--text: #e8e8ea;
|
|
--text-muted: #999;
|
|
--text-strong: #fff;
|
|
--accent: #3b82f6;
|
|
--accent-text: #60a5fa;
|
|
--danger: #ef4444;
|
|
--success: #22c55e;
|
|
--shadow: 0 4px 16px rgba(0,0,0,0.35);
|
|
--shadow-strong: 0 8px 32px rgba(0,0,0,0.5);
|
|
--modal-overlay: rgba(0,0,0,0.6);
|
|
}
|
|
[data-theme="light"] {
|
|
--bg: #f5f5f7;
|
|
--card-bg: #ffffff;
|
|
--card-bg-2: #f8f8fb;
|
|
--card-border: #e0e0e3;
|
|
--text: #1a1a1a;
|
|
--text-muted: #666;
|
|
--text-strong: #000;
|
|
--accent: #2563eb;
|
|
--accent-text: #2563eb;
|
|
--danger: #dc2626;
|
|
--success: #16a34a;
|
|
--shadow: 0 4px 16px rgba(0,0,0,0.08);
|
|
--shadow-strong: 0 8px 32px rgba(0,0,0,0.15);
|
|
--modal-overlay: rgba(0,0,0,0.4);
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body {
|
|
margin: 0; padding: 0;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
min-height: 100vh;
|
|
transition: background 0.3s, color 0.3s;
|
|
}
|
|
.container { max-width: 800px; margin: 0 auto; padding: 24px 24px 100px; }
|
|
h1 { font-size: 36px; margin: 0 0 8px; color: var(--text-strong); font-weight: 700; }
|
|
.subtitle { color: var(--text-muted); margin-bottom: 28px; font-size: 15px; }
|
|
.back-link { display: inline-block; margin-bottom: 16px; color: var(--accent-text); text-decoration: none; font-size: 14px; }
|
|
.back-link:hover { text-decoration: underline; }
|
|
|
|
.home-btn {
|
|
position: fixed;
|
|
top: 16px; left: 16px;
|
|
z-index: 1000;
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--card-border);
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
padding: 10px 16px;
|
|
border-radius: 22px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
transition: all 0.2s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
@media (max-width: 768px) {
|
|
.home-btn { top: 10px; left: 10px; padding: 8px 12px; font-size: 13px; }
|
|
.theme-toggle { top: 10px; right: 10px; width: 38px; height: 38px; font-size: 18px; }
|
|
}
|
|
.home-btn:hover {
|
|
transform: translateX(-2px);
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
}
|
|
.theme-toggle {
|
|
position: fixed;
|
|
top: 16px; right: 16px;
|
|
width: 44px; height: 44px;
|
|
border-radius: 50%;
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--card-border);
|
|
color: var(--text);
|
|
font-size: 20px;
|
|
cursor: pointer;
|
|
z-index: 100;
|
|
box-shadow: var(--shadow);
|
|
transition: all 0.2s;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.theme-toggle:hover { transform: rotate(15deg) scale(1.1); border-color: var(--accent); }
|
|
|
|
/* ===== Buttons ===== */
|
|
.btn {
|
|
background: var(--accent);
|
|
color: #fff;
|
|
border: 0;
|
|
border-radius: 10px;
|
|
padding: 10px 20px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn:hover { transform: translateY(-1px); }
|
|
.btn-secondary { background: var(--card-bg-2); color: var(--text); border: 1px solid var(--card-border); }
|
|
.btn-danger { background: var(--danger); }
|
|
.btn-large { font-size: 16px; padding: 14px 28px; }
|
|
|
|
/* ===== Setup-Wizard ===== */
|
|
.wizard {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px;
|
|
padding: 32px 28px;
|
|
box-shadow: var(--shadow);
|
|
max-width: 600px;
|
|
margin: 40px auto;
|
|
}
|
|
.wizard h2 { margin: 0 0 8px; color: var(--text-strong); font-size: 24px; }
|
|
.wizard p { color: var(--text-muted); margin: 0 0 24px; font-size: 14px; line-height: 1.5; }
|
|
.wizard-input {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 12px;
|
|
}
|
|
.wizard-input input {
|
|
flex: 1;
|
|
background: var(--card-bg-2);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
color: var(--text);
|
|
font-size: 14px;
|
|
}
|
|
.wizard-input input:focus { outline: 2px solid var(--accent); }
|
|
.wizard-input input[type="color"] { width: 50px; padding: 4px; cursor: pointer; }
|
|
.wizard-input select {
|
|
background: var(--card-bg-2);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 8px;
|
|
padding: 8px;
|
|
color: var(--text);
|
|
font-size: 18px;
|
|
}
|
|
.player-pills {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin: 16px 0 24px;
|
|
min-height: 40px;
|
|
}
|
|
.player-pill {
|
|
background: var(--card-bg-2);
|
|
border: 2px solid var(--accent);
|
|
border-radius: 999px;
|
|
padding: 6px 14px;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.player-pill button {
|
|
background: transparent;
|
|
border: 0;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
}
|
|
.player-pill button:hover { color: var(--danger); }
|
|
|
|
/* ===== Header / Stats ===== */
|
|
.top-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-end;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
.top-bar h1 { margin: 0; }
|
|
.top-actions { display: flex; gap: 8px; }
|
|
|
|
.stats-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 20px;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.stats-bar .pill {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 999px;
|
|
padding: 6px 12px;
|
|
}
|
|
.stats-bar .pill strong { color: var(--text-strong); }
|
|
|
|
.filter-bar {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.filter-btn {
|
|
background: var(--card-bg-2);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 999px;
|
|
padding: 6px 14px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
color: var(--text);
|
|
}
|
|
.filter-btn.active {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: #fff;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ===== Entry Card ===== */
|
|
.entry {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 14px;
|
|
padding: 18px;
|
|
margin-bottom: 14px;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
.entry-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
.author-avatar {
|
|
width: 36px; height: 36px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 20px;
|
|
color: #fff;
|
|
flex-shrink: 0;
|
|
}
|
|
.entry-author { font-weight: 600; color: var(--text-strong); font-size: 15px; }
|
|
.entry-time { font-size: 12px; color: var(--text-muted); }
|
|
.entry-mood { margin-left: auto; font-size: 22px; }
|
|
.entry-title { font-size: 17px; font-weight: 700; margin: 6px 0 4px; color: var(--text-strong); }
|
|
.entry-body { font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-wrap: break-word; }
|
|
.entry-location {
|
|
display: inline-block;
|
|
margin-top: 10px;
|
|
background: var(--card-bg-2);
|
|
border-radius: 6px;
|
|
padding: 4px 10px;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
.entry-tags { margin-top: 8px; display: flex; gap: 6px; flex-wrap: wrap; }
|
|
.entry-tag {
|
|
background: var(--card-bg-2);
|
|
border-radius: 999px;
|
|
padding: 2px 8px;
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.entry-media {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
gap: 6px;
|
|
margin-top: 10px;
|
|
}
|
|
.entry-media img {
|
|
width: 100%;
|
|
height: 120px;
|
|
object-fit: cover;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.entry-footer {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid var(--card-border);
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
.reaction-btn {
|
|
background: var(--card-bg-2);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 999px;
|
|
padding: 4px 10px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.reaction-btn:hover { transform: scale(1.1); }
|
|
.reaction-btn.mine { background: rgba(59,130,246,0.2); border-color: var(--accent); }
|
|
.reaction-btn .count { font-size: 11px; color: var(--text-muted); font-weight: 600; }
|
|
|
|
.entry-actions {
|
|
margin-left: auto;
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
.entry-actions button {
|
|
background: transparent;
|
|
border: 0;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
padding: 4px 8px;
|
|
}
|
|
.entry-actions button:hover { color: var(--danger); }
|
|
|
|
/* ===== FAB ===== */
|
|
.fab {
|
|
position: fixed;
|
|
bottom: 30px; right: 30px;
|
|
width: 60px; height: 60px;
|
|
border-radius: 50%;
|
|
background: var(--accent);
|
|
color: #fff;
|
|
border: 0;
|
|
font-size: 32px;
|
|
cursor: pointer;
|
|
box-shadow: var(--shadow-strong);
|
|
transition: all 0.2s;
|
|
z-index: 50;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.fab:hover { transform: scale(1.1) rotate(90deg); }
|
|
|
|
/* ===== Modal ===== */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--modal-overlay);
|
|
display: none;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 200;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
.modal-overlay.open { display: flex; }
|
|
.modal {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 16px 16px 0 0;
|
|
padding: 24px;
|
|
max-width: 600px;
|
|
width: 100%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
box-shadow: var(--shadow-strong);
|
|
}
|
|
@media (min-width: 600px) {
|
|
.modal-overlay { align-items: center; }
|
|
.modal { border-radius: 16px; max-height: 80vh; }
|
|
}
|
|
.modal h2 { margin: 0 0 16px; color: var(--text-strong); font-size: 22px; }
|
|
|
|
.form-group { margin-bottom: 14px; }
|
|
.form-group label {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
font-size: 13px;
|
|
color: var(--text-muted);
|
|
font-weight: 600;
|
|
}
|
|
.form-group input, .form-group select, .form-group textarea {
|
|
width: 100%;
|
|
background: var(--card-bg-2);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
color: var(--text);
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
}
|
|
.form-group textarea { resize: vertical; min-height: 80px; }
|
|
|
|
.mood-picker {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.mood-btn {
|
|
background: var(--card-bg-2);
|
|
border: 2px solid var(--card-border);
|
|
border-radius: 8px;
|
|
padding: 6px 10px;
|
|
font-size: 22px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
.mood-btn.active { border-color: var(--accent); background: rgba(59,130,246,0.2); }
|
|
.mood-btn:hover { transform: scale(1.15); }
|
|
|
|
.photo-preview {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
|
gap: 6px;
|
|
margin-top: 10px;
|
|
}
|
|
.photo-preview-item {
|
|
position: relative;
|
|
aspect-ratio: 1;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
.photo-preview-item img { width: 100%; height: 100%; object-fit: cover; }
|
|
.photo-preview-item .remove {
|
|
position: absolute;
|
|
top: 2px; right: 2px;
|
|
background: rgba(0,0,0,0.7);
|
|
color: #fff;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
width: 22px; height: 22px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 20px;
|
|
}
|
|
.modal-actions .btn { flex: 1; }
|
|
|
|
/* ===== Image Lightbox ===== */
|
|
.lightbox {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0,0,0,0.92);
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 300;
|
|
cursor: zoom-out;
|
|
}
|
|
.lightbox.open { display: flex; }
|
|
.lightbox img { max-width: 95vw; max-height: 95vh; border-radius: 8px; }
|
|
|
|
/* ===== Empty / Footer ===== */
|
|
.empty {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
color: var(--text-muted);
|
|
font-style: italic;
|
|
background: var(--card-bg);
|
|
border: 1px dashed var(--card-border);
|
|
border-radius: 14px;
|
|
}
|
|
.empty .big { font-size: 48px; margin-bottom: 12px; }
|
|
|
|
.footer-note {
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
font-size: 12px;
|
|
margin-top: 30px;
|
|
}
|
|
|
|
.storage-warn {
|
|
background: rgba(234,179,8,0.15);
|
|
border: 1px solid #eab308;
|
|
color: #eab308;
|
|
padding: 10px 14px;
|
|
border-radius: 8px;
|
|
margin-bottom: 14px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.comment-section {
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
border-top: 1px solid var(--card-border);
|
|
}
|
|
.comment {
|
|
background: var(--card-bg-2);
|
|
border-radius: 8px;
|
|
padding: 8px 10px;
|
|
margin-top: 6px;
|
|
font-size: 13px;
|
|
}
|
|
.comment .author { font-weight: 600; color: var(--text-strong); }
|
|
.comment .text { color: var(--text); }
|
|
.comment-form {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-top: 8px;
|
|
}
|
|
.comment-form input {
|
|
flex: 1;
|
|
background: var(--card-bg-2);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 6px;
|
|
padding: 6px 10px;
|
|
color: var(--text);
|
|
font-size: 13px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<a href="index.html" class="home-btn" title="Zurück zur Startseite">🏠 Startseite</a>
|
|
<button class="theme-toggle" id="themeToggle" aria-label="Theme wechseln">🌙</button>
|
|
|
|
<div class="container">
|
|
<a href="index.html" class="back-link">← Zurück zur Übersicht</a>
|
|
|
|
<!-- ===== Setup-Wizard (nur beim ersten Mal) ===== -->
|
|
<div class="wizard" id="wizard" style="display:none;">
|
|
<h2>📖 Willkommen im Memory-Buch</h2>
|
|
<p>Wer ist beim JGA dabei? Lege die Mitreisenden an — du kannst später weitere hinzufügen.</p>
|
|
|
|
<div class="form-group">
|
|
<label>Name</label>
|
|
<div class="wizard-input">
|
|
<input type="text" id="newPlayerName" placeholder="z.B. Bräutigam">
|
|
<input type="color" id="newPlayerColor" value="#e91e63">
|
|
<select id="newPlayerEmoji">
|
|
<option>🤵</option><option>🤠</option><option>🥳</option><option>😎</option>
|
|
<option>🤩</option><option>😈</option><option>🧔</option><option>👨</option>
|
|
<option>👩</option><option>🧑</option><option>🦸</option><option>🥷</option>
|
|
<option>🤡</option><option>👻</option><option>🎅</option><option>👽</option>
|
|
</select>
|
|
<button class="btn" id="addPlayerBtn">+</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="player-pills" id="playerPills"></div>
|
|
|
|
<button class="btn btn-large" id="finishSetupBtn" style="width:100%;">Setup abschließen →</button>
|
|
</div>
|
|
|
|
<!-- ===== Hauptansicht ===== -->
|
|
<div id="mainView" style="display:none;">
|
|
<div class="top-bar">
|
|
<div>
|
|
<h1>📖 JGA Prag 2026</h1>
|
|
<p class="subtitle" id="entryCount">0 Erinnerungen</p>
|
|
</div>
|
|
<div class="top-actions">
|
|
<button class="btn btn-secondary" id="managePlayersBtn">👥 Spieler</button>
|
|
<button class="btn btn-secondary" id="exportBtn">💾 Export</button>
|
|
<button class="btn btn-secondary" id="importBtn">📂 Import</button>
|
|
<input type="file" id="importFile" accept="application/json" style="display:none;">
|
|
</div>
|
|
</div>
|
|
|
|
<div id="storageWarn" class="storage-warn" style="display:none;">
|
|
⚠️ Speicher fast voll (<span id="storageUsed">0</span> KB belegt) — JSON-Backup machen!
|
|
</div>
|
|
|
|
<div class="filter-bar" id="filterBar">
|
|
<button class="filter-btn active" data-filter="all">Alle</button>
|
|
</div>
|
|
|
|
<div id="entries"></div>
|
|
|
|
<p class="footer-note">Memory-Buch wird automatisch in deinem Browser gespeichert. Backup via 💾 Export!</p>
|
|
</div>
|
|
|
|
<!-- ===== FAB ===== -->
|
|
<button class="fab" id="fab" style="display:none;" title="Neuer Eintrag">+</button>
|
|
</div>
|
|
|
|
<!-- ===== Modal: Neuer Eintrag ===== -->
|
|
<div class="modal-overlay" id="entryModal">
|
|
<div class="modal">
|
|
<h2>Neue Erinnerung</h2>
|
|
|
|
<div class="form-group">
|
|
<label>Autor</label>
|
|
<select id="entryAuthor"></select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Titel (optional)</label>
|
|
<input type="text" id="entryTitle" placeholder="z.B. Die legendäre Boot-Tour" maxlength="80">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Was ist passiert?</label>
|
|
<textarea id="entryBody" placeholder="Erzähl, was los war..."></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Stimmung</label>
|
|
<div class="mood-picker" id="moodPicker">
|
|
<button class="mood-btn" data-mood="😊">😊</button>
|
|
<button class="mood-btn" data-mood="😂">😂</button>
|
|
<button class="mood-btn" data-mood="🥲">🥲</button>
|
|
<button class="mood-btn" data-mood="😍">😍</button>
|
|
<button class="mood-btn" data-mood="🤩">🤩</button>
|
|
<button class="mood-btn" data-mood="😎">😎</button>
|
|
<button class="mood-btn" data-mood="🤮">🤮</button>
|
|
<button class="mood-btn" data-mood="🥴">🥴</button>
|
|
<button class="mood-btn" data-mood="😭">😭</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Location (optional)</label>
|
|
<select id="entryLocation">
|
|
<option value="">— keine —</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Tags (Komma-getrennt)</label>
|
|
<input type="text" id="entryTags" placeholder="lustig, boot, beer-boat">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Fotos (automatisch resized auf 1200px)</label>
|
|
<input type="file" id="entryPhotos" accept="image/*" multiple capture="environment">
|
|
<div class="photo-preview" id="photoPreview"></div>
|
|
</div>
|
|
|
|
<div class="modal-actions">
|
|
<button class="btn btn-secondary" id="cancelEntryBtn">Abbrechen</button>
|
|
<button class="btn" id="saveEntryBtn">💾 Speichern</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== Modal: Spieler verwalten ===== -->
|
|
<div class="modal-overlay" id="playersModal">
|
|
<div class="modal">
|
|
<h2>👥 Spieler verwalten</h2>
|
|
<div class="form-group">
|
|
<label>Neuer Spieler</label>
|
|
<div class="wizard-input">
|
|
<input type="text" id="newPName" placeholder="Name">
|
|
<input type="color" id="newPColor" value="#3b82f6">
|
|
<select id="newPEmoji">
|
|
<option>🤵</option><option>🤠</option><option>🥳</option><option>😎</option>
|
|
<option>🤩</option><option>😈</option><option>🧔</option><option>👨</option>
|
|
<option>👩</option><option>🧑</option><option>🦸</option><option>🥷</option>
|
|
</select>
|
|
<button class="btn" id="addPBtn">+</button>
|
|
</div>
|
|
</div>
|
|
<div class="player-pills" id="existingPlayers"></div>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-secondary" id="closePlayersBtn">Schließen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== Lightbox ===== -->
|
|
<div class="lightbox" id="lightbox">
|
|
<img id="lightboxImg" src="" alt="">
|
|
</div>
|
|
|
|
<script>
|
|
/* ===== Theme ===== */
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
function setTheme(mode) {
|
|
document.documentElement.setAttribute('data-theme', mode);
|
|
localStorage.setItem('jga-theme', mode);
|
|
themeToggle.textContent = mode === 'dark' ? '🌙' : '☀️';
|
|
}
|
|
themeToggle.addEventListener('click', () => {
|
|
const cur = document.documentElement.getAttribute('data-theme');
|
|
setTheme(cur === 'dark' ? 'light' : 'dark');
|
|
});
|
|
setTheme(localStorage.getItem('jga-theme') || 'dark');
|
|
|
|
/* ===== Locations (gleiche wie im Tourenplan) ===== */
|
|
const LOCATIONS = [
|
|
{ name: 'Hotel Uno', lat: 50.0717636, lon: 14.4999461 },
|
|
{ name: 'Endorfin Escape', lat: 50.0876, lon: 14.4213 },
|
|
{ name: 'Beer Boat', lat: 50.0942, lon: 14.4270 },
|
|
{ name: 'Bowling Ovčín', lat: 50.0711, lon: 14.4050 },
|
|
{ name: 'Mysterium Tour', lat: 50.0870, lon: 14.4207 },
|
|
{ name: 'Paintball Praha', lat: 50.0500, lon: 14.3500 },
|
|
{ name: 'FunArena Axtwerfen', lat: 50.0657, lon: 14.4387 },
|
|
{ name: 'Mercuria Lasertag', lat: 50.0510, lon: 14.4900 },
|
|
{ name: 'Prague Ranger', lat: 50.0860, lon: 14.4180 },
|
|
{ name: 'Level Minigolf', lat: 50.0980, lon: 14.4250 },
|
|
{ name: 'KartPlanet', lat: 50.0730, lon: 14.4700 },
|
|
{ name: 'Enigma', lat: 50.0860, lon: 14.4200 },
|
|
{ name: 'Sonstiges (kein Tag)', lat: null, lon: null }
|
|
];
|
|
|
|
/* ===== State ===== */
|
|
const REACTIONS = ['❤️','😂','🥲','😍','🤩','😎','🔥'];
|
|
const DEFAULT_AUTHOR = { name: 'Bräutigam', color: '#e91e63', emoji: '🤵' };
|
|
const STORAGE_KEYS = {
|
|
players: 'jga-memory-players',
|
|
entries: 'jga-memory-entries',
|
|
setupDone: 'jga-memory-setup-done'
|
|
};
|
|
|
|
const state = {
|
|
players: JSON.parse(localStorage.getItem(STORAGE_KEYS.players) || '[]'),
|
|
entries: JSON.parse(localStorage.getItem(STORAGE_KEYS.entries) || '[]'),
|
|
setupDone: localStorage.getItem(STORAGE_KEYS.setupDone) === '1',
|
|
filter: 'all',
|
|
pendingPhotos: [],
|
|
pendingMood: null
|
|
};
|
|
|
|
function save() {
|
|
localStorage.setItem(STORAGE_KEYS.players, JSON.stringify(state.players));
|
|
localStorage.setItem(STORAGE_KEYS.entries, JSON.stringify(state.entries));
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
}
|
|
|
|
function timeAgo(ts) {
|
|
const sec = (Date.now() - ts) / 1000;
|
|
if (sec < 60) return 'gerade eben';
|
|
if (sec < 3600) return `vor ${Math.floor(sec/60)} Min`;
|
|
if (sec < 86400) return `vor ${Math.floor(sec/3600)} Std`;
|
|
if (sec < 604800) return `vor ${Math.floor(sec/86400)} Tag${Math.floor(sec/86400)>1?'en':''}`;
|
|
return new Date(ts).toLocaleDateString('de-DE');
|
|
}
|
|
|
|
function getStorageUsage() {
|
|
let bytes = 0;
|
|
for (const key in localStorage) {
|
|
if (localStorage.hasOwnProperty(key) && key.startsWith('jga-memory')) {
|
|
bytes += (localStorage[key].length + key.length) * 2;
|
|
}
|
|
}
|
|
return Math.round(bytes / 1024);
|
|
}
|
|
|
|
/* ===== Setup-Wizard ===== */
|
|
function showWizard() {
|
|
document.getElementById('wizard').style.display = 'block';
|
|
document.getElementById('mainView').style.display = 'none';
|
|
document.getElementById('fab').style.display = 'none';
|
|
renderWizardPlayers();
|
|
}
|
|
function showMain() {
|
|
document.getElementById('wizard').style.display = 'none';
|
|
document.getElementById('mainView').style.display = 'block';
|
|
document.getElementById('fab').style.display = 'flex';
|
|
renderAuthors();
|
|
renderLocationOptions();
|
|
renderFilters();
|
|
renderEntries();
|
|
}
|
|
|
|
function renderWizardPlayers() {
|
|
const c = document.getElementById('playerPills');
|
|
c.innerHTML = state.players.map((p, i) => `
|
|
<div class="player-pill" style="border-color:${p.color}">
|
|
<span>${p.emoji}</span> <span>${escapeHtml(p.name)}</span>
|
|
<button data-idx="${i}" title="Entfernen">✕</button>
|
|
</div>
|
|
`).join('') || '<p style="color:var(--text-muted); font-size:13px;">Noch keine Spieler — füge welche oben hinzu</p>';
|
|
c.querySelectorAll('button').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
state.players.splice(parseInt(btn.dataset.idx), 1);
|
|
save();
|
|
renderWizardPlayers();
|
|
});
|
|
});
|
|
}
|
|
|
|
document.getElementById('addPlayerBtn').addEventListener('click', () => {
|
|
const name = document.getElementById('newPlayerName').value.trim();
|
|
const color = document.getElementById('newPlayerColor').value;
|
|
const emoji = document.getElementById('newPlayerEmoji').value;
|
|
if (!name) return;
|
|
state.players.push({ name, color, emoji });
|
|
document.getElementById('newPlayerName').value = '';
|
|
save();
|
|
renderWizardPlayers();
|
|
});
|
|
document.getElementById('newPlayerName').addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') document.getElementById('addPlayerBtn').click();
|
|
});
|
|
document.getElementById('finishSetupBtn').addEventListener('click', () => {
|
|
if (state.players.length === 0) {
|
|
// Mindestens Bräutigam als Default
|
|
state.players.push(DEFAULT_AUTHOR);
|
|
save();
|
|
}
|
|
state.setupDone = true;
|
|
localStorage.setItem(STORAGE_KEYS.setupDone, '1');
|
|
showMain();
|
|
});
|
|
|
|
/* ===== Spieler-Modal ===== */
|
|
document.getElementById('managePlayersBtn').addEventListener('click', () => {
|
|
document.getElementById('playersModal').classList.add('open');
|
|
renderExistingPlayers();
|
|
});
|
|
document.getElementById('closePlayersBtn').addEventListener('click', () => {
|
|
document.getElementById('playersModal').classList.remove('open');
|
|
renderAuthors();
|
|
renderEntries();
|
|
});
|
|
document.getElementById('addPBtn').addEventListener('click', () => {
|
|
const name = document.getElementById('newPName').value.trim();
|
|
const color = document.getElementById('newPColor').value;
|
|
const emoji = document.getElementById('newPEmoji').value;
|
|
if (!name) return;
|
|
state.players.push({ name, color, emoji });
|
|
document.getElementById('newPName').value = '';
|
|
save();
|
|
renderExistingPlayers();
|
|
renderAuthors();
|
|
renderEntries();
|
|
});
|
|
function renderExistingPlayers() {
|
|
const c = document.getElementById('existingPlayers');
|
|
c.innerHTML = state.players.map((p, i) => `
|
|
<div class="player-pill" style="border-color:${p.color}">
|
|
<span>${p.emoji}</span> <span>${escapeHtml(p.name)}</span>
|
|
<button data-idx="${i}">✕</button>
|
|
</div>
|
|
`).join('');
|
|
c.querySelectorAll('button').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
state.players.splice(parseInt(btn.dataset.idx), 1);
|
|
save();
|
|
renderExistingPlayers();
|
|
renderAuthors();
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ===== Filter ===== */
|
|
function renderFilters() {
|
|
const bar = document.getElementById('filterBar');
|
|
const others = state.players.map(p => `<button class="filter-btn" data-filter="author:${escapeHtml(p.name)}">${p.emoji} ${escapeHtml(p.name)}</button>`).join('');
|
|
bar.innerHTML = `
|
|
<button class="filter-btn ${state.filter==='all'?'active':''}" data-filter="all">Alle</button>
|
|
<button class="filter-btn ${state.filter==='photo'?'active':''}" data-filter="photo">📷 Mit Fotos</button>
|
|
${others}
|
|
`;
|
|
bar.querySelectorAll('.filter-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
state.filter = btn.dataset.filter;
|
|
renderFilters();
|
|
renderEntries();
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ===== Locations & Authors ===== */
|
|
function renderLocationOptions() {
|
|
const sel = document.getElementById('entryLocation');
|
|
sel.innerHTML = '<option value="">— keine —</option>' +
|
|
LOCATIONS.map(l => `<option value="${escapeHtml(l.name)}">${escapeHtml(l.name)}</option>`).join('');
|
|
}
|
|
function renderAuthors() {
|
|
const sel = document.getElementById('entryAuthor');
|
|
sel.innerHTML = state.players.map(p => `<option value="${escapeHtml(p.name)}">${p.emoji} ${escapeHtml(p.name)}</option>`).join('');
|
|
const last = localStorage.getItem('jga-memory-last-author');
|
|
if (last && state.players.find(p => p.name === last)) sel.value = last;
|
|
}
|
|
|
|
/* ===== Foto Resize ===== */
|
|
function resizeImage(file, maxWidth = 1200, quality = 0.85) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
let w = img.width, h = img.height;
|
|
if (w > maxWidth) {
|
|
h = h * (maxWidth / w);
|
|
w = maxWidth;
|
|
}
|
|
canvas.width = w; canvas.height = h;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.drawImage(img, 0, 0, w, h);
|
|
resolve(canvas.toDataURL('image/jpeg', quality));
|
|
};
|
|
img.onerror = reject;
|
|
img.src = e.target.result;
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
document.getElementById('entryPhotos').addEventListener('change', async (e) => {
|
|
const files = Array.from(e.target.files);
|
|
for (const f of files) {
|
|
try {
|
|
const dataUrl = await resizeImage(f);
|
|
state.pendingPhotos.push({ type: 'image', dataUrl, filename: f.name });
|
|
} catch (err) {
|
|
console.error('Bild-Fehler:', err);
|
|
}
|
|
}
|
|
e.target.value = '';
|
|
renderPhotoPreview();
|
|
});
|
|
function renderPhotoPreview() {
|
|
const c = document.getElementById('photoPreview');
|
|
c.innerHTML = state.pendingPhotos.map((p, i) => `
|
|
<div class="photo-preview-item">
|
|
<img src="${p.dataUrl}" alt="">
|
|
<button class="remove" data-idx="${i}">✕</button>
|
|
</div>
|
|
`).join('');
|
|
c.querySelectorAll('.remove').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
state.pendingPhotos.splice(parseInt(btn.dataset.idx), 1);
|
|
renderPhotoPreview();
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ===== Mood Picker ===== */
|
|
document.querySelectorAll('.mood-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.mood-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
state.pendingMood = btn.dataset.mood;
|
|
});
|
|
});
|
|
|
|
/* ===== Eintrag speichern ===== */
|
|
document.getElementById('saveEntryBtn').addEventListener('click', () => {
|
|
const author = document.getElementById('entryAuthor').value;
|
|
const title = document.getElementById('entryTitle').value.trim();
|
|
const body = document.getElementById('entryBody').value.trim();
|
|
const location = document.getElementById('entryLocation').value;
|
|
const tags = document.getElementById('entryTags').value.split(',').map(s => s.trim()).filter(Boolean);
|
|
if (!body && state.pendingPhotos.length === 0) {
|
|
alert('Bitte Text oder Foto hinzufügen');
|
|
return;
|
|
}
|
|
if (!author) return;
|
|
localStorage.setItem('jga-memory-last-author', author);
|
|
|
|
state.entries.push({
|
|
id: 'entry-' + Date.now(),
|
|
type: state.pendingPhotos.length > 0 ? 'mixed' : 'text',
|
|
author,
|
|
title: title || null,
|
|
body,
|
|
location: location || null,
|
|
tags,
|
|
mood: state.pendingMood || null,
|
|
media: state.pendingPhotos,
|
|
reactions: {},
|
|
comments: [],
|
|
createdAt: Date.now()
|
|
});
|
|
save();
|
|
closeEntryModal();
|
|
renderEntries();
|
|
checkStorage();
|
|
});
|
|
|
|
/* ===== Modal ===== */
|
|
document.getElementById('fab').addEventListener('click', openEntryModal);
|
|
function openEntryModal() {
|
|
document.getElementById('entryModal').classList.add('open');
|
|
document.getElementById('entryTitle').value = '';
|
|
document.getElementById('entryBody').value = '';
|
|
document.getElementById('entryTags').value = '';
|
|
document.getElementById('entryLocation').value = '';
|
|
state.pendingPhotos = [];
|
|
state.pendingMood = null;
|
|
renderPhotoPreview();
|
|
document.querySelectorAll('.mood-btn').forEach(b => b.classList.remove('active'));
|
|
renderAuthors();
|
|
}
|
|
function closeEntryModal() {
|
|
document.getElementById('entryModal').classList.remove('open');
|
|
}
|
|
document.getElementById('cancelEntryBtn').addEventListener('click', closeEntryModal);
|
|
|
|
/* ===== Einträge rendern ===== */
|
|
function renderEntries() {
|
|
const container = document.getElementById('entries');
|
|
let filtered = state.entries.slice().reverse();
|
|
if (state.filter === 'photo') {
|
|
filtered = filtered.filter(e => e.media && e.media.length > 0);
|
|
} else if (state.filter.startsWith('author:')) {
|
|
const name = state.filter.substring(7);
|
|
filtered = filtered.filter(e => e.author === name);
|
|
}
|
|
|
|
document.getElementById('entryCount').textContent = `${state.entries.length} Erinnerung${state.entries.length !== 1 ? 'en' : ''}`;
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty">
|
|
<div class="big">📖</div>
|
|
<p>Noch keine Erinnerungen${state.filter !== 'all' ? ' in diesem Filter' : ''}.</p>
|
|
<p style="font-size:13px; margin-top:8px;">Klicke unten rechts auf <strong>+</strong> um die erste zu erstellen.</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = filtered.map(e => {
|
|
const author = state.players.find(p => p.name === e.author) || { color: '#888', emoji: '👤' };
|
|
const photoHtml = (e.media && e.media.length > 0) ? `
|
|
<div class="entry-media">
|
|
${e.media.map(m => `<img src="${m.dataUrl}" data-img="${m.dataUrl}" alt="">`).join('')}
|
|
</div>` : '';
|
|
const tagsHtml = e.tags && e.tags.length > 0 ? `
|
|
<div class="entry-tags">
|
|
${e.tags.map(t => `<span class="entry-tag">#${escapeHtml(t)}</span>`).join('')}
|
|
</div>` : '';
|
|
const locHtml = e.location ? `<div class="entry-location">📍 ${escapeHtml(e.location)}</div>` : '';
|
|
const moodHtml = e.mood ? `<div class="entry-mood">${e.mood}</div>` : '';
|
|
|
|
const reactionHtml = REACTIONS.map(r => {
|
|
const users = e.reactions[r] || [];
|
|
const mine = users.includes(currentAuthor());
|
|
return `<button class="reaction-btn ${mine ? 'mine' : ''}" data-reaction="${r}" data-id="${e.id}">
|
|
${r} <span class="count">${users.length || ''}</span>
|
|
</button>`;
|
|
}).join('');
|
|
|
|
const commentsHtml = (e.comments && e.comments.length > 0) ? e.comments.map(c => `
|
|
<div class="comment">
|
|
<span class="author">${escapeHtml(c.author)}:</span>
|
|
<span class="text">${escapeHtml(c.text)}</span>
|
|
</div>
|
|
`).join('') : '';
|
|
|
|
return `
|
|
<div class="entry">
|
|
<div class="entry-header">
|
|
<div class="author-avatar" style="background:${author.color}">${author.emoji}</div>
|
|
<div>
|
|
<div class="entry-author">${escapeHtml(e.author)}</div>
|
|
<div class="entry-time">${timeAgo(e.createdAt)}</div>
|
|
</div>
|
|
${moodHtml}
|
|
</div>
|
|
${e.title ? `<div class="entry-title">${escapeHtml(e.title)}</div>` : ''}
|
|
${e.body ? `<div class="entry-body">${escapeHtml(e.body)}</div>` : ''}
|
|
${photoHtml}
|
|
${locHtml}
|
|
${tagsHtml}
|
|
<div class="entry-footer">
|
|
${reactionHtml}
|
|
<div class="entry-actions">
|
|
<button data-delete="${e.id}" title="Löschen">🗑️</button>
|
|
</div>
|
|
</div>
|
|
<div class="comment-section">
|
|
${commentsHtml}
|
|
<form class="comment-form" data-form="${e.id}">
|
|
<input type="text" placeholder="Kommentar..." maxlength="200">
|
|
<button type="submit" class="btn btn-secondary">💬</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
// Events binden
|
|
container.querySelectorAll('[data-reaction]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const id = btn.dataset.id;
|
|
const r = btn.dataset.reaction;
|
|
const entry = state.entries.find(x => x.id === id);
|
|
if (!entry) return;
|
|
const me = currentAuthor();
|
|
if (!entry.reactions) entry.reactions = {};
|
|
if (!entry.reactions[r]) entry.reactions[r] = [];
|
|
const idx = entry.reactions[r].indexOf(me);
|
|
if (idx >= 0) entry.reactions[r].splice(idx, 1);
|
|
else entry.reactions[r].push(me);
|
|
save();
|
|
renderEntries();
|
|
});
|
|
});
|
|
container.querySelectorAll('[data-delete]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
if (!confirm('Wirklich löschen?')) return;
|
|
state.entries = state.entries.filter(x => x.id !== btn.dataset.delete);
|
|
save();
|
|
renderEntries();
|
|
});
|
|
});
|
|
container.querySelectorAll('[data-img]').forEach(img => {
|
|
img.addEventListener('click', () => {
|
|
document.getElementById('lightboxImg').src = img.dataset.img;
|
|
document.getElementById('lightbox').classList.add('open');
|
|
});
|
|
});
|
|
container.querySelectorAll('[data-form]').forEach(form => {
|
|
form.addEventListener('submit', (ev) => {
|
|
ev.preventDefault();
|
|
const input = form.querySelector('input');
|
|
const text = input.value.trim();
|
|
if (!text) return;
|
|
const entry = state.entries.find(x => x.id === form.dataset.form);
|
|
if (!entry) return;
|
|
if (!entry.comments) entry.comments = [];
|
|
entry.comments.push({ author: currentAuthor(), text, createdAt: Date.now() });
|
|
save();
|
|
renderEntries();
|
|
});
|
|
});
|
|
}
|
|
|
|
function currentAuthor() {
|
|
const sel = document.getElementById('entryAuthor');
|
|
if (sel && sel.value) return sel.value;
|
|
return state.players[0]?.name || 'Anonym';
|
|
}
|
|
|
|
/* ===== Lightbox ===== */
|
|
document.getElementById('lightbox').addEventListener('click', () => {
|
|
document.getElementById('lightbox').classList.remove('open');
|
|
});
|
|
|
|
/* ===== Storage Warning ===== */
|
|
function checkStorage() {
|
|
const kb = getStorageUsage();
|
|
document.getElementById('storageUsed').textContent = kb;
|
|
if (kb > 4000) {
|
|
document.getElementById('storageWarn').style.display = 'block';
|
|
} else {
|
|
document.getElementById('storageWarn').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/* ===== Export / Import ===== */
|
|
document.getElementById('exportBtn').addEventListener('click', () => {
|
|
const data = {
|
|
version: 1,
|
|
exportedAt: new Date().toISOString(),
|
|
tripName: 'JGA Prag 2026',
|
|
players: state.players,
|
|
entries: state.entries
|
|
};
|
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `jga-prag-2026-memory-${new Date().toISOString().slice(0,10)}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
|
|
document.getElementById('importBtn').addEventListener('click', () => {
|
|
document.getElementById('importFile').click();
|
|
});
|
|
document.getElementById('importFile').addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
try {
|
|
const data = JSON.parse(ev.target.result);
|
|
if (!data.players || !data.entries) throw new Error('Ungültiges Format');
|
|
if (!confirm(`${data.entries.length} Einträge importieren? Vorhandene Daten werden überschrieben!`)) return;
|
|
state.players = data.players;
|
|
state.entries = data.entries;
|
|
save();
|
|
renderEntries();
|
|
renderAuthors();
|
|
renderFilters();
|
|
checkStorage();
|
|
alert(`✅ Importiert: ${data.entries.length} Einträge, ${data.players.length} Spieler`);
|
|
} catch (err) {
|
|
alert('Fehler beim Import: ' + err.message);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
e.target.value = '';
|
|
});
|
|
|
|
/* ===== Init ===== */
|
|
if (!state.setupDone) {
|
|
showWizard();
|
|
} else {
|
|
showMain();
|
|
}
|
|
checkStorage();
|
|
|
|
// PWA Service Worker — Network-First Strategie (Updates kommen sofort durch)
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('./sw.js')
|
|
.then(reg => {
|
|
console.log('[PWA] SW registered:', reg.scope);
|
|
// Update-Hook: wenn ein neuer SW installiert wurde, kurz reloaden
|
|
reg.addEventListener('updatefound', () => {
|
|
const newSw = reg.installing;
|
|
if (!newSw) return;
|
|
newSw.addEventListener('statechange', () => {
|
|
if (newSw.state === 'activated' && navigator.serviceWorker.controller) {
|
|
console.log('[PWA] Neue Version aktiviert — lade automatisch neu');
|
|
window.location.reload();
|
|
}
|
|
});
|
|
});
|
|
})
|
|
.catch(err => console.warn('[PWA] SW registration failed:', err));
|
|
});
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html> |