snapshot current state before gitea sync
This commit is contained in:
13
.claude/settings.local.json
Normal file
13
.claude/settings.local.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(ssh:*)",
|
||||||
|
"Bash(sips:*)",
|
||||||
|
"Bash(python3:*)",
|
||||||
|
"WebFetch(domain:manual.uberspace.de)",
|
||||||
|
"WebFetch(domain:lab.uberspace.de)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(rsync:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
250
app.js
Normal file
250
app.js
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
const words = [
|
||||||
|
"Kekse",
|
||||||
|
"Schach",
|
||||||
|
"Giraffe",
|
||||||
|
"Spaghetti",
|
||||||
|
"Rakete",
|
||||||
|
"Blume",
|
||||||
|
"Kino",
|
||||||
|
"Drachen",
|
||||||
|
"Buch",
|
||||||
|
"Kaffee",
|
||||||
|
"Berg",
|
||||||
|
"Pizza",
|
||||||
|
"Wolke",
|
||||||
|
"Bus",
|
||||||
|
"Zitrone",
|
||||||
|
"Gitarre",
|
||||||
|
"Meer",
|
||||||
|
"Bananen",
|
||||||
|
"Loewe",
|
||||||
|
"Regen",
|
||||||
|
];
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
players: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
imposterIndex: -1,
|
||||||
|
secretWord: "",
|
||||||
|
revealed: false,
|
||||||
|
discussionIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = {
|
||||||
|
playerInput: document.querySelector("#player-input"),
|
||||||
|
addPlayer: document.querySelector("#add-player"),
|
||||||
|
playerList: document.querySelector("#player-list"),
|
||||||
|
startGame: document.querySelector("#start-game"),
|
||||||
|
clearPlayers: document.querySelector("#clear-players"),
|
||||||
|
setupHint: document.querySelector("#setup-hint"),
|
||||||
|
setup: document.querySelector("#setup"),
|
||||||
|
round: document.querySelector("#round"),
|
||||||
|
discussion: document.querySelector("#discussion"),
|
||||||
|
done: document.querySelector("#done"),
|
||||||
|
roundCount: document.querySelector("#round-count"),
|
||||||
|
restart: document.querySelector("#restart"),
|
||||||
|
currentPlayer: document.querySelector("#current-player"),
|
||||||
|
roundScreen: document.querySelector("#round-screen"),
|
||||||
|
handoff: document.querySelector("#handoff"),
|
||||||
|
revealCard: document.querySelector("#reveal-card"),
|
||||||
|
revealWord: document.querySelector("#reveal-word"),
|
||||||
|
discussionCount: document.querySelector("#discussion-count"),
|
||||||
|
discussionPlayer: document.querySelector("#discussion-player"),
|
||||||
|
discussionNext: document.querySelector("#discussion-next"),
|
||||||
|
discussionRestart: document.querySelector("#discussion-restart"),
|
||||||
|
imposterFound: document.querySelector("#imposter-found"),
|
||||||
|
playAgain: document.querySelector("#play-again"),
|
||||||
|
editPlayers: document.querySelector("#edit-players"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const MIN_PLAYERS = 3;
|
||||||
|
const STORAGE_KEY = "imposterPlayers";
|
||||||
|
|
||||||
|
const updatePlayerList = () => {
|
||||||
|
elements.playerList.innerHTML = "";
|
||||||
|
state.players.forEach((player, index) => {
|
||||||
|
const item = document.createElement("li");
|
||||||
|
const name = document.createElement("span");
|
||||||
|
name.textContent = player;
|
||||||
|
const remove = document.createElement("button");
|
||||||
|
remove.type = "button";
|
||||||
|
remove.textContent = "Entfernen";
|
||||||
|
remove.addEventListener("click", () => {
|
||||||
|
state.players.splice(index, 1);
|
||||||
|
updatePlayerList();
|
||||||
|
});
|
||||||
|
item.append(name, remove);
|
||||||
|
elements.playerList.appendChild(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ready = state.players.length >= MIN_PLAYERS;
|
||||||
|
elements.startGame.disabled = !ready;
|
||||||
|
elements.setupHint.textContent = ready
|
||||||
|
? `${state.players.length} Spieler bereit.`
|
||||||
|
: `Mindestens ${MIN_PLAYERS} Spieler.`;
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.players));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addPlayer = () => {
|
||||||
|
const raw = elements.playerInput.value.trim();
|
||||||
|
if (!raw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.players.push(raw);
|
||||||
|
elements.playerInput.value = "";
|
||||||
|
updatePlayerList();
|
||||||
|
elements.playerInput.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRound = () => {
|
||||||
|
state.currentIndex = 0;
|
||||||
|
state.imposterIndex = Math.floor(Math.random() * state.players.length);
|
||||||
|
state.secretWord = words[Math.floor(Math.random() * words.length)];
|
||||||
|
state.revealed = false;
|
||||||
|
elements.revealCard.classList.add("flip-card--instant");
|
||||||
|
elements.setup.classList.add("hidden");
|
||||||
|
elements.discussion.classList.add("hidden");
|
||||||
|
elements.done.classList.add("hidden");
|
||||||
|
elements.round.classList.remove("hidden");
|
||||||
|
updateRoundView();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
elements.revealCard.classList.remove("flip-card--instant");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRoundView = () => {
|
||||||
|
const total = state.players.length;
|
||||||
|
const current = state.currentIndex + 1;
|
||||||
|
elements.roundCount.textContent = `Phase 1: Zuweisung \u2014 Spieler ${current} / ${total}`;
|
||||||
|
elements.currentPlayer.textContent = state.players[state.currentIndex];
|
||||||
|
elements.roundScreen.classList.remove("hidden");
|
||||||
|
const isImposter = state.currentIndex === state.imposterIndex;
|
||||||
|
elements.revealWord.textContent = isImposter ? "IMPOSTER" : state.secretWord;
|
||||||
|
elements.revealCard.classList.remove("flipped");
|
||||||
|
state.revealed = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleReveal = () => {
|
||||||
|
elements.revealCard.classList.toggle("flipped");
|
||||||
|
state.revealed = !state.revealed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handoffPlayer = () => {
|
||||||
|
elements.revealCard.classList.add("flip-card--instant");
|
||||||
|
elements.revealCard.classList.remove("flipped");
|
||||||
|
state.currentIndex += 1;
|
||||||
|
if (state.currentIndex >= state.players.length) {
|
||||||
|
startDiscussion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateRoundView();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
elements.revealCard.classList.remove("flip-card--instant");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDiscussion = () => {
|
||||||
|
state.discussionIndex = 0;
|
||||||
|
elements.round.classList.add("hidden");
|
||||||
|
elements.done.classList.add("hidden");
|
||||||
|
elements.discussion.classList.remove("hidden");
|
||||||
|
updateDiscussionView();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDiscussionView = () => {
|
||||||
|
const total = state.players.length;
|
||||||
|
const current = state.discussionIndex + 1;
|
||||||
|
elements.discussionCount.textContent = `Phase 2: Runde \u2014 Spieler ${current} / ${total}`;
|
||||||
|
elements.discussionPlayer.textContent = state.players[state.discussionIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextDiscussion = () => {
|
||||||
|
if (state.players.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.discussionIndex = (state.discussionIndex + 1) % state.players.length;
|
||||||
|
updateDiscussionView();
|
||||||
|
};
|
||||||
|
|
||||||
|
const endDiscussion = () => {
|
||||||
|
elements.discussion.classList.add("hidden");
|
||||||
|
elements.done.classList.remove("hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetToSetup = () => {
|
||||||
|
elements.round.classList.add("hidden");
|
||||||
|
elements.discussion.classList.add("hidden");
|
||||||
|
elements.done.classList.add("hidden");
|
||||||
|
elements.setup.classList.remove("hidden");
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearPlayers = () => {
|
||||||
|
state.players = [];
|
||||||
|
updatePlayerList();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPlayers = () => {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!saved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
state.players = parsed.filter((name) => typeof name === "string");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
elements.addPlayer.addEventListener("click", addPlayer);
|
||||||
|
elements.playerInput.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
addPlayer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.clearPlayers.addEventListener("click", clearPlayers);
|
||||||
|
elements.startGame.addEventListener("click", startRound);
|
||||||
|
elements.handoff.addEventListener("click", handoffPlayer);
|
||||||
|
elements.revealCard.addEventListener("click", toggleReveal);
|
||||||
|
elements.discussionNext.addEventListener("click", nextDiscussion);
|
||||||
|
elements.discussionRestart.addEventListener("click", startRound);
|
||||||
|
elements.imposterFound.addEventListener("click", endDiscussion);
|
||||||
|
elements.restart.addEventListener("click", startRound);
|
||||||
|
elements.playAgain.addEventListener("click", startRound);
|
||||||
|
elements.editPlayers.addEventListener("click", resetToSetup);
|
||||||
|
|
||||||
|
loadPlayers();
|
||||||
|
updatePlayerList();
|
||||||
|
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
window.addEventListener("load", async () => {
|
||||||
|
const reg = await navigator.serviceWorker.register("./sw.js");
|
||||||
|
|
||||||
|
const showUpdate = () => {
|
||||||
|
document.querySelector("#update-banner").classList.remove("hidden");
|
||||||
|
document.querySelector("#update-btn").addEventListener("click", () => {
|
||||||
|
reg.waiting.postMessage("SKIP_WAITING");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (reg.waiting) {
|
||||||
|
showUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
reg.addEventListener("updatefound", () => {
|
||||||
|
const newSW = reg.installing;
|
||||||
|
newSW.addEventListener("statechange", () => {
|
||||||
|
if (newSW.state === "installed" && navigator.serviceWorker.controller) {
|
||||||
|
showUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
BIN
apple-touch-icon.png
Normal file
BIN
apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
icon-192.png
Normal file
BIN
icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
icon-512.png
Normal file
BIN
icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
8
icon.svg
Normal file
8
icon.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Imposter">
|
||||||
|
<rect width="512" height="512" rx="120" fill="#ff7b54"/>
|
||||||
|
<circle cx="256" cy="210" r="110" fill="#fffaf0"/>
|
||||||
|
<rect x="140" y="300" width="232" height="120" rx="60" fill="#fffaf0"/>
|
||||||
|
<circle cx="230" cy="210" r="20" fill="#1a1a1a"/>
|
||||||
|
<circle cx="282" cy="210" r="20" fill="#1a1a1a"/>
|
||||||
|
<path d="M200 260c24 26 88 26 112 0" stroke="#1a1a1a" stroke-width="18" fill="none" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 504 B |
145
index.html
Normal file
145
index.html
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#f8f3e7" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<title>Imposter</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" />
|
||||||
|
<link rel="manifest" href="manifest.json" />
|
||||||
|
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||||
|
<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=Unbounded:wght@300;500;700&family=Space+Grotesk:wght@400;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="app">
|
||||||
|
<section class="screen" id="setup">
|
||||||
|
<header class="hero">
|
||||||
|
<div class="hero__tag">Handy weiterreichen</div>
|
||||||
|
<h1>Imposter</h1>
|
||||||
|
<p>
|
||||||
|
Trage Spieler ein, starte die Runde, und zeige jedem sein
|
||||||
|
Wort oder "IMPOSTER". Danach findet ihr heraus, wer das Wort
|
||||||
|
nicht kennt.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Spieler</h2>
|
||||||
|
<div class="input-row">
|
||||||
|
<input
|
||||||
|
id="player-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Name eingeben"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<button id="add-player" class="btn">Hinzufuegen</button>
|
||||||
|
</div>
|
||||||
|
<ul id="player-list" class="player-list"></ul>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="clear-players" class="btn btn--ghost">
|
||||||
|
Liste leeren
|
||||||
|
</button>
|
||||||
|
<button id="start-game" class="btn btn--primary">
|
||||||
|
Runde starten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p id="setup-hint" class="hint">Mindestens 3 Spieler.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screen hidden" id="round">
|
||||||
|
<div class="card">
|
||||||
|
<div class="round-header">
|
||||||
|
<div class="round-count" id="round-count">Spieler 1 / 1</div>
|
||||||
|
<button id="restart" class="btn btn--ghost">Neue Runde</button>
|
||||||
|
</div>
|
||||||
|
<div id="round-screen" class="pass-screen">
|
||||||
|
<h2>Phase 1: Zuweisung</h2>
|
||||||
|
<div class="player-chip" id="current-player">Spieler</div>
|
||||||
|
<div class="flip-card" id="reveal-card">
|
||||||
|
<div class="flip-card__inner">
|
||||||
|
<div class="flip-card__front">
|
||||||
|
<div class="reveal-label">Antippen zum Aufdecken</div>
|
||||||
|
</div>
|
||||||
|
<div class="flip-card__back">
|
||||||
|
<div class="reveal-label">Dein Hinweis</div>
|
||||||
|
<div class="reveal-word" id="reveal-word">WORT</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="hint">
|
||||||
|
Tippe auf die Karte, um dein Wort zu sehen. Gib das Handy
|
||||||
|
danach weiter.
|
||||||
|
</p>
|
||||||
|
<div class="actions actions--end">
|
||||||
|
<button id="handoff" class="btn btn--primary btn--big">
|
||||||
|
Weitergeben
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screen hidden" id="discussion">
|
||||||
|
<div class="card">
|
||||||
|
<div class="round-header">
|
||||||
|
<div class="round-count" id="discussion-count">
|
||||||
|
Diskussion 1 / 1
|
||||||
|
</div>
|
||||||
|
<button id="discussion-restart" class="btn btn--ghost">
|
||||||
|
Neue Runde
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pass-screen">
|
||||||
|
<h2>Wer ist dran?</h2>
|
||||||
|
<div
|
||||||
|
class="player-chip player-chip--large"
|
||||||
|
id="discussion-player"
|
||||||
|
>
|
||||||
|
Spieler
|
||||||
|
</div>
|
||||||
|
<p class="hint">Gib das Handy weiter.</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="imposter-found" class="btn btn--ghost">
|
||||||
|
Imposter gefunden
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="discussion-next"
|
||||||
|
class="btn btn--primary btn--big"
|
||||||
|
>
|
||||||
|
Weitergeben
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screen hidden" id="done">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Runde beendet</h2>
|
||||||
|
<p>Optional: Neue Runde starten oder Spieler bearbeiten.</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="edit-players" class="btn btn--ghost">
|
||||||
|
Spieler bearbeiten
|
||||||
|
</button>
|
||||||
|
<button id="play-again" class="btn btn--primary">
|
||||||
|
Neue Runde
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div class="update-banner hidden" id="update-banner">
|
||||||
|
Neue Version verfügbar
|
||||||
|
<button id="update-btn" class="btn btn--primary">Aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
manifest.json
Normal file
22
manifest.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "Imposter",
|
||||||
|
"short_name": "Imposter",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#f8f3e7",
|
||||||
|
"theme_color": "#f8f3e7",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
371
styles.css
Normal file
371
styles.css
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--bg: #f8f3e7;
|
||||||
|
--bg-alt: #f0e6d1;
|
||||||
|
--ink: #1a1a1a;
|
||||||
|
--accent: #ff7b54;
|
||||||
|
--accent-dark: #d95c38;
|
||||||
|
--muted: #5b5b5b;
|
||||||
|
--card: #fffaf0;
|
||||||
|
--shadow: 0 20px 45px rgba(26, 26, 26, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--ink);
|
||||||
|
background-color: var(--bg);
|
||||||
|
background: radial-gradient(circle at top, #fff4dc, var(--bg));
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen > .card {
|
||||||
|
width: min(560px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
#round > .card,
|
||||||
|
#discussion > .card,
|
||||||
|
#done > .card {
|
||||||
|
min-height: 480px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setup {
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
width: min(560px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-self: start;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fffaf0;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-family: "Unbounded", "Trebuchet MS", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.08;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2.2rem, 5vw, 3.2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: clamp(1.5rem, 3.5vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid rgba(26, 26, 26, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(26, 26, 26, 0.2);
|
||||||
|
font-size: 1rem;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border: none;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--bg-alt);
|
||||||
|
color: var(--ink);
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
font-family: "Space Grotesk", sans-serif;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
box-shadow: 0 10px 20px rgba(26, 26, 26, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fffaf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--ghost {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px dashed rgba(26, 26, 26, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--big {
|
||||||
|
padding: 14px 20px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 16px 0 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(26, 26, 26, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-list button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent-dark);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions--end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-count {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pass-screen {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pass-screen .actions {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-chip {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--bg-alt);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: clamp(1.2rem, 3.8vw, 1.8rem);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-chip--large {
|
||||||
|
font-size: clamp(1.8rem, 5vw, 2.8rem);
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card {
|
||||||
|
perspective: 600px;
|
||||||
|
width: min(320px, 80vw);
|
||||||
|
height: 180px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card__inner {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card.flipped .flip-card__inner {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card--instant .flip-card__inner {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card__front,
|
||||||
|
.flip-card__back {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px;
|
||||||
|
background: linear-gradient(135deg, #fff4dc, #ffe2c0);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(26, 26, 26, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-card__back {
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal-label {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal-word {
|
||||||
|
font-family: "Unbounded", sans-serif;
|
||||||
|
font-size: clamp(1.8rem, 6vw, 3rem);
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 16px 0 32px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.input-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.round-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
animation: fadeIn 0.35s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pass-screen .btn {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-banner {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: var(--ink);
|
||||||
|
color: #fffaf0;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-banner .btn {
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
}
|
||||||
45
sw.js
Normal file
45
sw.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const CACHE_NAME = "imposter-static-v4";
|
||||||
|
const ASSETS = [
|
||||||
|
"./",
|
||||||
|
"./index.html",
|
||||||
|
"./styles.css",
|
||||||
|
"./app.js",
|
||||||
|
"./manifest.json",
|
||||||
|
"./icon-192.png",
|
||||||
|
"./icon-512.png",
|
||||||
|
"./apple-touch-icon.png",
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches
|
||||||
|
.keys()
|
||||||
|
.then((keys) =>
|
||||||
|
Promise.all(
|
||||||
|
keys
|
||||||
|
.filter((key) => key !== CACHE_NAME)
|
||||||
|
.map((key) => caches.delete(key)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("message", (event) => {
|
||||||
|
if (event.data === "SKIP_WAITING") {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
event.respondWith(
|
||||||
|
caches
|
||||||
|
.match(event.request)
|
||||||
|
.then((response) => response || fetch(event.request)),
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user