rewrite impstr in typescript + tailwind, deploy as static pwa
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
250
app.js
@@ -1,250 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
145
bun.lock
Normal file
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "impstr",
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="],
|
||||
|
||||
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="],
|
||||
|
||||
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="],
|
||||
|
||||
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="],
|
||||
|
||||
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="],
|
||||
|
||||
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="],
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
||||
|
||||
"@tailwindcss/cli": ["@tailwindcss/cli@4.2.1", "", { "dependencies": { "@parcel/watcher": "^2.5.1", "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "enhanced-resolve": "^5.19.0", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.2.1" }, "bin": { "tailwindcss": "dist/index.mjs" } }, "sha512-b7MGn51IA80oSG+7fuAgzfQ+7pZBgjzbqwmiv6NO7/+a1sev32cGqnwhscT7h0EcAvMa9r7gjRylqOH8Xhc4DA=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
}
|
||||
}
|
||||
19
deploy.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="serve"
|
||||
REMOTE_DIR="/var/www/virtual/serve/html/impstr"
|
||||
|
||||
echo "==> Installing local dependencies..."
|
||||
bun install
|
||||
|
||||
echo "==> Building..."
|
||||
bun run build
|
||||
|
||||
echo "==> Syncing to ${REMOTE_HOST}:${REMOTE_DIR} ..."
|
||||
ssh "${REMOTE_HOST}" "mkdir -p ${REMOTE_DIR}"
|
||||
rsync -avz --delete \
|
||||
--exclude='.DS_Store' \
|
||||
dist/ "${REMOTE_HOST}:${REMOTE_DIR}/"
|
||||
|
||||
echo "Done. Live at https://serve.uber.space/impstr/"
|
||||
145
index.html
@@ -1,145 +0,0 @@
|
||||
<!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>
|
||||
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "impstr",
|
||||
"version": "2026.02.26",
|
||||
"scripts": {
|
||||
"build": "bun run build:js && bun run build:css && bun run build:assets",
|
||||
"build:js": "bun build src/client/main.ts --outfile dist/main.js --minify",
|
||||
"build:css": "tailwindcss -i src/client/input.css -o dist/styles.css --minify",
|
||||
"build:assets": "bun run scripts/copy-assets.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 504 B |
@@ -1,10 +1,10 @@
|
||||
const CACHE_NAME = "imposter-static-v4";
|
||||
const CACHE_NAME = "impstr-v1";
|
||||
const ASSETS = [
|
||||
"./",
|
||||
"./index.html",
|
||||
"./main.js",
|
||||
"./styles.css",
|
||||
"./app.js",
|
||||
"./manifest.json",
|
||||
"./manifest.webmanifest",
|
||||
"./icon-192.png",
|
||||
"./icon-512.png",
|
||||
"./apple-touch-icon.png",
|
||||
18
scripts/copy-assets.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { copyFileSync, mkdirSync } from "fs";
|
||||
|
||||
mkdirSync("dist", { recursive: true });
|
||||
|
||||
const assets: [string, string][] = [
|
||||
["src/index.html", "dist/index.html"],
|
||||
["public/sw.js", "dist/sw.js"],
|
||||
["public/manifest.webmanifest", "dist/manifest.webmanifest"],
|
||||
["public/icon-192.png", "dist/icon-192.png"],
|
||||
["public/icon-512.png", "dist/icon-512.png"],
|
||||
["public/apple-touch-icon.png", "dist/apple-touch-icon.png"],
|
||||
["public/icon.svg", "dist/icon.svg"],
|
||||
];
|
||||
|
||||
for (const [src, dest] of assets) {
|
||||
copyFileSync(src, dest);
|
||||
console.log(`Copied ${src} → ${dest}`);
|
||||
}
|
||||
179
src/client/input.css
Normal file
@@ -0,0 +1,179 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@source "./main.ts";
|
||||
@source "../index.html";
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
color-scheme: light;
|
||||
background-color: #f8f3e7;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
color: #1a1a1a;
|
||||
background: radial-gradient(circle at top, #fff4dc, #f8f3e7);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
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;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(26, 26, 26, 0.2);
|
||||
font-size: 1rem;
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.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.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fffaf0;
|
||||
border-radius: 24px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 20px 45px rgba(26, 26, 26, 0.12);
|
||||
border: 1px solid rgba(26, 26, 26, 0.08);
|
||||
animation: fadeIn 0.35s ease;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
padding: 14px 20px;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: #f0e6d1;
|
||||
color: #1a1a1a;
|
||||
transition: box-shadow 0.2s ease;
|
||||
font-family: "Space Grotesk", sans-serif;
|
||||
min-width: 0;
|
||||
min-height: 52px;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
box-shadow: 0 10px 20px rgba(26, 26, 26, 0.15);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn--primary {
|
||||
background: #ff7b54;
|
||||
color: #fffaf0;
|
||||
}
|
||||
|
||||
.btn--ghost {
|
||||
background: transparent;
|
||||
border: 1px dashed rgba(26, 26, 26, 0.25);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Outside any layer — wins over all Tailwind utilities */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
269
src/client/main.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
const WORDS: string[] = [
|
||||
"Kekse",
|
||||
"Schach",
|
||||
"Giraffe",
|
||||
"Spaghetti",
|
||||
"Rakete",
|
||||
"Blume",
|
||||
"Kino",
|
||||
"Drachen",
|
||||
"Buch",
|
||||
"Kaffee",
|
||||
"Berg",
|
||||
"Pizza",
|
||||
"Wolke",
|
||||
"Bus",
|
||||
"Zitrone",
|
||||
"Gitarre",
|
||||
"Meer",
|
||||
"Bananen",
|
||||
"Löwe",
|
||||
"Regen",
|
||||
];
|
||||
|
||||
const MIN_PLAYERS = 3;
|
||||
const STORAGE_KEY = "impstr-players";
|
||||
|
||||
interface State {
|
||||
players: string[];
|
||||
currentIndex: number;
|
||||
imposterIndex: number;
|
||||
secretWord: string;
|
||||
revealed: boolean;
|
||||
discussionIndex: number;
|
||||
}
|
||||
|
||||
const state: State = {
|
||||
players: [],
|
||||
currentIndex: 0,
|
||||
imposterIndex: -1,
|
||||
secretWord: "",
|
||||
revealed: false,
|
||||
discussionIndex: 0,
|
||||
};
|
||||
|
||||
function q<T extends HTMLElement>(id: string): T {
|
||||
return document.getElementById(id) as T;
|
||||
}
|
||||
|
||||
const el = {
|
||||
playerInput: q<HTMLInputElement>("player-input"),
|
||||
addPlayer: q<HTMLButtonElement>("add-player"),
|
||||
playerList: q<HTMLUListElement>("player-list"),
|
||||
startGame: q<HTMLButtonElement>("start-game"),
|
||||
clearPlayers: q<HTMLButtonElement>("clear-players"),
|
||||
setupHint: q<HTMLParagraphElement>("setup-hint"),
|
||||
setup: q<HTMLElement>("setup"),
|
||||
round: q<HTMLElement>("round"),
|
||||
discussion: q<HTMLElement>("discussion"),
|
||||
done: q<HTMLElement>("done"),
|
||||
roundCount: q<HTMLElement>("round-count"),
|
||||
restart: q<HTMLButtonElement>("restart"),
|
||||
currentPlayer: q<HTMLElement>("current-player"),
|
||||
handoff: q<HTMLButtonElement>("handoff"),
|
||||
revealCard: q<HTMLElement>("reveal-card"),
|
||||
revealWord: q<HTMLElement>("reveal-word"),
|
||||
discussionCount: q<HTMLElement>("discussion-count"),
|
||||
discussionPlayer: q<HTMLElement>("discussion-player"),
|
||||
discussionNext: q<HTMLButtonElement>("discussion-next"),
|
||||
discussionRestart: q<HTMLButtonElement>("discussion-restart"),
|
||||
imposterFound: q<HTMLButtonElement>("imposter-found"),
|
||||
playAgain: q<HTMLButtonElement>("play-again"),
|
||||
editPlayers: q<HTMLButtonElement>("edit-players"),
|
||||
};
|
||||
|
||||
function show(...els: HTMLElement[]): void {
|
||||
els.forEach((e) => e.classList.remove("hidden"));
|
||||
}
|
||||
|
||||
function hide(...els: HTMLElement[]): void {
|
||||
els.forEach((e) => e.classList.add("hidden"));
|
||||
}
|
||||
|
||||
function updatePlayerList(): void {
|
||||
el.playerList.innerHTML = "";
|
||||
state.players.forEach((player, index) => {
|
||||
const item = document.createElement("li");
|
||||
item.className =
|
||||
"flex items-center justify-between px-3 py-2.5 rounded-xl bg-white border border-[rgba(26,26,26,0.08)]";
|
||||
const name = document.createElement("span");
|
||||
name.textContent = player;
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.textContent = "Entfernen";
|
||||
remove.className =
|
||||
"bg-transparent border-0 text-[#d95c38] font-semibold cursor-pointer";
|
||||
remove.addEventListener("click", () => {
|
||||
state.players.splice(index, 1);
|
||||
updatePlayerList();
|
||||
});
|
||||
item.append(name, remove);
|
||||
el.playerList.appendChild(item);
|
||||
});
|
||||
|
||||
const ready = state.players.length >= MIN_PLAYERS;
|
||||
el.startGame.disabled = !ready;
|
||||
el.setupHint.textContent = ready
|
||||
? `${state.players.length} Spieler bereit.`
|
||||
: `Mindestens ${MIN_PLAYERS} Spieler.`;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.players));
|
||||
}
|
||||
|
||||
function addPlayer(): void {
|
||||
const raw = el.playerInput.value.trim();
|
||||
if (!raw) return;
|
||||
state.players.push(raw);
|
||||
el.playerInput.value = "";
|
||||
updatePlayerList();
|
||||
el.playerInput.focus();
|
||||
}
|
||||
|
||||
function startRound(): void {
|
||||
state.currentIndex = 0;
|
||||
state.imposterIndex = Math.floor(Math.random() * state.players.length);
|
||||
state.secretWord = WORDS[Math.floor(Math.random() * WORDS.length)];
|
||||
state.revealed = false;
|
||||
|
||||
el.revealCard.classList.add("flip-card--instant");
|
||||
el.revealCard.classList.remove("flipped");
|
||||
|
||||
show(el.round);
|
||||
hide(el.setup, el.discussion, el.done);
|
||||
updateRoundView();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
el.revealCard.classList.remove("flip-card--instant");
|
||||
});
|
||||
}
|
||||
|
||||
function updateRoundView(): void {
|
||||
const total = state.players.length;
|
||||
const current = state.currentIndex + 1;
|
||||
el.roundCount.textContent = `Phase 1: Zuweisung \u2014 Spieler ${current}\u00a0/\u00a0${total}`;
|
||||
el.currentPlayer.textContent = state.players[state.currentIndex];
|
||||
el.revealWord.textContent =
|
||||
state.currentIndex === state.imposterIndex ? "IMPOSTER" : state.secretWord;
|
||||
el.revealCard.classList.remove("flipped");
|
||||
state.revealed = false;
|
||||
}
|
||||
|
||||
function toggleReveal(): void {
|
||||
el.revealCard.classList.toggle("flipped");
|
||||
state.revealed = !state.revealed;
|
||||
}
|
||||
|
||||
function handoffPlayer(): void {
|
||||
el.revealCard.classList.add("flip-card--instant");
|
||||
el.revealCard.classList.remove("flipped");
|
||||
state.currentIndex += 1;
|
||||
if (state.currentIndex >= state.players.length) {
|
||||
startDiscussion();
|
||||
return;
|
||||
}
|
||||
updateRoundView();
|
||||
requestAnimationFrame(() => {
|
||||
el.revealCard.classList.remove("flip-card--instant");
|
||||
});
|
||||
}
|
||||
|
||||
function startDiscussion(): void {
|
||||
state.discussionIndex = 0;
|
||||
show(el.discussion);
|
||||
hide(el.round, el.done);
|
||||
updateDiscussionView();
|
||||
}
|
||||
|
||||
function updateDiscussionView(): void {
|
||||
const total = state.players.length;
|
||||
const current = state.discussionIndex + 1;
|
||||
el.discussionCount.textContent = `Phase 2: Diskussion \u2014 Spieler ${current}\u00a0/\u00a0${total}`;
|
||||
el.discussionPlayer.textContent = state.players[state.discussionIndex];
|
||||
}
|
||||
|
||||
function nextDiscussion(): void {
|
||||
if (state.players.length === 0) return;
|
||||
state.discussionIndex = (state.discussionIndex + 1) % state.players.length;
|
||||
updateDiscussionView();
|
||||
}
|
||||
|
||||
function endDiscussion(): void {
|
||||
show(el.done);
|
||||
hide(el.discussion);
|
||||
}
|
||||
|
||||
function resetToSetup(): void {
|
||||
show(el.setup);
|
||||
hide(el.round, el.discussion, el.done);
|
||||
}
|
||||
|
||||
function clearPlayers(): void {
|
||||
state.players = [];
|
||||
updatePlayerList();
|
||||
}
|
||||
|
||||
function loadPlayers(): void {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (!saved) return;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(saved);
|
||||
if (Array.isArray(parsed)) {
|
||||
state.players = parsed.filter(
|
||||
(name): name is string => typeof name === "string",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
el.addPlayer.addEventListener("click", addPlayer);
|
||||
el.playerInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") addPlayer();
|
||||
});
|
||||
el.clearPlayers.addEventListener("click", clearPlayers);
|
||||
el.startGame.addEventListener("click", startRound);
|
||||
el.handoff.addEventListener("click", handoffPlayer);
|
||||
el.revealCard.addEventListener("click", toggleReveal);
|
||||
el.discussionNext.addEventListener("click", nextDiscussion);
|
||||
el.discussionRestart.addEventListener("click", startRound);
|
||||
el.imposterFound.addEventListener("click", endDiscussion);
|
||||
el.restart.addEventListener("click", startRound);
|
||||
el.playAgain.addEventListener("click", startRound);
|
||||
el.editPlayers.addEventListener("click", resetToSetup);
|
||||
|
||||
// Init
|
||||
loadPlayers();
|
||||
updatePlayerList();
|
||||
|
||||
// Service worker
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", async () => {
|
||||
const reg = await navigator.serviceWorker.register("./sw.js");
|
||||
|
||||
const showUpdate = () => {
|
||||
const banner = document.getElementById("update-banner");
|
||||
if (!banner) return;
|
||||
banner.classList.remove("hidden");
|
||||
document.getElementById("update-btn")?.addEventListener("click", () => {
|
||||
(reg.waiting as ServiceWorker | null)?.postMessage("SKIP_WAITING");
|
||||
});
|
||||
};
|
||||
|
||||
if (reg.waiting) showUpdate();
|
||||
|
||||
reg.addEventListener("updatefound", () => {
|
||||
const newSW = reg.installing;
|
||||
if (!newSW) return;
|
||||
newSW.addEventListener("statechange", () => {
|
||||
if (newSW.state === "installed" && navigator.serviceWorker.controller) {
|
||||
showUpdate();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
126
src/index.html
Normal file
@@ -0,0 +1,126 @@
|
||||
<!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.webmanifest" />
|
||||
<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="w-full">
|
||||
|
||||
<!-- Setup -->
|
||||
<section id="setup" class="screen" style="justify-content:flex-start;padding-top:40px;">
|
||||
<header class="grid gap-3 w-full max-w-[560px]">
|
||||
<span class="inline-flex items-center self-start px-3 py-1.5 rounded-full bg-[#ff7b54] text-[#fffaf0] font-semibold text-sm tracking-wide">
|
||||
Handy weiterreichen
|
||||
</span>
|
||||
<h1>Imposter</h1>
|
||||
<p class="text-[#5b5b5b]">
|
||||
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 w-full max-w-[560px]">
|
||||
<h2>Spieler</h2>
|
||||
<div class="grid grid-cols-[1fr_auto] max-sm:grid-cols-1 gap-3 mt-4">
|
||||
<input
|
||||
id="player-input"
|
||||
type="text"
|
||||
placeholder="Name eingeben"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button id="add-player" class="btn">Hinzufügen</button>
|
||||
</div>
|
||||
<ul id="player-list" class="list-none p-0 m-0 mt-4 grid gap-2.5"></ul>
|
||||
<div class="flex max-sm:flex-col-reverse max-sm:items-stretch justify-between items-center gap-3 mt-5 flex-wrap">
|
||||
<button id="clear-players" class="btn btn--ghost">Liste leeren</button>
|
||||
<button id="start-game" class="btn btn--primary" disabled>Runde starten</button>
|
||||
</div>
|
||||
<p id="setup-hint" class="mt-3 text-[0.95rem] text-[#5b5b5b]">Mindestens 3 Spieler.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Round -->
|
||||
<section id="round" class="screen hidden">
|
||||
<div class="card w-full max-w-[560px] flex flex-col" style="min-height:480px;">
|
||||
<div class="flex max-sm:flex-col max-sm:items-stretch flex-wrap items-center justify-between gap-3">
|
||||
<div id="round-count" class="font-semibold">Phase 1: Zuweisung — Spieler 1 / 1</div>
|
||||
<button id="restart" class="btn btn--ghost">Neue Runde</button>
|
||||
</div>
|
||||
<div id="round-screen" class="flex flex-col items-center gap-4 mt-4 text-center flex-1">
|
||||
<div id="current-player" class="px-5 py-2.5 rounded-full bg-[#f0e6d1] font-semibold" style="font-size:clamp(1.2rem,3.8vw,1.8rem);"></div>
|
||||
<div id="reveal-card" class="flip-card">
|
||||
<div class="flip-card__inner">
|
||||
<div class="flip-card__front">
|
||||
<div class="text-[0.95rem] uppercase tracking-[0.08em] text-[#5b5b5b]">Antippen zum Aufdecken</div>
|
||||
</div>
|
||||
<div class="flip-card__back">
|
||||
<div class="text-[0.95rem] uppercase tracking-[0.08em] text-[#5b5b5b]">Dein Hinweis</div>
|
||||
<div id="reveal-word" class="mt-2.5 text-center" style="font-family:'Unbounded',sans-serif;font-size:clamp(1.8rem,6vw,3rem);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-[0.95rem] text-[#5b5b5b]">
|
||||
Tippe auf die Karte, um dein Wort zu sehen. Gib das Handy danach weiter.
|
||||
</p>
|
||||
<div class="flex justify-end items-center gap-3 mt-auto w-full max-sm:flex-col-reverse max-sm:items-stretch flex-wrap">
|
||||
<button id="handoff" class="btn btn--primary" style="min-width:180px;">Weitergeben</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Discussion -->
|
||||
<section id="discussion" class="screen hidden">
|
||||
<div class="card w-full max-w-[560px] flex flex-col" style="min-height:480px;">
|
||||
<div class="flex max-sm:flex-col max-sm:items-stretch flex-wrap items-center justify-between gap-3">
|
||||
<div id="discussion-count" class="font-semibold">Phase 2: Diskussion — Spieler 1 / 1</div>
|
||||
<button id="discussion-restart" class="btn btn--ghost">Neue Runde</button>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-4 mt-4 text-center flex-1">
|
||||
<h2>Wer ist dran?</h2>
|
||||
<div id="discussion-player" class="px-5 py-2.5 rounded-full bg-[#f0e6d1] font-semibold text-center tracking-[0.01em]" style="font-size:clamp(1.8rem,5vw,2.8rem);"></div>
|
||||
<p class="text-[0.95rem] text-[#5b5b5b]">Gib das Handy weiter.</p>
|
||||
<div class="flex justify-between items-center gap-3 mt-auto w-full max-sm:flex-col-reverse max-sm:items-stretch flex-wrap">
|
||||
<button id="imposter-found" class="btn btn--ghost">Imposter gefunden</button>
|
||||
<button id="discussion-next" class="btn btn--primary" style="min-width:180px;">Weitergeben</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Done -->
|
||||
<section id="done" class="screen hidden">
|
||||
<div class="card w-full max-w-[560px]">
|
||||
<h2>Runde beendet</h2>
|
||||
<p class="mt-2 text-[#5b5b5b]">Optional: Neue Runde starten oder Spieler bearbeiten.</p>
|
||||
<div class="flex justify-between items-center gap-3 mt-5 max-sm:flex-col-reverse max-sm:items-stretch flex-wrap">
|
||||
<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>
|
||||
|
||||
<!-- Update banner -->
|
||||
<div id="update-banner" class="hidden fixed bottom-0 left-0 right-0 flex items-center justify-center gap-4 px-6 py-4 bg-[#1a1a1a] text-[#fffaf0] font-semibold z-50">
|
||||
Neue Version verfügbar
|
||||
<button id="update-btn" class="btn btn--primary" style="min-height:40px;padding:8px 20px;">Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
371
styles.css
@@ -1,371 +0,0 @@
|
||||
: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;
|
||||
}
|
||||
10
tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"lib": ["ES2020", "DOM"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||