Kuldvillak MVP ei forki, Randel, fork you Randel Mandre SASS license peal
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1007 lines
33 KiB

<script lang="ts">
import { goto } from "$app/navigation";
import { onMount } from "svelte";
import { browser } from "$app/environment";
import { Toast, Settings } from "$lib/components";
import * as m from "$lib/paraglide/messages";
import { gameSession } from "$lib/stores/gameSession.svelte";
import { themeStore } from "$lib/stores/theme.svelte";
import type {
GameSettings,
Team,
Round,
Category,
Question,
FinalRound,
PointValuePreset,
} from "$lib/types/kuldvillak";
import { DEFAULT_SETTINGS } from "$lib/types/kuldvillak";
const AUTOSAVE_KEY = "kuldvillak-editor-autosave";
// State
let settings = $state<GameSettings>({
...DEFAULT_SETTINGS,
defaultTimerSeconds: 5,
answerRevealSeconds: 5,
});
let teams = $state<Team[]>([
{ id: generateId(), name: "Mängija 1", score: 0 },
]);
let rounds = $state<Round[]>([
createRound("Villak", 1, DEFAULT_SETTINGS),
createRound("Topeltvillak", 2, DEFAULT_SETTINGS),
]);
let finalRound = $state<FinalRound>({
category: "",
question: "",
answer: "",
});
let gameName = $state("");
let editingQuestion = $state<{
roundIndex: number;
catIndex: number;
qIndex: number;
} | null>(null);
let editingFinalQuestion = $state(false);
let settingsOpen = $state(false);
// Toast state
let toastMessage = $state("");
let toastType = $state<"error" | "success">("error");
let toastVisible = $state(false);
// File input ref
let fileInput: HTMLInputElement;
// Autosave to localStorage
function autoSave() {
if (!browser) return;
const data = { name: gameName, settings, teams, rounds, finalRound };
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(data));
}
// Load from localStorage on mount
onMount(() => {
const saved = localStorage.getItem(AUTOSAVE_KEY);
if (saved) {
try {
const data = JSON.parse(saved);
if (data.settings && data.teams && data.rounds) {
gameName = data.name || "";
const { teamColors, ...cleanSettings } = data.settings;
settings = { ...DEFAULT_SETTINGS, ...cleanSettings };
teams = data.teams.map((t: any) => ({
id: t.id,
name: t.name,
score: t.score ?? 0,
}));
rounds = data.rounds;
finalRound = data.finalRound || {
category: "",
question: "",
answer: "",
};
}
} catch {
/* ignore parse errors */
}
}
});
// Auto-save on any state change
$effect(() => {
const _ = [gameName, settings, teams, rounds, finalRound];
autoSave();
});
function showToast(message: string, type: "error" | "success" = "error") {
toastMessage = message;
toastType = type;
toastVisible = true;
}
function generateId(): string {
return Math.random().toString(36).substring(2, 11);
}
function createQuestion(points: number): Question {
return {
id: generateId(),
question: "",
answer: "",
points,
isDailyDouble: false,
isRevealed: false,
};
}
function createCategory(s: GameSettings, multiplier: number = 1): Category {
return {
id: generateId(),
name: "",
questions: s.pointValues.map((p) => createQuestion(p * multiplier)),
};
}
function createRound(
name: string,
multiplier: number,
s: GameSettings,
): Round {
return {
id: generateId(),
name,
categories: Array.from({ length: s.categoriesPerRound }, () =>
createCategory(s, multiplier),
),
pointMultiplier: multiplier,
};
}
function getPointsForRound(
preset: PointValuePreset,
roundIndex: number,
): number[] {
const base = [10, 20, 30, 40, 50];
const multiplier = roundIndex + 1;
if (preset === "round1") return base.map((v) => v * multiplier);
if (preset === "round2") return base.map((v) => v * 2 * multiplier);
if (preset === "multiplier")
return [1, 2, 3, 4, 5].map(
(i) => settings.basePointValue * i * multiplier,
);
if (preset === "custom")
return settings.pointValues.map((v) => v * multiplier);
return settings.pointValues;
}
function updatePreset(preset: PointValuePreset) {
settings.pointValuePreset = preset;
if (preset === "round1") {
settings.pointValues = [10, 20, 30, 40, 50];
}
updateQuestionPoints();
}
function updateQuestionPoints() {
rounds.forEach((round, ri) => {
const points = getPointsForRound(settings.pointValuePreset, ri);
round.categories.forEach((cat) =>
cat.questions.forEach((q, i) => {
q.points = points[i] ?? q.points;
}),
);
});
rounds = [...rounds];
}
function setRoundCount(count: 1 | 2) {
settings.numberOfRounds = count;
if (count === 1 && rounds.length > 1) {
rounds = [rounds[0]];
settings.dailyDoublesPerRound = [settings.dailyDoublesPerRound[0]];
} else if (count === 2 && rounds.length === 1) {
rounds = [...rounds, createRound("Topeltvillak", 2, settings)];
settings.dailyDoublesPerRound = [
settings.dailyDoublesPerRound[0],
2,
];
}
updateQuestionPoints();
}
function addTeam() {
if (teams.length >= 6) return;
teams = [
...teams,
{ id: generateId(), name: `Mängija ${teams.length + 1}`, score: 0 },
];
}
function removeTeam(id: string) {
if (teams.length <= 1) return;
teams = teams.filter((t) => t.id !== id);
}
function countDailyDoubles(roundIndex: number): number {
return rounds[roundIndex].categories.reduce(
(sum, cat) =>
sum + cat.questions.filter((q) => q.isDailyDouble).length,
0,
);
}
function validateGame(): string | null {
if (teams.length < 2) return m.kv_error_min_players();
for (let i = 0; i < rounds.length; i++) {
const hasQuestions = rounds[i].categories.some((cat) =>
cat.questions.some((q) => q.question.trim()),
);
if (!hasQuestions)
return m.kv_error_no_questions({ round: String(i + 1) });
}
if (settings.enableFinalRound && !finalRound.question.trim())
return m.kv_error_no_final();
return null;
}
let isStarting = $state(false);
async function startGame() {
const error = validateGame();
if (error) {
showToast(error, "error");
return;
}
isStarting = true;
try {
gameSession.startGame({
name: gameName,
settings,
teams,
rounds,
finalRound: settings.enableFinalRound ? finalRound : null,
});
window.open("/kuldvillak/play?view=projector", "_blank");
await goto("/kuldvillak/play");
} catch (err) {
showToast("Failed to start game: " + err, "error");
isStarting = false;
}
}
function saveGame() {
const game = { name: gameName, settings, teams, rounds, finalRound };
const blob = new Blob([JSON.stringify(game, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${gameName.replace(/\s+/g, "_") || "game"}.json`;
a.click();
URL.revokeObjectURL(url);
showToast(m.kv_toast_game_saved(), "success");
}
function resetGame() {
if (!confirm(m.kv_edit_reset_confirm())) return;
settings = {
...DEFAULT_SETTINGS,
defaultTimerSeconds: 5,
answerRevealSeconds: 5,
};
teams = [{ id: generateId(), name: "Mängija 1", score: 0 }];
rounds = [
createRound("Villak", 1, DEFAULT_SETTINGS),
createRound("Topeltvillak", 2, DEFAULT_SETTINGS),
];
finalRound = { category: "", question: "", answer: "" };
gameName = "";
localStorage.removeItem(AUTOSAVE_KEY);
showToast(m.kv_edit_reset_success(), "success");
}
function loadGame(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
if (data.settings && data.teams && data.rounds) {
gameName = data.name || "Loaded Game";
const { teamColors, ...cleanSettings } = data.settings;
settings = { ...DEFAULT_SETTINGS, ...cleanSettings };
teams = data.teams.map((t: any) => ({
id: t.id,
name: t.name,
score: t.score ?? 0,
}));
rounds = data.rounds;
finalRound = data.finalRound || {
category: "",
question: "",
answer: "",
};
showToast(m.kv_toast_game_loaded(), "success");
} else {
showToast(m.kv_toast_invalid_file(), "error");
}
} catch {
showToast(m.kv_toast_invalid_file(), "error");
}
};
reader.readAsText(file);
(event.target as HTMLInputElement).value = "";
}
function toggleDailyDouble() {
if (!editingQuestion) return;
const { roundIndex, catIndex, qIndex } = editingQuestion;
const q = rounds[roundIndex].categories[catIndex].questions[qIndex];
const maxDD = settings.dailyDoublesPerRound[roundIndex] ?? 1;
const currentCount = countDailyDoubles(roundIndex);
if (q.isDailyDouble) {
q.isDailyDouble = false;
} else if (currentCount < maxDD) {
q.isDailyDouble = true;
}
rounds = [...rounds];
}
function saveQuestion() {
editingQuestion = null;
}
function getEditingQuestion() {
if (!editingQuestion) return null;
return rounds[editingQuestion.roundIndex].categories[
editingQuestion.catIndex
].questions[editingQuestion.qIndex];
}
function getEditingCategory() {
if (!editingQuestion) return null;
return rounds[editingQuestion.roundIndex].categories[
editingQuestion.catIndex
];
}
</script>
<!-- Main Layout -->
<div class="min-h-screen bg-kv-black flex flex-col gap-2 md:gap-4 p-2 md:p-4">
<!-- Header -->
<header
class="bg-kv-blue flex flex-wrap items-center gap-4 lg:gap-8 p-2 md:p-4"
>
<div class="flex items-center gap-4 flex-1">
<a
href="/kuldvillak"
class="block w-12 h-12 hover:opacity-80 transition-opacity text-kv-yellow"
>
<svg
viewBox="0 0 48 48"
fill="currentColor"
class="w-full h-full"
>
<path
d="M29.5334 40L13.5334 24L29.5334 8L33.2668 11.7333L21.0001 24L33.2668 36.2667L29.5334 40Z"
/>
</svg>
</a>
<div class="flex-1 px-4 py-4 bg-kv-black/50">
<input
type="text"
bind:value={gameName}
placeholder={m.kv_edit_game_name()}
class="w-full bg-transparent border-none outline-none font-kv-body text-2xl text-kv-white uppercase kv-shadow-text placeholder:text-kv-white/50"
/>
</div>
</div>
<div class="flex items-center gap-8">
<button
onclick={() => fileInput.click()}
class="w-10 h-10 hover:opacity-80 transition-opacity bg-transparent border-none p-0 cursor-pointer text-kv-yellow"
>
<svg
viewBox="0 0 40 40"
fill="currentColor"
class="w-full h-full"
>
<path
d="M5.41634 33.3332C4.49967 33.3332 3.71495 33.0068 3.06217 32.354C2.4094 31.7012 2.08301 30.9165 2.08301 29.9998V9.99984C2.08301 9.08317 2.4094 8.29845 3.06217 7.64567C3.71495 6.99289 4.49967 6.6665 5.41634 6.6665H15.4163L18.7497 9.99984H32.083C32.9997 9.99984 33.7844 10.3262 34.4372 10.979C35.09 11.6318 35.4163 12.4165 35.4163 13.3332H17.3747L14.0413 9.99984H5.41634V29.9998L9.41634 16.6665H37.9163L33.6247 30.9582C33.4025 31.6804 32.9927 32.2568 32.3955 32.6873C31.7983 33.1179 31.1386 33.3332 30.4163 33.3332H5.41634ZM8.91634 29.9998H30.4163L33.4163 19.9998H11.9163L8.91634 29.9998Z"
/>
</svg>
</button>
<button
onclick={saveGame}
class="w-10 h-10 hover:opacity-80 transition-opacity bg-transparent border-none p-0 cursor-pointer text-kv-yellow"
>
<svg
viewBox="0 0 40 40"
fill="currentColor"
class="w-full h-full"
>
<path
d="M35 11.6667V31.6667C35 32.5833 34.6736 33.3681 34.0208 34.0208C33.3681 34.6736 32.5833 35 31.6667 35H8.33333C7.41667 35 6.63194 34.6736 5.97917 34.0208C5.32639 33.3681 5 32.5833 5 31.6667V8.33333C5 7.41667 5.32639 6.63194 5.97917 5.97917C6.63194 5.32639 7.41667 5 8.33333 5H28.3333L35 11.6667ZM31.6667 13.0833L26.9167 8.33333H8.33333V31.6667H31.6667V13.0833ZM20 30C21.3889 30 22.5694 29.5139 23.5417 28.5417C24.5139 27.5694 25 26.3889 25 25C25 23.6111 24.5139 22.4306 23.5417 21.4583C22.5694 20.4861 21.3889 20 20 20C18.6111 20 17.4306 20.4861 16.4583 21.4583C15.4861 22.4306 15 23.6111 15 25C15 26.3889 15.4861 27.5694 16.4583 28.5417C17.4306 29.5139 18.6111 30 20 30ZM10 16.6667H25V10H10V16.6667Z"
/>
</svg>
</button>
<button
onclick={resetGame}
class="w-10 h-10 hover:opacity-80 transition-opacity bg-transparent border-none p-0 cursor-pointer text-kv-yellow"
>
<svg
viewBox="0 0 40 40"
fill="currentColor"
class="w-full h-full"
>
<path
d="M20.0837 33.3332C16.3614 33.3332 13.1948 32.0415 10.5837 29.4582C7.97255 26.8748 6.66699 23.7221 6.66699 19.9998V19.7082L4.00033 22.3748L1.66699 20.0415L8.33366 13.3748L15.0003 20.0415L12.667 22.3748L10.0003 19.7082V19.9998C10.0003 22.7776 10.9795 25.1387 12.9378 27.0832C14.8962 29.0276 17.2781 29.9998 20.0837 29.9998C20.8059 29.9998 21.5142 29.9165 22.2087 29.7498C22.9031 29.5832 23.5837 29.3332 24.2503 28.9998L26.7503 31.4998C25.6948 32.111 24.6114 32.5693 23.5003 32.8748C22.3892 33.1804 21.2503 33.3332 20.0837 33.3332ZM31.667 26.6248L25.0003 19.9582L27.3337 17.6248L30.0003 20.2915V19.9998C30.0003 17.2221 29.0212 14.8609 27.0628 12.9165C25.1045 10.9721 22.7225 9.99984 19.917 9.99984C19.1948 9.99984 18.4864 10.0832 17.792 10.2498C17.0975 10.4165 16.417 10.6665 15.7503 10.9998L13.2503 8.49984C14.3059 7.88873 15.3892 7.43039 16.5003 7.12484C17.6114 6.81928 18.7503 6.6665 19.917 6.6665C23.6392 6.6665 26.8059 7.95817 29.417 10.5415C32.0281 13.1248 33.3337 16.2776 33.3337 19.9998V20.2915L36.0003 17.6248L38.3337 19.9582L31.667 26.6248Z"
/>
</svg>
</button>
<button
onclick={() => (settingsOpen = true)}
class="w-10 h-10 hover:opacity-80 transition-opacity bg-transparent border-none p-0 cursor-pointer text-kv-yellow"
>
<svg
viewBox="0 0 24 24"
fill="currentColor"
class="w-full h-full"
>
<path
d="M10.0846 19L9.80597 16.76C9.65506 16.7017 9.51285 16.6317 9.37935 16.55C9.24585 16.4683 9.11526 16.3808 8.98756 16.2875L6.91542 17.1625L5 13.8375L6.79353 12.4725C6.78192 12.3908 6.77612 12.3121 6.77612 12.2363V11.7638C6.77612 11.6879 6.78192 11.6092 6.79353 11.5275L5 10.1625L6.91542 6.8375L8.98756 7.7125C9.11526 7.61917 9.24876 7.53167 9.38806 7.45C9.52736 7.36833 9.66667 7.29833 9.80597 7.24L10.0846 5H13.9154L14.194 7.24C14.3449 7.29833 14.4871 7.36833 14.6206 7.45C14.7541 7.53167 14.8847 7.61917 15.0124 7.7125L17.0846 6.8375L19 10.1625L17.2065 11.5275C17.2181 11.6092 17.2239 11.6879 17.2239 11.7638V12.2363C17.2239 12.3121 17.2123 12.3908 17.1891 12.4725L18.9826 13.8375L17.0672 17.1625L15.0124 16.2875C14.8847 16.3808 14.7512 16.4683 14.6119 16.55C14.4726 16.6317 14.3333 16.7017 14.194 16.76L13.9154 19H10.0846ZM12 14.5C12.6944 14.5 13.2847 14.2569 13.7708 13.7708C14.2569 13.2847 14.5 12.6944 14.5 12C14.5 11.3056 14.2569 10.7153 13.7708 10.2292C13.2847 9.74306 12.6944 9.5 12 9.5C11.3056 9.5 10.7153 9.74306 10.2292 10.2292C9.74306 10.7153 9.5 11.3056 9.5 12C9.5 12.6944 9.74306 13.2847 10.2292 13.7708C10.7153 14.2569 11.3056 14.5 12 14.5Z"
/>
</svg>
</button>
</div>
<input
type="file"
accept=".json"
class="hidden"
bind:this={fileInput}
onchange={loadGame}
/>
<button
onclick={startGame}
disabled={isStarting}
class="bg-kv-yellow px-6 py-4 font-kv-body text-2xl text-black uppercase cursor-pointer hover:opacity-90 transition-opacity disabled:opacity-50 border-none kv-shadow-button"
>
{isStarting ? "⏳" : "▶"}
{m.kv_edit_start()}
</button>
</header>
<!-- Settings Panel -->
<section class="bg-kv-blue p-2 md:p-4 overflow-x-auto">
<!-- Title Row with Buttons -->
<div
class="flex flex-wrap items-center justify-between gap-4 mb-4 md:mb-8"
>
<h2
class="font-kv-body text-lg md:text-[28px] text-kv-white uppercase kv-shadow-text m-0"
>
{m.kv_edit_settings_teams()}
</h2>
<div class="flex gap-2">
<button
class="px-4 py-2 bg-kv-yellow font-kv-body text-sm md:text-lg text-black uppercase cursor-pointer border-none kv-shadow-button hover:opacity-90"
>
{m.kv_edit_rules()}
</button>
<button
class="px-4 py-2 bg-kv-yellow font-kv-body text-sm md:text-lg text-black uppercase cursor-pointer border-none kv-shadow-button hover:opacity-90"
>
{m.kv_edit_how_to()}
</button>
</div>
</div>
<div class="flex flex-wrap gap-4 lg:gap-16 xl:gap-32">
<!-- Left Options -->
<div class="flex gap-8">
<!-- Labels Column -->
<div class="flex flex-col gap-4">
<div class="h-12 flex items-center">
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_edit_rounds()}</span
>
</div>
<div class="h-12 flex items-center">
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_play_timer()}</span
>
</div>
<div class="h-12 flex items-center">
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_play_timer_reveal()}</span
>
</div>
<div class="h-12 flex items-center">
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_edit_final_round()}</span
>
</div>
</div>
<!-- Values Column -->
<div class="flex flex-col gap-4">
<!-- Round count -->
<div class="h-12 flex items-center">
<button
onclick={() => setRoundCount(1)}
class="w-12 h-12 border-4 border-kv-black flex items-center justify-center font-kv-body text-xl uppercase cursor-pointer bg-transparent transition-colors {settings.numberOfRounds ===
1
? 'text-kv-yellow'
: 'text-kv-white'}">1</button
>
<button
onclick={() => setRoundCount(2)}
class="w-12 h-12 border-4 border-kv-black flex items-center justify-center font-kv-body text-xl uppercase cursor-pointer bg-transparent transition-colors {settings.numberOfRounds ===
2
? 'text-kv-yellow'
: 'text-kv-white'}">2</button
>
</div>
<!-- Answer time -->
<div class="h-12 flex items-center gap-2">
<div
class="w-12 h-12 border-4 border-kv-black flex items-center justify-center"
>
<input
type="number"
bind:value={settings.defaultTimerSeconds}
min={1}
max={60}
class="w-full h-full bg-transparent border-none outline-none font-kv-body text-xl text-kv-white text-center uppercase [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
</div>
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_play_seconds()}</span
>
</div>
<!-- Reveal time -->
<div class="h-12 flex items-center gap-2">
<div
class="w-12 h-12 border-4 border-kv-black flex items-center justify-center"
>
<input
type="number"
bind:value={settings.answerRevealSeconds}
min={1}
max={60}
class="w-full h-full bg-transparent border-none outline-none font-kv-body text-xl text-kv-white text-center uppercase [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
</div>
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_play_seconds()}</span
>
</div>
<!-- Final round -->
<div class="h-12 flex items-center">
<button
onclick={() =>
settings.enableFinalRound
? (editingFinalQuestion = true)
: (settings.enableFinalRound = true)}
class="px-4 py-2 bg-kv-yellow font-kv-body text-base text-black uppercase cursor-pointer border-none kv-shadow-button hover:opacity-90"
>
{settings.enableFinalRound
? m.kv_edit_question()
: m.kv_edit_disabled()}
</button>
</div>
</div>
</div>
<!-- Middle Options -->
<div class="flex gap-8">
<!-- Labels -->
<div class="flex flex-col gap-4">
<div class="h-12 flex items-center">
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_edit_points()}</span
>
</div>
<div class="h-12 flex items-center">
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_edit_values()}</span
>
</div>
<div class="h-12 flex items-center">
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_edit_negative_scores()}</span
>
</div>
<div class="h-12 flex items-center">
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>{m.kv_edit_teams_label()}</span
>
</div>
</div>
<!-- Values -->
<div class="flex flex-col gap-4">
<!-- Points preset toggle -->
<div class="h-12 flex items-center">
<button
onclick={() => updatePreset("round1")}
class="px-4 h-12 border-4 border-kv-black flex items-center justify-center font-kv-body text-xl uppercase cursor-pointer bg-transparent transition-colors {settings.pointValuePreset ===
'round1'
? 'text-kv-yellow bg-kv-blue'
: 'text-kv-white'}"
>{m.kv_edit_preset_normal()}</button
>
<button
onclick={() => updatePreset("custom")}
class="px-4 h-12 border-4 border-kv-black flex items-center justify-center font-kv-body text-xl uppercase cursor-pointer bg-transparent transition-colors {settings.pointValuePreset ===
'custom'
? 'text-kv-yellow bg-kv-blue'
: 'text-kv-white'}">{m.kv_edit_custom()}</button
>
</div>
<!-- Point values -->
<div class="h-12 flex items-center">
{#each settings.pointValues as val, i}
<div
class="w-12 h-12 border-4 border-kv-black flex items-center justify-center"
>
{#if settings.pointValuePreset === "custom"}
<input
type="number"
bind:value={settings.pointValues[i]}
onchange={updateQuestionPoints}
min={1}
max={9999}
class="w-full h-full bg-transparent border-none outline-none font-kv-body text-lg text-kv-white text-center [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
/>
{:else}
<span
class="font-kv-body text-lg text-kv-white/50"
>{val}</span
>
{/if}
</div>
{/each}
</div>
<!-- Negative scores -->
<div class="h-12 flex items-center">
<button
onclick={() =>
(settings.allowNegativeScores =
!settings.allowNegativeScores)}
class="w-8 h-8 border-none cursor-pointer p-0 rounded-sm flex items-center justify-center {settings.allowNegativeScores
? 'bg-kv-yellow'
: 'bg-white'}"
>
{#if settings.allowNegativeScores}
<span class="text-black text-lg font-bold"
></span
>
{/if}
</button>
</div>
<!-- Players -->
<div class="h-12 flex items-center gap-2 flex-wrap">
{#each teams as team (team.id)}
<div
class="flex items-center h-12 border-4 border-kv-black px-2"
>
<input
type="text"
bind:value={team.name}
class="bg-transparent border-none outline-none font-kv-body text-xl text-kv-yellow uppercase min-w-[80px] max-w-[120px] kv-shadow-text"
/>
<button
onclick={() => removeTeam(team.id)}
class="w-8 h-8 bg-transparent border-none cursor-pointer p-0 flex items-center justify-center hover:opacity-70 flex-shrink-0 text-kv-yellow"
>
<svg
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
d="M6.6188 19.4811L4.5188 17.3811L9.9188 11.9811L4.5188 6.61855L6.6188 4.51855L12.0188 9.91855L17.3813 4.51855L19.4813 6.61855L14.0813 11.9811L19.4813 17.3811L17.3813 19.4811L12.0188 14.0811L6.6188 19.4811Z"
/>
</svg>
</button>
</div>
{/each}
{#if teams.length < 6}
<button
onclick={addTeam}
disabled={teams.length >= 6}
class="w-10 h-10 bg-transparent border-none cursor-pointer p-0 hover:opacity-70 flex items-center justify-center text-kv-yellow"
>
<svg
viewBox="0 0 48 48"
fill="currentColor"
class="w-8 h-8"
>
<path
d="M22.2857 25.7143H12V22.2857H22.2857V12H25.7143V22.2857H36V25.7143H25.7143V36H22.2857V25.7143Z"
/>
</svg>
</button>
{/if}
</div>
</div>
</div>
</div>
</section>
<!-- Game Boards -->
<div class="flex flex-col lg:flex-row gap-2 md:gap-4 flex-1">
{#each rounds as round, ri}
<section class="flex-1 flex flex-col bg-kv-black">
<!-- Round Header -->
<div class="bg-kv-blue p-4">
<h2
class="font-kv-body text-[28px] uppercase kv-shadow-text m-0"
>
<span class="text-kv-white"
>{ri === 0 ? "Villak" : "Topeltvillak"}</span
>
<span class="text-kv-yellow text-xl ml-2"
>({m.kv_edit_dd_count()}
{countDailyDoubles(ri)}/{settings
.dailyDoublesPerRound[ri] ?? 1})</span
>
</h2>
</div>
<!-- Categories -->
<div
class="grid grid-cols-3 md:grid-cols-6 gap-1 md:gap-2 bg-kv-black py-2"
>
{#each round.categories as cat, ci}
<div class="bg-kv-blue py-2">
<input
type="text"
bind:value={cat.name}
placeholder={m.kv_edit_category()}
class="w-full bg-transparent border-none outline-none font-kv-body text-sm text-kv-white text-center uppercase kv-shadow-text placeholder:text-kv-white/50"
/>
</div>
{/each}
</div>
<!-- Questions Grid -->
<div
class="grid grid-cols-3 md:grid-cols-6 gap-1 md:gap-2 bg-kv-black flex-1"
>
{#each { length: settings.questionsPerCategory } as _, qi}
{#each round.categories as cat, ci}
{@const q = cat.questions[qi]}
<button
onclick={() =>
(editingQuestion = {
roundIndex: ri,
catIndex: ci,
qIndex: qi,
})}
class="bg-kv-blue flex items-center justify-center cursor-pointer border-none transition-opacity relative
{q.question.trim() ? 'opacity-100' : 'opacity-50'}
{q.isDailyDouble ? 'ring-2 ring-inset ring-kv-yellow' : ''}"
>
<span
class="font-kv-price text-kv-yellow text-4xl kv-shadow-price"
>{q.points}</span
>
</button>
{/each}
{/each}
</div>
</section>
{/each}
</div>
</div>
<!-- Question Edit Modal -->
{#if editingQuestion}
{@const q =
rounds[editingQuestion.roundIndex].categories[editingQuestion.catIndex]
.questions[editingQuestion.qIndex]}
{@const cat =
rounds[editingQuestion.roundIndex].categories[editingQuestion.catIndex]}
{@const maxDD =
settings.dailyDoublesPerRound[editingQuestion.roundIndex] ?? 1}
{@const currentDD = countDailyDoubles(editingQuestion.roundIndex)}
<div
class="fixed inset-0 bg-kv-black/80 flex items-center justify-center z-50 p-2 md:p-8"
onclick={(e) => e.target === e.currentTarget && saveQuestion()}
onkeydown={(e) => e.key === "Escape" && saveQuestion()}
role="dialog"
tabindex="-1"
>
<div
class="bg-kv-blue border-8 md:border-[16px] border-kv-black p-4 md:p-8 w-full max-w-2xl flex flex-col gap-4 md:gap-8 max-h-[95vh] overflow-y-auto"
>
<!-- Header -->
<div class="flex items-start justify-between gap-4">
<h3
class="font-kv-body text-xl md:text-[28px] text-kv-white uppercase kv-shadow-text m-0"
>
{cat.name || m.kv_edit_category()} - {q.points}
</h3>
<button
onclick={saveQuestion}
class="w-8 h-8 bg-transparent border-none cursor-pointer p-0 hover:opacity-70 text-kv-yellow"
>
<svg
viewBox="0 0 24 24"
fill="currentColor"
class="w-full h-full"
>
<path
d="M6.6188 19.4811L4.5188 17.3811L9.9188 11.9811L4.5188 6.61855L6.6188 4.51855L12.0188 9.91855L17.3813 4.51855L19.4813 6.61855L14.0813 11.9811L19.4813 17.3811L17.3813 19.4811L12.0188 14.0811L6.6188 19.4811Z"
/>
</svg>
</button>
</div>
<!-- Inputs -->
<div class="flex flex-col gap-4">
<div class="border-4 border-kv-black p-4 h-28">
<textarea
bind:value={q.question}
placeholder={m.kv_edit_question()}
class="w-full h-full bg-transparent border-none outline-none font-kv-body text-base text-kv-white uppercase resize-none placeholder:text-kv-white/50"
></textarea>
</div>
<div class="border-4 border-kv-black p-4">
<input
type="text"
bind:value={q.imageUrl}
placeholder={m.kv_edit_image_link()}
class="w-full bg-transparent border-none outline-none font-kv-body text-base text-kv-white uppercase placeholder:text-kv-white/50"
/>
</div>
<div class="border-4 border-kv-black p-4 h-28">
<textarea
bind:value={q.answer}
placeholder={m.kv_edit_answer()}
class="w-full h-full bg-transparent border-none outline-none font-kv-body text-base text-kv-white uppercase resize-none placeholder:text-kv-white/50"
></textarea>
</div>
<!-- Daily Double checkbox -->
<div class="flex items-center gap-2">
<button
onclick={toggleDailyDouble}
disabled={!q.isDailyDouble && currentDD >= maxDD}
class="w-8 h-8 rounded-sm cursor-pointer border-none p-0 disabled:opacity-50 flex items-center justify-center {q.isDailyDouble
? 'bg-kv-yellow'
: 'bg-white'}"
>
{#if q.isDailyDouble}
<span class="text-black text-lg font-bold"></span>
{/if}
</button>
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>
{m.kv_edit_daily_double()} ({currentDD}/{maxDD})
</span>
</div>
</div>
<!-- Save button -->
<button
onclick={saveQuestion}
class="bg-kv-yellow px-6 py-4 font-kv-body text-2xl text-black uppercase cursor-pointer border-none kv-shadow-button hover:opacity-90 self-start"
>
{m.kv_edit_save_exit()}
</button>
</div>
</div>
{/if}
<!-- Final Question Modal -->
{#if editingFinalQuestion}
<div
class="fixed inset-0 bg-kv-black/80 flex items-center justify-center z-50 p-2 md:p-8"
onclick={(e) =>
e.target === e.currentTarget && (editingFinalQuestion = false)}
onkeydown={(e) => e.key === "Escape" && (editingFinalQuestion = false)}
role="dialog"
tabindex="-1"
>
<div
class="bg-kv-blue border-8 md:border-[16px] border-kv-black p-4 md:p-8 w-full max-w-2xl flex flex-col gap-4 md:gap-8 max-h-[95vh] overflow-y-auto"
>
<div class="flex items-start justify-between gap-4">
<h3
class="font-kv-body text-xl md:text-[28px] text-kv-white uppercase kv-shadow-text"
>
{m.kv_edit_final_round()}
</h3>
<button
onclick={() => (editingFinalQuestion = false)}
class="w-8 h-8 bg-transparent border-none cursor-pointer p-0 hover:opacity-70 text-kv-yellow"
>
<svg
viewBox="0 0 24 24"
fill="currentColor"
class="w-full h-full"
>
<path
d="M6.6188 19.4811L4.5188 17.3811L9.9188 11.9811L4.5188 6.61855L6.6188 4.51855L12.0188 9.91855L17.3813 4.51855L19.4813 6.61855L14.0813 11.9811L19.4813 17.3811L17.3813 19.4811L12.0188 14.0811L6.6188 19.4811Z"
/>
</svg>
</button>
</div>
<div class="flex flex-col gap-4">
<div class="border-4 border-kv-black p-4">
<input
type="text"
bind:value={finalRound.category}
placeholder={m.kv_edit_category()}
class="w-full bg-transparent border-none outline-none font-kv-body text-base text-kv-white uppercase placeholder:text-kv-white/50"
/>
</div>
<div class="border-4 border-kv-black p-4 h-28">
<textarea
bind:value={finalRound.question}
placeholder={m.kv_edit_question()}
class="w-full h-full bg-transparent border-none outline-none font-kv-body text-base text-kv-white uppercase resize-none placeholder:text-kv-white/50"
></textarea>
</div>
<div class="border-4 border-kv-black p-4 h-28">
<textarea
bind:value={finalRound.answer}
placeholder={m.kv_edit_answer()}
class="w-full h-full bg-transparent border-none outline-none font-kv-body text-base text-kv-white uppercase resize-none placeholder:text-kv-white/50"
></textarea>
</div>
<!-- Toggle final round on/off -->
<div class="flex items-center gap-2">
<button
onclick={() =>
(settings.enableFinalRound =
!settings.enableFinalRound)}
class="w-8 h-8 rounded-sm cursor-pointer border-none p-0 flex items-center justify-center {settings.enableFinalRound
? 'bg-kv-yellow'
: 'bg-white'}"
>
{#if settings.enableFinalRound}
<span class="text-black text-lg font-bold"></span>
{/if}
</button>
<span
class="font-kv-body text-xl text-kv-white uppercase kv-shadow-text"
>
{m.kv_edit_final_enabled()}
</span>
</div>
</div>
<button
onclick={() => (editingFinalQuestion = false)}
class="bg-kv-yellow px-6 py-4 font-kv-body text-2xl text-black uppercase cursor-pointer border-none kv-shadow-button hover:opacity-90 self-start"
>
{m.kv_edit_save_exit()}
</button>
</div>
</div>
{/if}
<!-- Settings Modal -->
<Settings bind:open={settingsOpen} />
<!-- Toast -->
<Toast bind:visible={toastVisible} message={toastMessage} type={toastType} />
<!-- Loading overlay -->
{#if isStarting}
<div
class="fixed inset-0 bg-kv-black/90 flex flex-col items-center justify-center z-50 gap-4"
>
<div
class="w-16 h-16 border-4 border-kv-yellow border-t-transparent rounded-full animate-spin"
></div>
<p class="font-kv-body text-2xl text-kv-white uppercase">
{m.kv_edit_starting_game()}
</p>
</div>
{/if}