743 lines
36 KiB
HTML
743 lines
36 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de" data-theme="ayto">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AYTO? Match-Rechner | Themed Edition</title>
|
|
<!-- Anti-Caching Meta Tags to ensure latest version loads -->
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
<meta http-equiv="Pragma" content="no-cache">
|
|
<meta http-equiv="Expires" content="0">
|
|
|
|
<link rel="icon" href="/ayto/favicon.ico" type="image/x-icon">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
|
<!-- Lucide Icons -->
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
|
|
<style>
|
|
/* === THEME DEFINITIONS === */
|
|
:root, [data-theme="ayto"] {
|
|
--bg-color: #0a0a0f;
|
|
--panel-bg: rgba(20, 20, 30, 0.7);
|
|
--panel-border: rgba(0, 229, 255, 0.2);
|
|
--primary: #00e5ff; /* Cyan */
|
|
--primary-hover: #00b3cc;
|
|
--secondary: #ff007a; /* Magenta */
|
|
--text-main: #ffffff;
|
|
--text-muted: #8b92a5;
|
|
--input-bg: rgba(0, 0, 0, 0.4);
|
|
--success: #00ff88;
|
|
--error: #ff3333;
|
|
}
|
|
|
|
[data-theme="sai"] {
|
|
--bg-color: #eafafc;
|
|
--panel-bg: rgba(255, 255, 255, 0.8);
|
|
--panel-border: rgba(107, 192, 213, 0.4);
|
|
--primary: #f38da1; /* SAI Pink */
|
|
--primary-hover: #e07489;
|
|
--secondary: #6bc0d5; /* SAI Blue */
|
|
--text-main: #2c3e50;
|
|
--text-muted: #7f8c8d;
|
|
--input-bg: #f8fcfd;
|
|
--success: #2ecc71;
|
|
--error: #e74c3c;
|
|
}
|
|
|
|
[data-theme="trench"] {
|
|
--bg-color: #151515;
|
|
--panel-bg: rgba(34, 34, 34, 0.8);
|
|
--panel-border: rgba(252, 227, 0, 0.2);
|
|
--primary: #fce300; /* Trench Yellow */
|
|
--primary-hover: #d4bf00;
|
|
--secondary: #575c3a; /* Olive */
|
|
--text-main: #e0e0e0;
|
|
--text-muted: #888888;
|
|
--input-bg: #111111;
|
|
--success: #8bb35c;
|
|
--error: #cc4444;
|
|
}
|
|
|
|
[data-theme="blurryface"] {
|
|
--bg-color: #ececec;
|
|
--panel-bg: rgba(255, 255, 255, 0.9);
|
|
--panel-border: rgba(206, 45, 45, 0.3);
|
|
--primary: #ce2d2d; /* Blurryface Red */
|
|
--primary-hover: #a82020;
|
|
--secondary: #000000; /* Black */
|
|
--text-main: #111111;
|
|
--text-muted: #666666;
|
|
--input-bg: #f5f5f5;
|
|
--success: #27ae60;
|
|
--error: #ce2d2d;
|
|
}
|
|
|
|
[data-theme="clancy"] {
|
|
--bg-color: #1c1c1c;
|
|
--panel-bg: rgba(44, 44, 44, 0.8);
|
|
--panel-border: rgba(217, 56, 46, 0.3);
|
|
--primary: #f2c12e; /* Clancy Yellow */
|
|
--primary-hover: #d9ab24;
|
|
--secondary: #d9382e; /* Clancy Red */
|
|
--text-main: #f5f5f5;
|
|
--text-muted: #a0a0a0;
|
|
--input-bg: #111111;
|
|
--success: #f2c12e;
|
|
--error: #d9382e;
|
|
}
|
|
|
|
/* === GLOBAL STYLES === */
|
|
body {
|
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-main);
|
|
transition: background-color 0.4s ease, color 0.4s ease;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.theme-panel {
|
|
background: var(--panel-bg);
|
|
border: 1px solid var(--panel-border);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.theme-text { color: var(--text-main); }
|
|
.theme-text-muted { color: var(--text-muted); }
|
|
.theme-primary-text { color: var(--primary); }
|
|
.theme-secondary-text { color: var(--secondary); }
|
|
|
|
.theme-input {
|
|
background: var(--input-bg);
|
|
border: 1px solid var(--panel-border);
|
|
color: var(--text-main);
|
|
transition: all 0.2s ease;
|
|
}
|
|
.theme-input:focus {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 2px var(--panel-border);
|
|
outline: none;
|
|
}
|
|
|
|
.theme-btn {
|
|
background: var(--primary);
|
|
color: var(--bg-color);
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.theme-btn:hover:not(:disabled) {
|
|
background: var(--primary-hover);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 15px var(--panel-border);
|
|
}
|
|
.theme-btn:active:not(:disabled) {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.theme-btn-outline {
|
|
background: transparent;
|
|
border: 2px solid var(--primary);
|
|
color: var(--primary);
|
|
font-weight: 700;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.theme-btn-outline:hover:not(:disabled) {
|
|
background: var(--primary);
|
|
color: var(--bg-color);
|
|
}
|
|
|
|
.neon-glow {
|
|
text-shadow: 0 0 10px var(--primary), 0 0 20px var(--secondary);
|
|
}
|
|
|
|
/* Scrollbar */
|
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
::-webkit-scrollbar-track { background: var(--bg-color); }
|
|
::-webkit-scrollbar-thumb { background: var(--primary); border-radius: 4px; }
|
|
|
|
/* Probabilities */
|
|
.prob-badge {
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
font-weight: 800;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/* Matrix Table Styling */
|
|
table th, table td { border-color: var(--panel-border); }
|
|
|
|
/* Select Reset */
|
|
select.theme-input {
|
|
appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 0.75rem center;
|
|
background-size: 1rem;
|
|
padding-right: 2.5rem;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from { opacity: 0; transform: translateY(20px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.animate-slide-in { animation: slideIn 0.5s ease-out forwards; }
|
|
</style>
|
|
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {}
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body class="pb-12">
|
|
|
|
<!-- Navbar -->
|
|
<nav class="sticky top-0 z-50 theme-panel border-b border-t-0 border-l-0 border-r-0 py-4 px-6 mb-8 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="p-2 rounded-xl" style="background: var(--primary); color: var(--bg-color);">
|
|
<i data-lucide="heart-pulse" class="w-6 h-6"></i>
|
|
</div>
|
|
<div>
|
|
<h1 class="text-xl font-black tracking-tight" style="color: var(--primary);">AYTO MATCH-RECHNER</h1>
|
|
<p class="text-xs font-bold uppercase tracking-widest theme-text-muted">Probability Engine</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 w-full sm:w-auto">
|
|
<label for="theme-select" class="text-xs font-bold uppercase tracking-widest theme-text-muted hidden sm:block">Theme:</label>
|
|
<select id="theme-select" class="theme-input py-2 px-4 rounded-xl text-xs font-bold w-full sm:w-auto cursor-pointer border-2" style="border-color: var(--primary);">
|
|
<option value="ayto">📺 Are You The One (Neon)</option>
|
|
<option value="sai">🐉 Scaled And Icy (TØP)</option>
|
|
<option value="trench">🦅 Trench (TØP)</option>
|
|
<option value="blurryface">🔴 Blurryface (TØP)</option>
|
|
<option value="clancy">🔥 Clancy (TØP)</option>
|
|
</select>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main Content -->
|
|
<main class="w-full max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8 items-start">
|
|
|
|
<!-- LEFT COLUMN: INPUTS -->
|
|
<div class="lg:col-span-4 flex flex-col gap-6">
|
|
|
|
<!-- 1. The Cast -->
|
|
<section class="theme-panel rounded-3xl p-6 shadow-xl animate-slide-in">
|
|
<div class="flex items-center gap-3 mb-6 border-b pb-4" style="border-color: var(--panel-border);">
|
|
<i data-lucide="users" class="w-6 h-6 theme-primary-text"></i>
|
|
<h2 class="text-lg font-black uppercase tracking-widest">1. Die Villa</h2>
|
|
</div>
|
|
|
|
<div class="space-y-4 mb-6">
|
|
<div>
|
|
<label class="text-[10px] font-black uppercase tracking-widest mb-2 block theme-text-muted">Gruppe A (Wählt zuerst)</label>
|
|
<textarea id="group1-names" rows="4" class="w-full theme-input rounded-xl p-4 text-sm" placeholder="Namen untereinander, z.B. Sophie, Lara..."></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="text-[10px] font-black uppercase tracking-widest mb-2 block theme-text-muted">Gruppe B (Die Auswahl)</label>
|
|
<textarea id="group2-names" rows="4" class="w-full theme-input rounded-xl p-4 text-sm" placeholder="Namen untereinander, z.B. Max, Leon..."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="setup-button" class="w-full theme-btn py-4 rounded-xl shadow-lg">
|
|
VILLA STARTEN
|
|
</button>
|
|
|
|
<div class="grid grid-cols-2 gap-3 mt-4">
|
|
<button id="save-button" class="w-full theme-btn-outline py-2.5 rounded-xl text-xs disabled:opacity-30 flex items-center justify-center gap-2" disabled>
|
|
<i data-lucide="save" class="w-4 h-4"></i> SICHERN
|
|
</button>
|
|
<button id="load-button" class="w-full theme-btn-outline py-2.5 rounded-xl text-xs flex items-center justify-center gap-2">
|
|
<i data-lucide="folder-up" class="w-4 h-4"></i> LADEN
|
|
</button>
|
|
<input type="file" id="file-loader" class="hidden" accept=".json">
|
|
</div>
|
|
<p id="initial-possibilities-text" class="text-center mt-4 text-[10px] font-bold theme-text-muted italic tracking-wide"></p>
|
|
</section>
|
|
|
|
<!-- Hidden until setup -->
|
|
<div id="input-sections" class="hidden flex-col gap-6">
|
|
|
|
<!-- 2. Truth Booth -->
|
|
<section class="theme-panel rounded-3xl p-6 shadow-xl animate-slide-in">
|
|
<div class="flex items-center gap-3 mb-6 border-b pb-4" style="border-color: var(--panel-border);">
|
|
<i data-lucide="monitor-play" class="w-6 h-6 theme-primary-text"></i>
|
|
<h2 class="text-lg font-black uppercase tracking-widest">2. Truth Booth</h2>
|
|
</div>
|
|
<form id="truth-booth-form" class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<select id="tb-group1" class="theme-input rounded-xl p-3 text-sm"></select>
|
|
<select id="tb-group2" class="theme-input rounded-xl p-3 text-sm"></select>
|
|
</div>
|
|
<select id="tb-result" class="w-full theme-input rounded-xl p-3 text-sm font-bold">
|
|
<option value="no-match">❌ KEIN MATCH</option>
|
|
<option value="match">💖 PERFECT MATCH</option>
|
|
</select>
|
|
<button type="submit" class="w-full theme-btn py-3 rounded-xl text-sm border border-transparent hover:border-white/20">
|
|
ERGEBNIS HINZUFÜGEN
|
|
</button>
|
|
</form>
|
|
<div id="truth-booth-list" class="mt-6 flex flex-col gap-2"></div>
|
|
</section>
|
|
|
|
<!-- 3. Matching Night -->
|
|
<section class="theme-panel rounded-3xl p-6 shadow-xl animate-slide-in">
|
|
<div class="flex items-center gap-3 mb-6 border-b pb-4" style="border-color: var(--panel-border);">
|
|
<i data-lucide="flame" class="w-6 h-6 theme-secondary-text"></i>
|
|
<h2 class="text-lg font-black uppercase tracking-widest">3. Matching Night</h2>
|
|
</div>
|
|
<form id="ceremony-form">
|
|
<div id="ceremony-pairs-container" class="space-y-3 mb-6 max-h-[350px] overflow-y-auto pr-2"></div>
|
|
|
|
<div class="theme-input rounded-xl p-4 mb-4 flex items-center justify-between border-2" style="border-color: var(--panel-border);">
|
|
<span class="text-xs font-black uppercase tracking-widest theme-text-muted">Lichter (Beams)</span>
|
|
<div class="flex items-center gap-4">
|
|
<button type="button" onclick="ceremony_beams.stepDown()" class="w-8 h-8 flex items-center justify-center hover:bg-black/20 rounded-lg theme-text">
|
|
<i data-lucide="minus" class="w-4 h-4"></i>
|
|
</button>
|
|
<input type="number" id="ceremony-beams" min="0" value="0" class="w-12 bg-transparent text-center font-black text-2xl focus:outline-none theme-text">
|
|
<button type="button" onclick="ceremony_beams.stepUp()" class="w-8 h-8 flex items-center justify-center hover:bg-black/20 rounded-lg theme-text">
|
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="w-full theme-btn py-4 rounded-xl shadow-lg" style="background: var(--secondary); color: var(--bg-color);">
|
|
NACHT SPEICHERN
|
|
</button>
|
|
</form>
|
|
<div id="ceremony-list" class="mt-6 flex flex-col gap-4"></div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIGHT COLUMN: RESULTS -->
|
|
<div class="lg:col-span-8 flex flex-col gap-6">
|
|
|
|
<section id="status-dashboard" class="hidden animate-slide-in">
|
|
<div class="theme-panel rounded-3xl p-8 relative overflow-hidden flex flex-col md:flex-row items-center justify-between gap-6 shadow-2xl">
|
|
<!-- Decorative Glow -->
|
|
<div class="absolute -right-16 -top-16 w-64 h-64 rounded-full blur-3xl opacity-20" style="background: var(--primary); z-index: 0;"></div>
|
|
|
|
<div style="z-index: 1;">
|
|
<h3 class="text-xs font-black uppercase tracking-[0.2em] mb-2 theme-text-muted">Verbleibende Möglichkeiten</h3>
|
|
<p id="total-possibilities" class="text-5xl sm:text-7xl font-black tracking-tighter neon-glow" style="color: var(--primary);">--</p>
|
|
</div>
|
|
|
|
<div class="w-full md:w-auto flex flex-col gap-3" style="z-index: 1;">
|
|
<button id="calculate-button" class="w-full theme-btn py-4 px-8 rounded-xl flex items-center justify-center gap-3">
|
|
<i data-lucide="refresh-cw" class="w-5 h-5"></i> NEU BERECHNEN
|
|
</button>
|
|
<div class="flex items-center justify-center gap-2">
|
|
<span class="w-2 h-2 rounded-full animate-pulse" style="background: var(--success);"></span>
|
|
<span class="text-[10px] font-bold uppercase tracking-widest theme-text-muted">Live Engine Active</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="grid-section" class="hidden theme-panel rounded-3xl p-6 overflow-hidden shadow-2xl animate-slide-in">
|
|
<div class="flex flex-col xl:flex-row xl:items-end justify-between gap-4 mb-6">
|
|
<div>
|
|
<h2 class="text-2xl font-black tracking-tight mb-1 theme-text">Match-Matrix</h2>
|
|
<p class="text-xs font-bold uppercase tracking-widest theme-text-muted">Wahrscheinlichkeit pro Paar</p>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2 text-[10px] font-bold tracking-widest">
|
|
<div class="px-3 py-1.5 rounded-lg flex items-center gap-2 border" style="background: rgba(0,255,136,0.1); color: var(--success); border-color: rgba(0,255,136,0.3);">
|
|
<i data-lucide="check-circle" class="w-3 h-3"></i> 100% MATCH
|
|
</div>
|
|
<div class="px-3 py-1.5 rounded-lg flex items-center gap-2 border" style="background: rgba(255,51,51,0.1); color: var(--error); border-color: rgba(255,51,51,0.3);">
|
|
<i data-lucide="x-circle" class="w-3 h-3"></i> UNMÖGLICH
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto border rounded-xl" style="border-color: var(--panel-border);">
|
|
<table id="probability-table" class="w-full text-left border-collapse min-w-max">
|
|
<thead id="probability-table-head" style="background: var(--input-bg);" class="text-[10px] font-black uppercase tracking-widest theme-text-muted"></thead>
|
|
<tbody id="probability-table-body" class="divide-y" style="border-color: var(--panel-border);"></tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Global Modals/Overlays -->
|
|
<div id="error-message" class="hidden fixed bottom-6 left-1/2 -translate-x-1/2 theme-panel border-l-4 px-6 py-4 rounded-xl shadow-2xl z-[100] flex items-center gap-4 transition-all duration-300" style="border-left-color: var(--error);">
|
|
<i data-lucide="alert-triangle" class="w-6 h-6" style="color: var(--error);"></i>
|
|
<div>
|
|
<p class="text-xs font-black uppercase tracking-widest mb-1 theme-text">Achtung</p>
|
|
<p id="error-text" class="text-sm font-medium theme-text-muted"></p>
|
|
</div>
|
|
<button onclick="document.getElementById('error-message').classList.add('hidden')" class="ml-4 opacity-50 hover:opacity-100 theme-text"><i data-lucide="x" class="w-4 h-4"></i></button>
|
|
</div>
|
|
|
|
<div id="loading-spinner" class="hidden fixed inset-0 backdrop-blur-md flex items-center justify-center z-[200]" style="background: rgba(0,0,0,0.7);">
|
|
<div class="theme-panel p-10 rounded-3xl flex flex-col items-center shadow-2xl">
|
|
<div class="relative w-16 h-16 mb-6">
|
|
<div class="absolute inset-0 rounded-full border-4 opacity-20" style="border-color: var(--primary);"></div>
|
|
<div class="absolute inset-0 rounded-full border-4 border-t-transparent animate-spin" style="border-color: var(--primary);"></div>
|
|
</div>
|
|
<p class="text-sm font-black uppercase tracking-[0.3em] theme-text mb-2">Algorithmus läuft</p>
|
|
<p class="text-xs theme-text-muted">Berechne Kombinationen...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="module">
|
|
let group1Names = [];
|
|
let group2Names = [];
|
|
let truthBooths = [];
|
|
let ceremonies = [];
|
|
|
|
const $ = (selector) => document.getElementById(selector);
|
|
|
|
const setupButton = $("setup-button");
|
|
const calculateButton = $("calculate-button");
|
|
const saveButton = $("save-button");
|
|
const loadButton = $("load-button");
|
|
const fileLoader = $("file-loader");
|
|
const themeSelect = $("theme-select");
|
|
const inputSections = $("input-sections");
|
|
const statusDashboard = $("status-dashboard");
|
|
|
|
const g1NamesText = $("group1-names");
|
|
const g2NamesText = $("group2-names");
|
|
|
|
const tbForm = $("truth-booth-form");
|
|
const tbGroup1 = $("tb-group1");
|
|
const tbGroup2 = $("tb-group2");
|
|
const tbResult = $("tb-result");
|
|
const tbList = $("truth-booth-list");
|
|
|
|
const ceremonyForm = $("ceremony-form");
|
|
const ceremonyPairsContainer = $("ceremony-pairs-container");
|
|
const ceremonyBeams = $("ceremony-beams");
|
|
const ceremonyList = $("ceremony-list");
|
|
|
|
const gridSection = $("grid-section");
|
|
const resultsDisplay = $("results-display");
|
|
const totalPossibilitiesText = $("total-possibilities");
|
|
|
|
const probTableHead = $("probability-table-head");
|
|
const probTableBody = $("probability-table-body");
|
|
|
|
const errorMessage = $("error-message");
|
|
const errorText = $("error-text");
|
|
const loadingSpinner = $("loading-spinner");
|
|
const initialPossibilitiesText = $("initial-possibilities-text");
|
|
|
|
// Initialize Icons
|
|
lucide.createIcons();
|
|
|
|
// --- Cookie Theme Manager ---
|
|
function setCookie(name, value, days) {
|
|
let expires = "";
|
|
if (days) {
|
|
let date = new Date();
|
|
date.setTime(date.getTime() + (days*24*60*60*1000));
|
|
expires = "; expires=" + date.toUTCString();
|
|
}
|
|
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
|
}
|
|
|
|
function getCookie(name) {
|
|
let nameEQ = name + "=";
|
|
let ca = document.cookie.split(';');
|
|
for(let i=0; i < ca.length; i++) {
|
|
let c = ca[i];
|
|
while (c.charAt(0)==' ') c = c.substring(1,c.length);
|
|
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function applyTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
themeSelect.value = theme;
|
|
setCookie('ayto-theme', theme, 365); // Save for 1 year
|
|
}
|
|
|
|
themeSelect.addEventListener('change', (e) => applyTheme(e.target.value));
|
|
|
|
// Load saved theme on boot
|
|
const savedTheme = getCookie('ayto-theme') || 'ayto';
|
|
applyTheme(savedTheme);
|
|
|
|
// --- UI Feedback ---
|
|
function showLoading() { loadingSpinner.classList.remove("hidden"); }
|
|
function hideLoading() { loadingSpinner.classList.add("hidden"); }
|
|
|
|
function showError(message) {
|
|
errorText.textContent = message;
|
|
errorMessage.classList.remove("hidden");
|
|
setTimeout(() => errorMessage.classList.add("hidden"), 5000);
|
|
}
|
|
|
|
// --- Core Logic ---
|
|
function handleSetupContestants() {
|
|
const g1 = g1NamesText.value.split('\n').map(s => s.trim()).filter(s => s.length > 0);
|
|
const g2 = g2NamesText.value.split('\n').map(s => s.trim()).filter(s => s.length > 0);
|
|
|
|
if (g1.length === 0 || g2.length === 0) return showError("Bitte Namen für beide Gruppen eintragen.");
|
|
if (g1.length > g2.length) return showError("Gruppe A darf nicht größer sein als Gruppe B.");
|
|
|
|
group1Names = g1;
|
|
group2Names = g2;
|
|
truthBooths = [];
|
|
ceremonies = [];
|
|
tbList.innerHTML = '';
|
|
ceremonyList.innerHTML = '';
|
|
|
|
populateSelectors();
|
|
populateCeremonyPairBuilder();
|
|
|
|
initialPossibilitiesText.textContent = `${group1Names.length} in Gruppe A • ${group2Names.length} in Gruppe B`;
|
|
|
|
inputSections.classList.remove("hidden");
|
|
inputSections.classList.add("flex");
|
|
statusDashboard.classList.remove("hidden");
|
|
gridSection.classList.remove("hidden");
|
|
|
|
saveButton.disabled = false;
|
|
lucide.createIcons();
|
|
|
|
handleCalculate(); // Auto-calculate on start
|
|
return true;
|
|
}
|
|
|
|
function populateSelectors() {
|
|
const opt = (name) => `<option value="${name}">${name}</option>`;
|
|
tbGroup1.innerHTML = group1Names.map(opt).join('');
|
|
tbGroup2.innerHTML = group2Names.map(opt).join('');
|
|
}
|
|
|
|
function populateCeremonyPairBuilder() {
|
|
ceremonyPairsContainer.innerHTML = group1Names.map(name => `
|
|
<div class="flex items-center gap-3 p-2 border-b last:border-0" style="border-color: var(--panel-border);">
|
|
<span class="text-xs font-bold w-20 truncate theme-text">${name}</span>
|
|
<select data-g1-name="${name}" class="ceremony-pair-select flex-1 theme-input rounded-xl p-2 text-xs font-bold">
|
|
<option value="">Wähle Match...</option>
|
|
${group2Names.map(g2name => `<option value="${g2name}">${g2name}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderTruthBoothUI(booth) {
|
|
const el = document.createElement('div');
|
|
el.className = `p-3 rounded-xl flex justify-between items-center theme-input border-l-4 animate-slide-in shadow-md`;
|
|
el.style.borderLeftColor = booth.isMatch ? 'var(--success)' : 'var(--error)';
|
|
el.innerHTML = `
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-xs font-black uppercase tracking-wider theme-text">${booth.p1} + ${booth.p2}</span>
|
|
<span class="text-[10px] font-bold uppercase tracking-widest px-2 py-1 rounded" style="background: ${booth.isMatch ? 'rgba(0,255,136,0.1)' : 'rgba(255,51,51,0.1)'}; color: ${booth.isMatch ? 'var(--success)' : 'var(--error)'}">
|
|
${booth.isMatch ? 'MATCH' : 'NO MATCH'}
|
|
</span>
|
|
</div>
|
|
<button data-id="${booth.id}" class="remove-tb-btn p-1 transition-all opacity-50 hover:opacity-100" style="color: var(--error);"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
|
|
`;
|
|
tbList.appendChild(el);
|
|
el.querySelector('.remove-tb-btn').addEventListener('click', handleRemoveTruthBooth);
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function handleAddTruthBooth(e) {
|
|
e.preventDefault();
|
|
const booth = { id: Date.now(), p1: tbGroup1.value, p2: tbGroup2.value, isMatch: tbResult.value === 'match' };
|
|
truthBooths.push(booth);
|
|
renderTruthBoothUI(booth);
|
|
handleCalculate();
|
|
}
|
|
|
|
function handleRemoveTruthBooth(e) {
|
|
const idToRemove = Number(e.currentTarget.dataset.id);
|
|
truthBooths = truthBooths.filter(b => b.id !== idToRemove);
|
|
e.currentTarget.closest('div').remove();
|
|
handleCalculate();
|
|
}
|
|
|
|
function renderCeremonyUI(ceremony, index) {
|
|
const el = document.createElement('div');
|
|
el.className = 'p-5 rounded-2xl theme-input border animate-slide-in shadow-md relative overflow-hidden';
|
|
el.style.borderColor = 'var(--panel-border)';
|
|
el.innerHTML = `
|
|
<div class="absolute top-0 left-0 w-1 h-full" style="background: var(--secondary);"></div>
|
|
<div class="flex justify-between items-center mb-4 pb-3 border-b" style="border-color: var(--panel-border);">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-sm font-black uppercase tracking-widest theme-text">Nacht ${index}</span>
|
|
<span class="px-2.5 py-1 rounded-md text-[10px] font-bold uppercase" style="background: var(--primary); color: var(--bg-color);">${ceremony.beams} Lichter</span>
|
|
</div>
|
|
<button data-id="${ceremony.id}" class="remove-ceremony-btn p-1 transition-all opacity-50 hover:opacity-100" style="color: var(--error);"><i data-lucide="trash-2" class="w-4 h-4"></i></button>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
${Object.entries(ceremony.pairs).map(([p1, p2]) => `
|
|
<div class="text-[10px] px-2 py-1 rounded border font-bold theme-text flex gap-1" style="background: var(--input-bg); border-color: var(--panel-border);">
|
|
<span>${p1}</span> <span class="theme-text-muted">&</span> <span>${p2}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
ceremonyList.appendChild(el);
|
|
el.querySelector('.remove-ceremony-btn').addEventListener('click', handleRemoveCeremony);
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function handleAddCeremony(e) {
|
|
e.preventDefault();
|
|
const pairSelectors = document.querySelectorAll('.ceremony-pair-select');
|
|
const pairs = {};
|
|
const selectedG2Names = [];
|
|
|
|
for (const select of pairSelectors) {
|
|
const g2Name = select.value;
|
|
if (!g2Name) return showError("Bitte wähle alle Paarungen aus.");
|
|
selectedG2Names.push(g2Name);
|
|
pairs[select.dataset.g1Name] = g2Name;
|
|
}
|
|
|
|
if (new Set(selectedG2Names).size !== selectedG2Names.length) return showError("Jede Person aus Gruppe B darf pro Nacht nur einmal gewählt werden.");
|
|
|
|
const beams = parseInt(ceremonyBeams.value, 10);
|
|
const ceremony = { id: Date.now(), pairs, beams };
|
|
ceremonies.push(ceremony);
|
|
renderCeremonyUI(ceremony, ceremonies.length);
|
|
handleCalculate();
|
|
}
|
|
|
|
function handleRemoveCeremony(e) {
|
|
const idToRemove = Number(e.currentTarget.dataset.id);
|
|
ceremonies = ceremonies.filter(c => c.id !== idToRemove);
|
|
ceremonyList.innerHTML = '';
|
|
ceremonies.forEach((c, i) => renderCeremonyUI(c, i + 1));
|
|
handleCalculate();
|
|
}
|
|
|
|
async function handleCalculate() {
|
|
showLoading();
|
|
try {
|
|
const payload = {
|
|
group1: group1Names,
|
|
group2: group2Names,
|
|
truth_booths: truthBooths.map(tb => ({ p1: tb.p1, p2: tb.p2, match_: tb.isMatch })),
|
|
ceremonies: ceremonies.map(c => ({ pairs: c.pairs, beams: c.beams }))
|
|
};
|
|
|
|
// The browser proxy resolves /api/solve to the backend automatically
|
|
const res = await fetch('/api/solve', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!res.ok) throw new Error("Backend Fehler");
|
|
const results = await res.json();
|
|
|
|
totalPossibilitiesText.textContent = results.possibilities.toLocaleString('de-DE');
|
|
|
|
if (results.possibilities === 0) {
|
|
showError("WIDERSPRUCH: Die eingegebenen Daten sind mathematisch unmöglich!");
|
|
probTableBody.innerHTML = '';
|
|
probTableHead.innerHTML = '';
|
|
} else {
|
|
displayProbabilityGrid(results.grid_data);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
showError("Verbindung zum Server fehlgeschlagen. Stelle sicher, dass das Backend läuft.");
|
|
} finally {
|
|
hideLoading();
|
|
}
|
|
}
|
|
|
|
function displayProbabilityGrid(gridData) {
|
|
probTableHead.innerHTML = `
|
|
<tr>
|
|
<th class="p-3 border-b border-r" style="border-color: var(--panel-border);"></th>
|
|
${gridData.columns.map(name => `<th class="p-3 border-b whitespace-nowrap text-center" style="border-color: var(--panel-border);">${name}</th>`).join('')}
|
|
</tr>
|
|
`;
|
|
|
|
probTableBody.innerHTML = gridData.index.map((g1Name, rowIndex) => `
|
|
<tr class="hover:bg-white/[0.02] transition-colors">
|
|
<th class="p-3 text-xs font-black border-r sticky left-0 z-10 theme-input" style="border-color: var(--panel-border);">${g1Name}</th>
|
|
${gridData.columns.map((g2Name, colIndex) => {
|
|
const prob = gridData.data[rowIndex][colIndex] * 100;
|
|
let badgeStyle = '';
|
|
let textContent = `${prob.toFixed(1)}%`;
|
|
|
|
if (prob >= 99.9) {
|
|
badgeStyle = 'background: rgba(0,255,136,0.15); color: var(--success); border: 1px solid var(--success); box-shadow: 0 0 10px rgba(0,255,136,0.2);';
|
|
textContent = '100%';
|
|
} else if (prob <= 0.1) {
|
|
badgeStyle = 'background: rgba(255,51,51,0.05); color: var(--error); opacity: 0.4;';
|
|
textContent = '0%';
|
|
} else {
|
|
badgeStyle = 'color: var(--text-main); font-weight: 700;';
|
|
}
|
|
|
|
return `<td class="p-3 text-center border-b" style="border-color: var(--panel-border);">
|
|
<span class="prob-badge" style="${badgeStyle}">${textContent}</span>
|
|
</td>`;
|
|
}).join('')}
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function handleSaveConfig() {
|
|
const state = { group1Names, group2Names, truthBooths, ceremonies };
|
|
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url; a.download = 'ayto-daten.json';
|
|
a.click();
|
|
}
|
|
|
|
function handleLoadConfig() { fileLoader.click(); }
|
|
|
|
fileLoader.addEventListener("change", (e) => {
|
|
const file = e.target.files[0];
|
|
if(!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = (ev) => {
|
|
try {
|
|
const s = JSON.parse(ev.target.result);
|
|
g1NamesText.value = s.group1Names.join('\n');
|
|
g2NamesText.value = s.group2Names.join('\n');
|
|
handleSetupContestants();
|
|
truthBooths = s.truthBooths || [];
|
|
ceremonies = s.ceremonies || [];
|
|
truthBooths.forEach(renderTruthBoothUI);
|
|
ceremonies.forEach((c, i) => renderCeremonyUI(c, i + 1));
|
|
handleCalculate();
|
|
} catch(err) {
|
|
showError("Fehler beim Laden der Datei. Ist sie beschädigt?");
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
e.target.value = ''; // Reset so same file can be loaded again
|
|
});
|
|
|
|
// Event Listeners
|
|
setupButton.addEventListener("click", handleSetupContestants);
|
|
tbForm.addEventListener("submit", handleAddTruthBooth);
|
|
ceremonyForm.addEventListener("submit", handleAddCeremony);
|
|
calculateButton.addEventListener("click", handleCalculate);
|
|
saveButton.addEventListener("click", handleSaveConfig);
|
|
loadButton.addEventListener("click", handleLoadConfig);
|
|
</script>
|
|
</body>
|
|
</html> |