Files
ayto/ayto/index.html
2026-03-15 10:09:05 +01:00

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>