updated look and feel
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Rust
|
||||
/target/
|
||||
**/target/
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
**/*.bak
|
||||
|
||||
# IDEs and Editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
.docker/
|
||||
docker-compose.override.yml
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Local environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
630
ayto/index.html
630
ayto/index.html
@@ -3,142 +3,125 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Are You The One? Calculator</title>
|
||||
<title>AYTO? Match Calculator</title>
|
||||
<link rel="icon" href="/ayto/favicon.ico" type="image/x-icon">
|
||||
<!-- Load Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Use Inter font -->
|
||||
<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=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root, [data-theme="mocha"] {
|
||||
--crust: #11111b;
|
||||
--mantle: #181825;
|
||||
--base: #1e1e2e;
|
||||
--surface0: #313244;
|
||||
--surface1: #45475a;
|
||||
--surface2: #585b70;
|
||||
--overlay0: #6c7086;
|
||||
--overlay1: #7f849c;
|
||||
--overlay2: #9399b2;
|
||||
--subtext0: #a6adc8;
|
||||
--subtext1: #bac2de;
|
||||
--text: #cdd6f4;
|
||||
--lavender: #b4befe;
|
||||
--blue: #89b4fa;
|
||||
--sapphire: #74c7ec;
|
||||
--sky: #89dceb;
|
||||
--teal: #94e2d5;
|
||||
--green: #a6e3a1;
|
||||
--yellow: #f9e2af;
|
||||
--peach: #fab387;
|
||||
--maroon: #eba0ac;
|
||||
--red: #f38ba8;
|
||||
--mauve: #cba6f7;
|
||||
--pink: #f5c2e7;
|
||||
--flamingo: #f2cdcd;
|
||||
--rosewater: #f5e0dc;
|
||||
--crust: #11111b; --mantle: #181825; --base: #1e1e2e;
|
||||
--surface0: #313244; --surface1: #45475a; --surface2: #585b70;
|
||||
--overlay0: #6c7086; --overlay1: #7f849c; --overlay2: #9399b2;
|
||||
--subtext0: #a6adc8; --subtext1: #bac2de; --text: #cdd6f4;
|
||||
--lavender: #b4befe; --blue: #89b4fa; --sapphire: #74c7ec;
|
||||
--sky: #89dceb; --teal: #94e2d5; --green: #a6e3a1;
|
||||
--yellow: #f9e2af; --peach: #fab387; --maroon: #eba0ac;
|
||||
--red: #f38ba8; --mauve: #cba6f7; --pink: #f5c2e7;
|
||||
--flamingo: #f2cdcd; --rosewater: #f5e0dc;
|
||||
}
|
||||
|
||||
[data-theme="latte"] {
|
||||
--crust: #dce0e8;
|
||||
--mantle: #e6e9ef;
|
||||
--base: #eff1f5;
|
||||
--surface0: #ccd0da;
|
||||
--surface1: #bcc0cc;
|
||||
--surface2: #acb0be;
|
||||
--overlay0: #9ca0b0;
|
||||
--overlay1: #8c8fa1;
|
||||
--overlay2: #7c7f93;
|
||||
--subtext0: #6c6f85;
|
||||
--subtext1: #5c5f77;
|
||||
--text: #4c4f69;
|
||||
--lavender: #7287fd;
|
||||
--blue: #1e66f5;
|
||||
--sapphire: #209fb5;
|
||||
--sky: #04a5e5;
|
||||
--teal: #179299;
|
||||
--green: #40a02b;
|
||||
--yellow: #df8e1d;
|
||||
--peach: #fe640b;
|
||||
--maroon: #e64553;
|
||||
--red: #d20f39;
|
||||
--mauve: #8839ef;
|
||||
--pink: #ea76cb;
|
||||
--flamingo: #dd7878;
|
||||
--rosewater: #dc8a78;
|
||||
--crust: #dce0e8; --mantle: #e6e9ef; --base: #eff1f5;
|
||||
--surface0: #ccd0da; --surface1: #bcc0cc; --surface2: #acb0be;
|
||||
--overlay0: #9ca0b0; --overlay1: #8c8fa1; --overlay2: #7c7f93;
|
||||
--subtext0: #6c6f85; --subtext1: #5c5f77; --text: #4c4f69;
|
||||
--lavender: #7287fd; --blue: #1e66f5; --sapphire: #209fb5;
|
||||
--sky: #04a5e5; --teal: #179299; --green: #40a02b;
|
||||
--yellow: #df8e1d; --peach: #fe640b; --maroon: #e64553;
|
||||
--red: #d20f39; --mauve: #8839ef; --pink: #ea76cb;
|
||||
--flamingo: #dd7878; --rosewater: #dc8a78;
|
||||
}
|
||||
|
||||
[data-theme="frappe"] {
|
||||
--crust: #232634;
|
||||
--mantle: #292c3c;
|
||||
--base: #303446;
|
||||
--surface0: #414559;
|
||||
--surface1: #51576d;
|
||||
--surface2: #626880;
|
||||
--overlay0: #737994;
|
||||
--overlay1: #838ba7;
|
||||
--overlay2: #949cbb;
|
||||
--subtext0: #a5adce;
|
||||
--subtext1: #b5bfe2;
|
||||
--text: #c6d0f5;
|
||||
--lavender: #babbf1;
|
||||
--blue: #8caaee;
|
||||
--sapphire: #85c1dc;
|
||||
--sky: #99d1db;
|
||||
--teal: #81c8be;
|
||||
--green: #a6d189;
|
||||
--yellow: #e5c890;
|
||||
--peach: #ef9f76;
|
||||
--maroon: #ea999c;
|
||||
--red: #e78284;
|
||||
--mauve: #ca9ee6;
|
||||
--pink: #f4b8e4;
|
||||
--flamingo: #eebebe;
|
||||
--rosewater: #f2d5cf;
|
||||
--crust: #232634; --mantle: #292c3c; --base: #303446;
|
||||
--surface0: #414559; --surface1: #51576d; --surface2: #626880;
|
||||
--overlay0: #737994; --overlay1: #838ba7; --overlay2: #949cbb;
|
||||
--subtext0: #a5adce; --subtext1: #b5bfe2; --text: #c6d0f5;
|
||||
--lavender: #babbf1; --blue: #8caaee; --sapphire: #85c1dc;
|
||||
--sky: #99d1db; --teal: #81c8be; --green: #a6d189;
|
||||
--yellow: #e5c890; --peach: #ef9f76; --maroon: #ea999c;
|
||||
--red: #e78284; --mauve: #ca9ee6; --pink: #f4b8e4;
|
||||
--flamingo: #eebebe; --rosewater: #f2d5cf;
|
||||
}
|
||||
|
||||
[data-theme="macchiato"] {
|
||||
--crust: #181926;
|
||||
--mantle: #1e2030;
|
||||
--base: #24273a;
|
||||
--surface0: #363a4f;
|
||||
--surface1: #494d64;
|
||||
--surface2: #5b6078;
|
||||
--overlay0: #6e738d;
|
||||
--overlay1: #8087a2;
|
||||
--overlay2: #939ab7;
|
||||
--subtext0: #a5adcb;
|
||||
--subtext1: #b8c0e0;
|
||||
--text: #cad3f5;
|
||||
--lavender: #b7bdf8;
|
||||
--blue: #8aadf4;
|
||||
--sapphire: #7dc4e4;
|
||||
--sky: #91d7e3;
|
||||
--teal: #8bd5ca;
|
||||
--green: #a6da95;
|
||||
--yellow: #eed49f;
|
||||
--peach: #f5a97f;
|
||||
--maroon: #ee99a0;
|
||||
--red: #ed8796;
|
||||
--mauve: #c6a0f6;
|
||||
--pink: #f5bde6;
|
||||
--flamingo: #f0c6c6;
|
||||
--rosewater: #f4dbd6;
|
||||
--crust: #181926; --mantle: #1e2030; --base: #24273a;
|
||||
--surface0: #363a4f; --surface1: #494d64; --surface2: #5b6078;
|
||||
--overlay0: #6e738d; --overlay1: #8087a2; --overlay2: #939ab7;
|
||||
--subtext0: #a5adcb; --subtext1: #b8c0e0; --text: #cad3f5;
|
||||
--lavender: #b7bdf8; --blue: #8aadf4; --sapphire: #7dc4e4;
|
||||
--sky: #91d7e3; --teal: #8bd5ca; --green: #a6da95;
|
||||
--yellow: #eed49f; --peach: #f5a97f; --maroon: #ee99a0;
|
||||
--red: #ed8796; --mauve: #c6a0f6; --pink: #f5bde6;
|
||||
--flamingo: #f0c6c6; --rosewater: #f4dbd6;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
scroll-behavior: smooth;
|
||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||||
background: linear-gradient(135deg, var(--crust), var(--base));
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
}
|
||||
|
||||
[data-theme="latte"] .glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
border-radius: 24px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(to right, var(--lavender), var(--blue));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--blue), var(--sapphire));
|
||||
color: var(--base);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
filter: brightness(1.1);
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 20px rgba(137, 180, 250, 0.3);
|
||||
}
|
||||
|
||||
.input-glass {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--surface1);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.input-glass:focus {
|
||||
border-color: var(--blue);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.1);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.table-container::-webkit-scrollbar-thumb { background-color: var(--overlay1); border-radius: 3px; }
|
||||
.table-container::-webkit-scrollbar-track { background-color: var(--mantle); }
|
||||
.prob-100 { background-color: var(--green); color: var(--base); font-weight: bold; }
|
||||
.prob-0 { background-color: var(--red); color: var(--base); }
|
||||
.prob-possible { background-color: var(--peach); color: var(--base); }
|
||||
.table-container::-webkit-scrollbar-thumb { background-color: var(--overlay1); border-radius: 10px; }
|
||||
|
||||
.prob-100 { background: linear-gradient(135deg, rgba(166, 227, 161, 0.2), rgba(166, 227, 161, 0.4)); color: var(--green); font-weight: 700; }
|
||||
.prob-0 { background: linear-gradient(135deg, rgba(243, 139, 168, 0.1), rgba(243, 139, 168, 0.2)); color: var(--red); opacity: 0.8; }
|
||||
.prob-possible { background: transparent; color: var(--text); }
|
||||
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.animate-fade-in { animation: fadeIn 0.4s ease-out forwards; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -146,185 +129,170 @@
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
rosewater: 'var(--rosewater)',
|
||||
flamingo: 'var(--flamingo)',
|
||||
pink: 'var(--pink)',
|
||||
mauve: 'var(--mauve)',
|
||||
red: 'var(--red)',
|
||||
maroon: 'var(--maroon)',
|
||||
peach: 'var(--peach)',
|
||||
yellow: 'var(--yellow)',
|
||||
green: 'var(--green)',
|
||||
teal: 'var(--teal)',
|
||||
sky: 'var(--sky)',
|
||||
sapphire: 'var(--sapphire)',
|
||||
blue: 'var(--blue)',
|
||||
lavender: 'var(--lavender)',
|
||||
text: 'var(--text)',
|
||||
subtext1: 'var(--subtext1)',
|
||||
subtext0: 'var(--subtext0)',
|
||||
overlay2: 'var(--overlay2)',
|
||||
overlay1: 'var(--overlay1)',
|
||||
overlay0: 'var(--overlay0)',
|
||||
surface2: 'var(--surface2)',
|
||||
surface1: 'var(--surface1)',
|
||||
surface0: 'var(--surface0)',
|
||||
base: 'var(--base)',
|
||||
mantle: 'var(--mantle)',
|
||||
crust: 'var(--crust)',
|
||||
rosewater: 'var(--rosewater)', flamingo: 'var(--flamingo)', pink: 'var(--pink)',
|
||||
mauve: 'var(--mauve)', red: 'var(--red)', maroon: 'var(--maroon)',
|
||||
peach: 'var(--peach)', yellow: 'var(--yellow)', green: 'var(--green)',
|
||||
teal: 'var(--teal)', sky: 'var(--sky)', sapphire: 'var(--sapphire)',
|
||||
blue: 'var(--blue)', lavender: 'var(--lavender)', text: 'var(--text)',
|
||||
subtext1: 'var(--subtext1)', subtext0: 'var(--subtext0)',
|
||||
overlay2: 'var(--overlay2)', overlay1: 'var(--overlay1)', overlay0: 'var(--overlay0)',
|
||||
surface2: 'var(--surface2)', surface1: 'var(--surface1)', surface0: 'var(--surface0)',
|
||||
base: 'var(--base)', mantle: 'var(--mantle)', crust: 'var(--crust)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-base text-text min-h-full">
|
||||
<body class="bg-base text-text min-h-full pb-12">
|
||||
|
||||
<!-- Theme Switcher -->
|
||||
<div class="w-full bg-mantle border-b border-surface0 p-2 flex justify-end items-center pr-4">
|
||||
<label for="theme-select" class="text-sm font-medium text-subtext1 mr-2">Theme:</label>
|
||||
<select id="theme-select" class="p-1 bg-surface1 border border-surface2 rounded text-text text-sm focus:ring-blue focus:border-blue">
|
||||
<option value="mocha">Mocha</option>
|
||||
<option value="macchiato">Macchiato</option>
|
||||
<option value="frappe">Frappé</option>
|
||||
<option value="latte">Latte</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Top Bar -->
|
||||
<nav class="w-full glass sticky top-0 z-50 border-b border-white/5 px-6 py-3 flex justify-between items-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-mauve to-pink flex items-center justify-center font-bold text-base">?</div>
|
||||
<span class="text-lg font-bold gradient-text tracking-tight">AYTO Matcher</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<select id="theme-select" class="px-3 py-1.5 glass rounded-xl text-xs font-semibold focus:ring-1 ring-blue/50 outline-none border-none">
|
||||
<option value="mocha">Mocha</option>
|
||||
<option value="macchiato">Macchiato</option>
|
||||
<option value="frappe">Frappé</option>
|
||||
<option value="latte">Latte</option>
|
||||
</select>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="max-w-7xl mx-auto p-4 sm:p-8">
|
||||
<!-- Header -->
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-lavender mb-2">Are You The One? Calculator</h1>
|
||||
<p class="text-lg text-subtext1">Find the remaining possibilities and probabilities for your season.</p>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-8 mt-8">
|
||||
<!-- Hero Header -->
|
||||
<header class="text-center mb-12 animate-fade-in">
|
||||
<h1 class="text-5xl font-extrabold tracking-tight mb-4 leading-tight">
|
||||
Unlock the <span class="gradient-text">Perfect Matches</span>
|
||||
</h1>
|
||||
<p class="text-subtext1 max-w-2xl mx-auto text-lg">
|
||||
The ultimate probability engine for "Are You The One?".
|
||||
Our Rust-powered core calculates millions of possibilities in real-time.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Error Message Popup -->
|
||||
<div id="error-message" class="hidden fixed top-5 right-5 bg-red text-base p-4 rounded-md shadow-lg z-50 transition-all duration-300 ease-out opacity-0">
|
||||
<div id="error-message" class="hidden fixed top-20 left-1/2 -translate-x-1/2 glass border-red/20 bg-red/10 text-red px-6 py-3 rounded-2xl shadow-2xl z-[60] backdrop-blur-xl border flex items-center space-x-3 transition-all duration-300">
|
||||
<span class="font-bold">Error</span>
|
||||
<span id="error-text"></span>
|
||||
</div>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<div id="loading-spinner" class="hidden fixed inset-0 bg-base bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue"></div>
|
||||
<p class="ml-4 text-text text-lg">Calculating with Rust Backend...</p>
|
||||
<div id="loading-spinner" class="hidden fixed inset-0 bg-base/60 backdrop-blur-md flex items-center justify-center z-[100]">
|
||||
<div class="glass p-8 rounded-[32px] flex flex-col items-center">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue mb-4"></div>
|
||||
<p class="text-blue font-bold tracking-widest uppercase text-xs">Processing Algorithm</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left Column: Inputs -->
|
||||
<div class="flex flex-col gap-8">
|
||||
<!-- Section 1: Setup Contestants -->
|
||||
<section id="setup-section" class="bg-surface1 p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">1. Setup Contestants</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 xl:grid-cols-12 gap-8 items-start">
|
||||
|
||||
<!-- Sidebar: Setup & Logic -->
|
||||
<div class="xl:col-span-5 space-y-6">
|
||||
|
||||
<!-- 1. Setup -->
|
||||
<section class="glass glass-card p-8 animate-fade-in" style="animation-delay: 0.1s">
|
||||
<div class="flex items-center space-x-3 mb-6">
|
||||
<span class="w-8 h-8 rounded-full bg-blue/10 text-blue flex items-center justify-center font-bold text-sm">1</span>
|
||||
<h2 class="text-xl font-bold">The Cast</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<label for="group1-names" class="block text-sm font-medium text-subtext1 mb-1">Group 1 (Matchers)</label>
|
||||
<textarea id="group1-names" rows="10" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text placeholder-subtext0 focus:ring-blue focus:border-blue" placeholder="Enter one name per line...
|
||||
(Must be <= Group 2)
|
||||
Jake
|
||||
Brett
|
||||
..."></textarea>
|
||||
<label class="block text-xs font-bold text-overlay2 uppercase tracking-widest mb-2">Matchers (G1)</label>
|
||||
<textarea id="group1-names" rows="8" class="w-full input-glass rounded-2xl p-4 text-sm placeholder:text-surface2" placeholder="One name per line..."></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="group2-names" class="block text-sm font-medium text-subtext1 mb-1">Group 2 (Pool)</label>
|
||||
<textarea id="group2-names" rows="10" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text placeholder-subtext0 focus:ring-blue focus:border-blue" placeholder="Enter one name per line...
|
||||
(Must be >= Group 1)
|
||||
Jenna
|
||||
Kayla
|
||||
..."></textarea>
|
||||
<label class="block text-xs font-bold text-overlay2 uppercase tracking-widest mb-2">The Pool (G2)</label>
|
||||
<textarea id="group2-names" rows="8" class="w-full input-glass rounded-2xl p-4 text-sm placeholder:text-surface2" placeholder="One name per line..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="setup-button" class="mt-4 w-full bg-blue hover:bg-sapphire text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Set Contestants
|
||||
<button id="setup-button" class="w-full py-4 rounded-2xl btn-primary font-bold text-sm shadow-lg tracking-wide uppercase">
|
||||
Initialize Season
|
||||
</button>
|
||||
|
||||
<!-- Save/Load Buttons -->
|
||||
<div class="mt-4 grid grid-cols-2 gap-4">
|
||||
<button id="save-button" class="w-full bg-surface2 hover:bg-overlay0 text-subtext1 font-bold py-2 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out" disabled>
|
||||
Save Configuration
|
||||
</button>
|
||||
<button id="load-button" class="w-full bg-surface2 hover:bg-overlay0 text-subtext1 font-bold py-2 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Load Configuration
|
||||
</button>
|
||||
<div class="flex space-x-3 mt-4">
|
||||
<button id="save-button" class="flex-1 py-3 glass rounded-xl text-xs font-bold text-subtext0 hover:text-text transition-colors uppercase disabled:opacity-30" disabled>Save</button>
|
||||
<button id="load-button" class="flex-1 py-3 glass rounded-xl text-xs font-bold text-subtext0 hover:text-text transition-colors uppercase">Load</button>
|
||||
<input type="file" id="file-loader" class="hidden" accept=".json">
|
||||
</div>
|
||||
<!-- Hidden file input for loading -->
|
||||
<input type="file" id="file-loader" class="hidden" accept=".json">
|
||||
|
||||
<p id="initial-possibilities-text" class="text-center mt-3 text-subtext0 text-sm italic"></p>
|
||||
<p id="initial-possibilities-text" class="text-center mt-4 text-xs font-medium text-overlay1 italic"></p>
|
||||
</section>
|
||||
|
||||
<!-- Section 2: Truth Booths -->
|
||||
<section id="truth-booth-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">2. Add Truth Booths</h2>
|
||||
<form id="truth-booth-form" class="grid grid-cols-1 sm:grid-cols-3 gap-4 items-end">
|
||||
<div>
|
||||
<label for="tb-group1" class="block text-sm font-medium text-subtext1 mb-1">Group 1</label>
|
||||
<select id="tb-group1" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text focus:ring-blue focus:border-blue"></select>
|
||||
<!-- 2. Truth Booth -->
|
||||
<section id="truth-booth-section" class="glass glass-card p-8 hidden animate-fade-in">
|
||||
<div class="flex items-center space-x-3 mb-6">
|
||||
<span class="w-8 h-8 rounded-full bg-green/10 text-green flex items-center justify-center font-bold text-sm">2</span>
|
||||
<h2 class="text-xl font-bold">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="input-glass rounded-xl p-3 text-sm focus:ring-0 appearance-none"></select>
|
||||
<select id="tb-group2" class="input-glass rounded-xl p-3 text-sm focus:ring-0 appearance-none"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tb-group2" class="block text-sm font-medium text-subtext1 mb-1">Group 2</label>
|
||||
<select id="tb-group2" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text focus:ring-blue focus:border-blue"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tb-result" class="block text-sm font-medium text-subtext1 mb-1">Result</label>
|
||||
<select id="tb-result" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text focus:ring-blue focus:border-blue">
|
||||
<option value="no-match">No Match</option>
|
||||
<option value="match">Perfect Match</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="sm:col-span-3 w-full bg-green hover:bg-teal text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Add Truth Booth Result
|
||||
</button>
|
||||
<select id="tb-result" class="w-full input-glass rounded-xl p-3 text-sm focus:ring-0 appearance-none">
|
||||
<option value="no-match">No Match</option>
|
||||
<option value="match">Perfect Match</option>
|
||||
</select>
|
||||
<button type="submit" class="w-full py-3 bg-green/20 hover:bg-green/30 text-green rounded-xl font-bold text-sm transition-all uppercase tracking-wider">Add Result</button>
|
||||
</form>
|
||||
<div id="truth-booth-list" class="mt-4 space-y-2"></div>
|
||||
<div id="truth-booth-list" class="mt-6 space-y-2"></div>
|
||||
</section>
|
||||
|
||||
<!-- Section 3: Matchup Ceremonies -->
|
||||
<section id="ceremony-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">3. Add Matchup Ceremonies</h2>
|
||||
<!-- 3. Ceremony -->
|
||||
<section id="ceremony-section" class="glass glass-card p-8 hidden animate-fade-in">
|
||||
<div class="flex items-center space-x-3 mb-6">
|
||||
<span class="w-8 h-8 rounded-full bg-mauve/10 text-mauve flex items-center justify-center font-bold text-sm">3</span>
|
||||
<h2 class="text-xl font-bold">Matchup Ceremony</h2>
|
||||
</div>
|
||||
<form id="ceremony-form">
|
||||
<p class="text-subtext1 mb-4">Set the pairs for this ceremony. Ensure each person from Group 2 is selected at most once.</p>
|
||||
<div id="ceremony-pairs-container" class="space-y-3 mb-4"></div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label for="ceremony-beams" class="block text-sm font-medium text-subtext1 whitespace-nowrap">Correct Beams:</label>
|
||||
<input type="number" id="ceremony-beams" min="0" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text placeholder-subtext0 focus:ring-blue focus:border-blue" placeholder="e.g., 3">
|
||||
<div id="ceremony-pairs-container" class="space-y-2 mb-6 max-h-60 overflow-y-auto pr-2 custom-scrollbar"></div>
|
||||
<div class="glass rounded-2xl p-4 mb-6 flex items-center justify-between">
|
||||
<label class="text-xs font-bold text-overlay2 uppercase tracking-widest">Correct Beams</label>
|
||||
<input type="number" id="ceremony-beams" min="0" value="0" class="w-16 input-glass rounded-xl p-2 text-center font-bold">
|
||||
</div>
|
||||
<button type="submit" class="mt-4 w-full bg-mauve hover:bg-pink text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Add Ceremony Result
|
||||
</button>
|
||||
<button type="submit" class="w-full py-4 bg-mauve/20 hover:bg-mauve/30 text-mauve rounded-2xl font-bold text-sm transition-all uppercase tracking-wider">Record Ceremony</button>
|
||||
</form>
|
||||
<div id="ceremony-list" class="mt-4 space-y-2"></div>
|
||||
<div id="ceremony-list" class="mt-6 space-y-3"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Results -->
|
||||
<div class="flex flex-col gap-8">
|
||||
<!-- Section 4: Calculate -->
|
||||
<section id="calculate-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden sticky top-8">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">4. Calculate Results</h2>
|
||||
<button id="calculate-button" class="w-full bg-blue hover:bg-sapphire text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Calculate Probabilities
|
||||
</button>
|
||||
<!-- Main Panel: Probabilities -->
|
||||
<div class="xl:col-span-7 space-y-6">
|
||||
|
||||
<section id="calculate-section" class="hidden animate-fade-in">
|
||||
<div class="glass glass-card p-1 bg-gradient-to-r from-blue/20 via-transparent to-sapphire/20">
|
||||
<button id="calculate-button" class="w-full py-6 rounded-[22px] text-lg font-extrabold tracking-widest uppercase hover:bg-white/5 transition-all">
|
||||
Run Probability Engine
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="results-display" class="mt-6 text-center hidden">
|
||||
<h3 class="text-lg font-medium text-subtext1">Remaining Possibilities:</h3>
|
||||
<p id="total-possibilities" class="text-5xl font-bold text-text my-2">--</p>
|
||||
<div id="results-display" class="mt-8 grid grid-cols-1 md:grid-cols-3 gap-4 hidden">
|
||||
<div class="glass glass-card p-6 text-center md:col-span-3">
|
||||
<h3 class="text-xs font-bold text-overlay2 uppercase tracking-[0.2em] mb-2">Remaining Possibilities</h3>
|
||||
<p id="total-possibilities" class="text-6xl font-black gradient-text">--</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 5: Probability Grid -->
|
||||
<section id="grid-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">Probability Grid</h2>
|
||||
<p class="text-subtext1 mb-4 text-sm">This table shows the probability of each pair being a "Perfect Match" based on the remaining possibilities.</p>
|
||||
<div id="probability-grid-container" class="table-container overflow-x-auto rounded-lg border border-surface2">
|
||||
<table id="probability-table" class="w-full text-sm text-center">
|
||||
<thead id="probability-table-head" class="bg-surface0 text-subtext1 uppercase tracking-wider"></thead>
|
||||
<tbody id="probability-table-body" class="bg-surface1 divide-y divide-surface2"></tbody>
|
||||
<section id="grid-section" class="glass glass-card p-8 hidden animate-fade-in">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold mb-1">Probability Matrix</h2>
|
||||
<p class="text-xs text-overlay2 font-medium tracking-wide uppercase">Real-time Surjective Mapping</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="probability-grid-container" class="table-container overflow-x-auto rounded-[24px] border border-white/5 bg-black/10">
|
||||
<table id="probability-table" class="w-full text-left">
|
||||
<thead id="probability-table-head" class="bg-white/5 text-[10px] font-black uppercase tracking-[0.2em] text-overlay1"></thead>
|
||||
<tbody id="probability-table-body" class="divide-y divide-white/5"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -373,13 +341,11 @@ Kayla
|
||||
const loadingSpinner = $("loading-spinner");
|
||||
const initialPossibilitiesText = $("initial-possibilities-text");
|
||||
|
||||
// Theme Handling
|
||||
themeSelect.addEventListener('change', (e) => {
|
||||
document.documentElement.setAttribute('data-theme', e.target.value);
|
||||
localStorage.setItem('ayto-theme', e.target.value);
|
||||
});
|
||||
|
||||
// Load saved theme
|
||||
const savedTheme = localStorage.getItem('ayto-theme');
|
||||
if (savedTheme) {
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
@@ -391,32 +357,21 @@ Kayla
|
||||
|
||||
function showError(message) {
|
||||
errorText.textContent = message;
|
||||
errorMessage.classList.remove("hidden", "opacity-0");
|
||||
errorMessage.classList.remove("hidden");
|
||||
errorMessage.classList.remove("opacity-0");
|
||||
setTimeout(() => {
|
||||
errorMessage.classList.add("opacity-0");
|
||||
setTimeout(() => errorMessage.classList.add("hidden"), 3000);
|
||||
}, 3000);
|
||||
setTimeout(() => errorMessage.classList.add("hidden"), 300);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
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) {
|
||||
showError("Please enter names for both groups.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g1.length > g2.length) {
|
||||
showError("Group 1 must have fewer or an equal number of members as Group 2.");
|
||||
return false;
|
||||
}
|
||||
if (g1.length === 0 || g2.length === 0) return showError("The villa is empty! Add names.");
|
||||
if (g1.length > g2.length) return showError("Too many matchers, not enough pool!");
|
||||
|
||||
if (new Set(g1).size !== g1.length || new Set(g2).size !== g2.length) {
|
||||
showError("Duplicate names are not allowed within a group.");
|
||||
return false;
|
||||
}
|
||||
|
||||
group1Names = g1;
|
||||
group2Names = g2;
|
||||
truthBooths = [];
|
||||
@@ -427,32 +382,29 @@ Kayla
|
||||
populateSelectors();
|
||||
populateCeremonyPairBuilder();
|
||||
|
||||
initialPossibilitiesText.textContent = `Calculator ready for ${group1Names.length} (G1) and ${group2Names.length} (G2) contestants.`;
|
||||
initialPossibilitiesText.textContent = `Villa ready: ${group1Names.length} vs ${group2Names.length}`;
|
||||
|
||||
tbSection.classList.remove("hidden");
|
||||
ceremonySection.classList.remove("hidden");
|
||||
calculateSection.classList.remove("hidden");
|
||||
gridSection.classList.remove("hidden");
|
||||
resultsDisplay.classList.add("hidden");
|
||||
totalPossibilitiesText.textContent = '--';
|
||||
probTableHead.innerHTML = '';
|
||||
probTableBody.innerHTML = '';
|
||||
|
||||
saveButton.disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function populateSelectors() {
|
||||
tbGroup1.innerHTML = group1Names.map(name => `<option value="${name}">${name}</option>`).join('');
|
||||
tbGroup2.innerHTML = group2Names.map(name => `<option value="${name}">${name}</option>`).join('');
|
||||
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="grid grid-cols-3 gap-2 items-center">
|
||||
<label class="text-subtext1 text-right col-span-1">${name}</label>
|
||||
<span class="text-overlay0 text-center col-auto">-</span>
|
||||
<select data-g1-name="${name}" class="ceremony-pair-select w-full p-2 bg-surface0 border border-surface2 rounded-md text-text col-span-1 focus:ring-blue focus:border-blue">
|
||||
<div class="flex items-center space-x-3 bg-white/5 p-3 rounded-xl border border-white/5">
|
||||
<span class="text-xs font-bold text-overlay2 w-20 truncate">${name}</span>
|
||||
<select data-g1-name="${name}" class="ceremony-pair-select flex-1 bg-surface0 rounded-lg p-2 text-xs outline-none focus:ring-1 ring-blue/30">
|
||||
<option value="">Select match...</option>
|
||||
${group2Names.map(g2name => `<option value="${g2name}">${g2name}</option>`).join('')}
|
||||
</select>
|
||||
@@ -462,10 +414,10 @@ Kayla
|
||||
|
||||
function renderTruthBoothUI(booth) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `p-3 rounded-md flex justify-between items-center text-base ${booth.isMatch ? 'bg-green' : 'bg-red'}`;
|
||||
el.className = `p-4 rounded-2xl flex justify-between items-center glass animate-fade-in ${booth.isMatch ? 'border-green/20 text-green' : 'border-red/20 text-red'}`;
|
||||
el.innerHTML = `
|
||||
<span class="text-base"><strong>${booth.p1}</strong> & <strong>${booth.p2}</strong> = ${booth.isMatch ? 'PERFECT MATCH' : 'NO MATCH'}</span>
|
||||
<button data-id="${booth.id}" class="remove-tb-btn text-surface0 hover:text-crust text-2xl leading-none font-bold">×</button>
|
||||
<span class="text-xs font-bold uppercase tracking-widest">${booth.p1} + ${booth.p2} = ${booth.isMatch ? 'Match' : 'No'}</span>
|
||||
<button data-id="${booth.id}" class="remove-tb-btn opacity-50 hover:opacity-100">×</button>
|
||||
`;
|
||||
tbList.appendChild(el);
|
||||
el.querySelector('.remove-tb-btn').addEventListener('click', handleRemoveTruthBooth);
|
||||
@@ -476,7 +428,6 @@ Kayla
|
||||
const booth = { id: Date.now(), p1: tbGroup1.value, p2: tbGroup2.value, isMatch: tbResult.value === 'match' };
|
||||
truthBooths.push(booth);
|
||||
renderTruthBoothUI(booth);
|
||||
tbForm.reset();
|
||||
}
|
||||
|
||||
function handleRemoveTruthBooth(e) {
|
||||
@@ -487,14 +438,19 @@ Kayla
|
||||
|
||||
function renderCeremonyUI(ceremony, index) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'p-3 rounded-md bg-surface2 ceremony-item';
|
||||
el.className = 'p-5 rounded-2xl glass border-white/5 ceremony-item animate-fade-in';
|
||||
el.innerHTML = `
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-semibold text-mauve"><strong>Ceremony ${index}</strong>: ${ceremony.beams} Beam${ceremony.beams === 1 ? '' : 's'}</span>
|
||||
<button data-id="${ceremony.id}" class="remove-ceremony-btn text-red hover:text-maroon text-2xl leading-none font-bold">×</button>
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<span class="text-xs font-black uppercase tracking-[0.2em] text-mauve">Ceremony ${index} • ${ceremony.beams} Beams</span>
|
||||
<button data-id="${ceremony.id}" class="remove-ceremony-btn text-red/50 hover:text-red transition-colors">×</button>
|
||||
</div>
|
||||
<div class="text-sm text-subtext0 mt-2 pl-4">
|
||||
${Object.entries(ceremony.pairs).map(([p1, p2]) => `<div>${p1} - ${p2}</div>`).join('')}
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
${Object.entries(ceremony.pairs).map(([p1, p2]) => `
|
||||
<div class="text-[10px] bg-white/5 p-1.5 rounded-lg border border-white/5 text-overlay1 flex justify-between">
|
||||
<span class="font-bold">${p1}</span>
|
||||
<span>${p2}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
ceremonyList.appendChild(el);
|
||||
@@ -508,39 +464,29 @@ Kayla
|
||||
const selectedG2Names = [];
|
||||
|
||||
for (const select of pairSelectors) {
|
||||
const g1Name = select.dataset.g1Name;
|
||||
const g2Name = select.value;
|
||||
if (!g2Name) return showError(`Please select a match for ${g1Name}.`);
|
||||
if (!g2Name) return showError("Incomplete ceremony! Pair everyone.");
|
||||
selectedG2Names.push(g2Name);
|
||||
pairs[g1Name] = g2Name;
|
||||
pairs[select.dataset.g1Name] = g2Name;
|
||||
}
|
||||
|
||||
if (new Set(selectedG2Names).size !== selectedG2Names.length) {
|
||||
return showError("Each person from Group 2 can only be matched once per ceremony.");
|
||||
}
|
||||
if (new Set(selectedG2Names).size !== selectedG2Names.length) return showError("Duplicate selection in ceremony!");
|
||||
|
||||
const beams = parseInt(ceremonyBeams.value, 10);
|
||||
if (isNaN(beams) || beams < 0 || beams > group1Names.length) {
|
||||
return showError(`Beams must be a number between 0 and ${group1Names.length}.`);
|
||||
}
|
||||
|
||||
const ceremony = { id: Date.now(), pairs, beams };
|
||||
ceremonies.push(ceremony);
|
||||
renderCeremonyUI(ceremony, ceremonies.length);
|
||||
ceremonyForm.reset();
|
||||
}
|
||||
|
||||
function handleRemoveCeremony(e) {
|
||||
const idToRemove = Number(e.target.dataset.id);
|
||||
ceremonies = ceremonies.filter(c => c.id !== idToRemove);
|
||||
e.target.closest('.ceremony-item').remove();
|
||||
ceremonyList.innerHTML = '';
|
||||
ceremonies.forEach((c, i) => renderCeremonyUI(c, i + 1));
|
||||
}
|
||||
|
||||
async function handleCalculate() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
group1: group1Names,
|
||||
@@ -555,26 +501,22 @@ Kayla
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Server error: ${res.statusText}`);
|
||||
}
|
||||
|
||||
if (!res.ok) throw new Error("Backend malfunction");
|
||||
const results = await res.json();
|
||||
|
||||
totalPossibilitiesText.textContent = results.possibilities.toLocaleString();
|
||||
resultsDisplay.classList.remove("hidden");
|
||||
|
||||
if (results.possibilities === 0) {
|
||||
showError("Impossible scenario. The data entered contradicts itself.");
|
||||
showError("Impossible scenario detected!");
|
||||
probTableBody.innerHTML = '';
|
||||
probTableHead.innerHTML = '';
|
||||
} else {
|
||||
displayProbabilityGrid(results.grid_data);
|
||||
gridSection.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showError("An error occurred connecting to the backend API.");
|
||||
showError("Connection failed");
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
@@ -582,70 +524,50 @@ Kayla
|
||||
|
||||
function displayProbabilityGrid(gridData) {
|
||||
probTableHead.innerHTML = `
|
||||
<tr class="sticky top-0 bg-surface0 z-10">
|
||||
<th class="p-3"></th>
|
||||
${gridData.columns.map(name => `<th class="p-3 whitespace-nowrap">${name}</th>`).join('')}
|
||||
<tr>
|
||||
<th class="p-5 text-[10px] border-b border-white/5"></th>
|
||||
${gridData.columns.map(name => `<th class="p-5 text-[10px] border-b border-white/5 whitespace-nowrap">${name}</th>`).join('')}
|
||||
</tr>
|
||||
`;
|
||||
|
||||
probTableBody.innerHTML = gridData.index.map((g1Name, rowIndex) => `
|
||||
<tr class="hover:bg-surface2">
|
||||
<th class="p-3 whitespace-nowrap bg-surface0 sticky left-0 z-5">${g1Name}</th>
|
||||
<tr class="hover:bg-white/[0.02] transition-colors">
|
||||
<th class="p-5 text-[10px] font-black border-r border-white/5 bg-white/[0.01] sticky left-0 z-10">${g1Name}</th>
|
||||
${gridData.columns.map((g2Name, colIndex) => {
|
||||
const prob = gridData.data[rowIndex][colIndex] * 100;
|
||||
let cellClass = 'prob-possible';
|
||||
if (prob === 100) cellClass = 'prob-100';
|
||||
else if (prob === 0) cellClass = 'prob-0';
|
||||
return `<td class="${cellClass} p-3">${prob.toFixed(1)}%</td>`;
|
||||
if (prob >= 99.9) cellClass = 'prob-100';
|
||||
else if (prob <= 0.1) cellClass = 'prob-0';
|
||||
return `<td class="${cellClass} p-5 text-sm font-bold border-white/5">${prob.toFixed(1)}%</td>`;
|
||||
}).join('')}
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function handleSaveConfig() {
|
||||
if (group1Names.length === 0) return showError("Please set contestants before saving.");
|
||||
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-save.json';
|
||||
document.body.appendChild(a);
|
||||
a.href = url; a.download = 'ayto-save.json';
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function handleLoadConfig() { fileLoader.click(); }
|
||||
|
||||
function handleFileSelected(e) {
|
||||
fileLoader.addEventListener("change", (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try { loadState(JSON.parse(event.target.result)); }
|
||||
catch (err) { showError("Failed to load file."); }
|
||||
reader.onload = (ev) => {
|
||||
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));
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = null;
|
||||
}
|
||||
|
||||
function loadState(state) {
|
||||
if (!state.group1Names || !state.group2Names || !state.truthBooths || !state.ceremonies) {
|
||||
return showError("Invalid save file. Required fields are missing.");
|
||||
}
|
||||
g1NamesText.value = state.group1Names.join('\n');
|
||||
g2NamesText.value = state.group2Names.join('\n');
|
||||
if (!handleSetupContestants()) return;
|
||||
|
||||
truthBooths = state.truthBooths;
|
||||
ceremonies = state.ceremonies;
|
||||
|
||||
truthBooths.forEach(renderTruthBoothUI);
|
||||
ceremonies.forEach((c, i) => renderCeremonyUI(c, i + 1));
|
||||
|
||||
handleCalculate();
|
||||
}
|
||||
});
|
||||
|
||||
setupButton.addEventListener("click", handleSetupContestants);
|
||||
tbForm.addEventListener("submit", handleAddTruthBooth);
|
||||
@@ -653,8 +575,6 @@ Kayla
|
||||
calculateButton.addEventListener("click", handleCalculate);
|
||||
saveButton.addEventListener("click", handleSaveConfig);
|
||||
loadButton.addEventListener("click", handleLoadConfig);
|
||||
fileLoader.addEventListener("change", handleFileSelected);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user