GUI #32
|
|
@ -185,10 +185,10 @@ private void broadcastRoundResult(String gameId) {
|
||||||
}else{
|
}else{
|
||||||
// nächste Runde starten
|
// nächste Runde starten
|
||||||
// ...
|
// ...
|
||||||
new Thread(() -> {
|
// new Thread(() -> {
|
||||||
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
|
// try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
|
||||||
nextround(gameId);
|
// nextround(gameId);
|
||||||
}).start();
|
// }).start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,107 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Spiel erstellen – Spotify Roulette</title>
|
<title>Spiel erstellen – Spotify Roulette</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#121212; --card:#181818; --border:#282828; --text:#fff; --muted:#b3b3b3;
|
||||||
|
--accent:#1db954; --accent-press:#169e47; --glow:rgba(29,185,84,.25);
|
||||||
|
--radius:16px; --shadow:0 10px 30px rgba(0,0,0,.35)
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{height:100%}
|
||||||
|
body{
|
||||||
|
margin:0; font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
|
||||||
|
color:var(--text);
|
||||||
|
background: radial-gradient(1000px 500px at 20% -10%, rgba(29,185,84,.14), transparent 60%),
|
||||||
|
radial-gradient(900px 450px at 110% -20%, rgba(29,185,84,.12), transparent 70%),
|
||||||
|
var(--bg);
|
||||||
|
padding:24px;
|
||||||
|
}
|
||||||
|
.container{max-width:980px; margin:0 auto}
|
||||||
|
header{display:flex; align-items:center; gap:12px; padding-bottom:12px; border-bottom:1px solid var(--border)}
|
||||||
|
.logo{width:40px; height:40px; border-radius:50%; background:var(--accent); color:#121212; display:grid; place-content:center; box-shadow:0 0 0 6px var(--glow)}
|
||||||
|
h1{margin:0; font-size:24px}
|
||||||
|
.pill{margin-left:auto; display:inline-flex; gap:8px; align-items:center; padding:6px 10px; border-radius:999px; border:1px solid var(--border); background:#101010; color:var(--muted); font-size:12px}
|
||||||
|
|
||||||
|
.grid{display:grid; grid-template-columns:1.2fr .8fr; gap:20px; margin-top:20px}
|
||||||
|
@media (max-width:900px){ .grid{grid-template-columns:1fr} }
|
||||||
|
|
||||||
|
.card{background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow)}
|
||||||
|
.card .hd{padding:18px 18px 8px}
|
||||||
|
.card .bd{padding:0 18px 18px}
|
||||||
|
.muted{color:var(--muted)}
|
||||||
|
|
||||||
|
.btn{width:100%; border:0; border-radius:14px; padding:16px 18px; cursor:pointer; font-weight:700; display:flex; align-items:center; justify-content:center; gap:10px; transition:transform .06s ease, filter .15s ease, background .2s ease}
|
||||||
|
.btn:active{transform:translateY(1px)}
|
||||||
|
.btn-primary{background:var(--accent); color:#0a0a0a}
|
||||||
|
.btn-primary:hover{filter:brightness(1.05)}
|
||||||
|
.btn-primary:active{background:var(--accent-press)}
|
||||||
|
|
||||||
|
.list{display:flex; flex-direction:column; gap:10px; margin-top:10px}
|
||||||
|
.row{display:flex; gap:10px; align-items:center}
|
||||||
|
.help{font-size:14px; color:var(--muted); margin-top:10px}
|
||||||
|
|
||||||
|
.tile{display:flex; gap:14px; align-items:flex-start; padding:14px; border:1px solid var(--border); border-radius:14px; background:#101010}
|
||||||
|
.tile .icon{width:44px; height:44px; border-radius:10px; display:grid; place-content:center; background:#0e0e0e; border:1px solid var(--border)}
|
||||||
|
.tile .txt{display:flex; flex-direction:column}
|
||||||
|
.tile .txt b{font-size:16px}
|
||||||
|
.tile .txt span{font-size:13px; color:var(--muted)}
|
||||||
|
.badge{display:inline-flex; align-items:center; gap:6px; padding:4px 8px; border-radius:999px; border:1px solid var(--border); background:#0f0f0f; color:var(--muted); font-size:12px}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<div class="logo" aria-hidden="true">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2 2 7l10 5 10-5-10-5Zm10 7-10 5v9l10-5V9ZM2 9v9l10 5v-9L2 9Z"/></svg>
|
||||||
|
</div>
|
||||||
<h1>Neues Spiel erstellen</h1>
|
<h1>Neues Spiel erstellen</h1>
|
||||||
<button id="createGame">Spiel erstellen</button>
|
<span class="pill">Angemeldet als <b id="uname" style="color:#fff">—</b></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="grid" aria-label="Spiel erstellen">
|
||||||
|
<div class="card">
|
||||||
|
<div class="hd"><h2 style="margin:0">Spiel starten</h2></div>
|
||||||
|
<div class="bd">
|
||||||
|
<div class="tile" style="margin-bottom:12px">
|
||||||
|
<div class="icon" aria-hidden="true">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="#1db954"><path d="M12 3 2 9l10 6 10-6-10-6Zm10 8-10 6-10-6v4l10 6 10-6v-4Z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="txt">
|
||||||
|
<b>Standard-Spiel</b>
|
||||||
|
<span>Erstellt eine Lobby mit Standardregeln. </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="createGame" class="btn btn-primary">Spiel erstellen</button>
|
||||||
|
<p class="help">Du bekommst einen Spiel‑Code zum Teilen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="hd"><h2 style="margin:0">WIP Einstellungen</h2></div>
|
||||||
|
<div class="bd">
|
||||||
|
<div class="list">
|
||||||
|
<div class="row"><span class="badge">Bald</span><span class="muted">Max. Spieler • Rundenanzahl • Song‑Ausschnitt • Privat/Öffentlich</span></div>
|
||||||
|
<div class="row"><span class="muted">Noch in arbeit</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
// Nur für die Anzeige oben rechts; beeinflusst deinen Flow nicht
|
||||||
|
const p = new URLSearchParams(location.search);
|
||||||
|
const u = p.get('username');
|
||||||
|
if (u) document.querySelector('#uname').textContent = u;
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="module" src="/js/create-game.js"></script>
|
<script type="module" src="/js/create-game.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,162 +1,228 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Spotify Roulette – Spiel</title>
|
<title>Spotify Roulette – Spiel</title>
|
||||||
|
|
||||||
|
<!-- Font -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:root{
|
:root{
|
||||||
--primary-color: #1DB954;
|
--bg:#121212; --elev:#181818; --border:#282828; --text:#fff; --muted:#b3b3b3;
|
||||||
--bg-light: #121212; /* vorher: #f2f2f2 */
|
--accent:#1db954; --accent-press:#169e47; --glow:rgba(29,185,84,.25);
|
||||||
--bg-dark: #000000; /* vorher: #121212 */
|
--radius:16px; --shadow:0 10px 30px rgba(0,0,0,.35);
|
||||||
--text-light: #eaeaea; /* vorher: #ffffff */
|
|
||||||
--text-dark: #eaeaea; /* vorher: #333333 */
|
|
||||||
--accent: #191414;
|
|
||||||
--option-circle-size: 80%;
|
|
||||||
--option-max-size: 400px;
|
|
||||||
--option-radius: 120px;
|
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
*{box-sizing:border-box}
|
||||||
|
html,body{height:100%}
|
||||||
body{
|
body{
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
margin:0; font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
|
||||||
background: var(--bg-light);
|
color:var(--text);
|
||||||
color: var(--text-dark);
|
background:
|
||||||
display: grid;
|
radial-gradient(1200px 600px at 20% -10%, rgba(29,185,84,.15), transparent 60%),
|
||||||
grid-template-areas:
|
radial-gradient(1000px 500px at 100% -20%, rgba(29,185,84,.12), transparent 70%),
|
||||||
"header header"
|
var(--bg);
|
||||||
"main aside"
|
|
||||||
"footer footer";
|
|
||||||
grid-template-columns: 3fr 1fr;
|
|
||||||
grid-template-rows: auto 1fr auto;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container{max-width:1100px; margin:0 auto; padding:24px}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
header{
|
header{
|
||||||
grid-area: header;
|
display:flex; align-items:center; justify-content:space-between; gap:16px;
|
||||||
background: var(--primary-color);
|
padding-bottom:12px; border-bottom:1px solid var(--border);
|
||||||
color: var(--text-light);
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
}
|
||||||
main { grid-area: main; padding: 2rem; }
|
.brand{display:flex; align-items:center; gap:12px}
|
||||||
aside {
|
.logo{width:36px; height:36px; border-radius:50%; background:var(--accent); color:#121212;
|
||||||
grid-area: aside;
|
display:grid; place-content:center; box-shadow:0 0 0 6px var(--glow)}
|
||||||
background: var(--accent);
|
h1{margin:0; font-size:20px; letter-spacing:.2px}
|
||||||
color: var(--text-light);
|
.pill{display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:999px;
|
||||||
padding: 1.5rem;
|
border:1px solid var(--border); background:#101010; color:var(--muted); font-weight:600}
|
||||||
overflow-y: auto;
|
.pill b{color:#fff; letter-spacing:.08em}
|
||||||
}
|
|
||||||
footer {
|
|
||||||
grid-area: footer;
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--text-light);
|
|
||||||
text-align: center;
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
.section-title {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
border-bottom: 2px solid var(--primary-color);
|
|
||||||
padding-bottom: 0.3rem;
|
|
||||||
}
|
|
||||||
ul { list-style: none; }
|
|
||||||
#controls, .round-controls { margin: 1rem 0; text-align: center; }
|
|
||||||
button {
|
|
||||||
background: var(--primary-color);
|
|
||||||
border: none;
|
|
||||||
color: var(--text-light);
|
|
||||||
padding: 0.6rem 1.2rem;
|
|
||||||
margin: 0.3rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
button:hover:not(:disabled) { background: #0c903a; }
|
|
||||||
button:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
||||||
|
|
||||||
/* Rundenbereich */
|
.row{display:flex; gap:10px; align-items:center; flex-wrap:wrap}
|
||||||
#roundArea {
|
.btn{border:0; padding:10px 14px; border-radius:999px; font-weight:700; cursor:pointer;
|
||||||
margin-top: 1rem;
|
transition:transform .06s, filter .15s, background .2s}
|
||||||
background: var(--bg-dark);
|
.btn:active{transform:translateY(1px)}
|
||||||
color: var(--text-light);
|
.btn-primary{background:var(--accent); color:#0a0a0a}
|
||||||
padding: 0.7rem 1rem;
|
.btn-primary:hover{filter:brightness(1.05)}
|
||||||
border-radius: 6px;
|
.btn-primary:active{background:var(--accent-press)}
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
.btn-ghost{background:transparent; color:var(--text); border:1px solid var(--border)}
|
||||||
text-align: center;
|
.btn-ghost:hover{border-color:var(--muted)}
|
||||||
}
|
.muted{color:var(--muted)}
|
||||||
#songEmbed iframe { border-radius: 6px; }
|
|
||||||
|
|
||||||
:root {
|
/* Grid */
|
||||||
/* 80% der Haupt-Container-Breite, max. 400px */
|
.grid{display:grid; grid-template-columns:1.1fr .9fr; gap:24px; margin-top:20px}
|
||||||
--option-circle-size: 80%;
|
@media (max-width:980px){.grid{grid-template-columns:1fr}}
|
||||||
--option-max-size: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card{background:var(--elev); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow)}
|
||||||
|
.card-hd{padding:18px 18px 8px; display:flex; align-items:center; justify-content:space-between}
|
||||||
|
.card-bd{padding:16px 18px 20px}
|
||||||
|
|
||||||
|
/* Now Playing */
|
||||||
|
.now{display:grid; grid-template-columns:96px 1fr; gap:16px; align-items:center}
|
||||||
|
.cover{width:96px; height:96px; border-radius:10px; background:#0d0d0d; border:1px solid var(--border);
|
||||||
|
overflow:hidden; display:grid; place-content:center}
|
||||||
|
.cover img{width:100%; height:100%; object-fit:cover}
|
||||||
|
.track-title{font-size:18px; font-weight:700; margin:0 0 2px}
|
||||||
|
.artists{color:var(--muted); margin:0 0 6px}
|
||||||
|
#songEmbed iframe{border-radius:10px; width:100%; height:80px; border:0}
|
||||||
|
|
||||||
|
/* Round area (Kreis) */
|
||||||
|
#roundArea[hidden]{display:none}
|
||||||
#options{
|
#options{
|
||||||
position: relative;
|
position:relative; width:100%; max-width:520px; aspect-ratio:1; margin:14px 0 6px;
|
||||||
width: var(--option-circle-size);
|
border-radius:50%;
|
||||||
max-width: var(--option-max-size);
|
|
||||||
aspect-ratio: 1; /* Höhe = Breite */
|
|
||||||
margin: 2rem auto;
|
|
||||||
}
|
}
|
||||||
|
.player-option{
|
||||||
|
|
||||||
#options .player-option {
|
|
||||||
position:absolute;
|
position:absolute;
|
||||||
width: 100px;
|
|
||||||
height: 40px;
|
|
||||||
left:50%;
|
left:50%;
|
||||||
top:50%;
|
top:50%;
|
||||||
transform-origin: center calc(-50% - 10px);
|
transform-origin:center;
|
||||||
text-align: center;
|
background:#0f0f0f; color:var(--text);
|
||||||
padding: 0.5rem;
|
border:1px solid var(--border);
|
||||||
border-radius: 4px;
|
border-radius:999px; padding:8px 12px;
|
||||||
|
font-weight:700;
|
||||||
|
white-space:nowrap;
|
||||||
|
transition:filter .15s, transform .06s, background .2s, box-shadow .2s;
|
||||||
|
box-shadow:0 0 0 0 var(--glow);
|
||||||
}
|
}
|
||||||
#options .player-option:nth-child(1) { transform: rotate(0deg) translate(0, var(--option-radius)); }
|
.player-option:hover{filter:brightness(1.1)}
|
||||||
#options .player-option:nth-child(2) { transform: rotate(90deg) translate(0, var(--option-radius)); }
|
.player-option:active{transform:scale(.98)}
|
||||||
#options .player-option:nth-child(3) { transform: rotate(180deg) translate(0, var(--option-radius)); }
|
.player-option:disabled{opacity:.7}
|
||||||
#options .player-option:nth-child(4) { transform: rotate(270deg) translate(0, var(--option-radius)); }
|
.player-option.correct{background:var(--accent); color:#0a0a0a; box-shadow:0 0 0 6px var(--glow)}
|
||||||
|
|
||||||
|
/* Players + Scoreboard */
|
||||||
|
#playersList{list-style:none; padding:0; margin:0; display:grid;
|
||||||
|
grid-template-columns:repeat(auto-fill, minmax(200px,1fr)); gap:12px}
|
||||||
|
#playersList > *{display:flex; align-items:center; gap:10px; padding:10px;
|
||||||
|
border:1px solid var(--border); border-radius:12px; background:#101010}
|
||||||
|
|
||||||
|
#scoreboard{list-style:none; padding:0; margin:0}
|
||||||
|
#scoreboard li{display:flex; justify-content:space-between; padding:10px 4px; border-bottom:1px solid var(--border)}
|
||||||
|
|
||||||
|
/* Aside / Device select */
|
||||||
|
aside{background:transparent}
|
||||||
|
.section-title{font-weight:700; margin:0 0 10px}
|
||||||
|
#songList{list-style:none; padding:0; margin:0 0 14px; display:flex; flex-direction:column; gap:8px}
|
||||||
|
#songList li{padding:10px 12px; border:1px solid var(--border); border-radius:10px; background:#101010}
|
||||||
|
|
||||||
|
#deviceSelectArea label{color:var(--muted); font-size:13px}
|
||||||
|
#deviceSelect{
|
||||||
|
width:100%; margin-top:8px;
|
||||||
|
background:#0f0f0f; color:#fff; border:1px solid var(--border); border-radius:12px; padding:10px 12px; outline:none;
|
||||||
|
}
|
||||||
|
#deviceSelect:focus{border-color:var(--accent); box-shadow:0 0 0 3px var(--glow)}
|
||||||
|
|
||||||
|
/* Toast (Clipboard-Notice) */
|
||||||
|
.toast{position:fixed; left:50%; bottom:22px; transform:translateX(-50%);
|
||||||
|
background:#101010; color:#fff; border:1px solid var(--border);
|
||||||
|
padding:10px 14px; border-radius:999px; box-shadow:var(--shadow); z-index:50; display:none}
|
||||||
|
.toast.show{display:block}
|
||||||
|
|
||||||
|
/* Slide-out Drawer */
|
||||||
|
.drawer{position:fixed; top:0; right:0; height:100%; width:340px; background:#0f0f0f;
|
||||||
|
border-left:1px solid var(--border); transform:translateX(100%); transition:transform .25s ease;
|
||||||
|
z-index:60; display:flex; flex-direction:column}
|
||||||
|
.drawer.open{transform:translateX(0)}
|
||||||
|
.drawer header{padding:16px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center}
|
||||||
|
.drawer .list{padding:12px 16px; overflow:auto}
|
||||||
|
.drawer .track{display:flex; align-items:center; gap:10px; padding:8px 6px; border-radius:10px}
|
||||||
|
.drawer .track:hover{background:#141414}
|
||||||
|
.drawer .tcover{width:40px; height:40px; border-radius:6px; background:#1a1a1a; border:1px solid var(--border)}
|
||||||
|
.drawer .meta{display:flex; flex-direction:column}
|
||||||
|
.drawer .meta b{font-size:13px}
|
||||||
|
.drawer .meta span{font-size:12px; color:var(--muted)}
|
||||||
|
|
||||||
|
/* === Pie-Slices === */
|
||||||
|
#options svg.options-svg { width:100%; height:auto; display:block; border-radius:50%; }
|
||||||
|
.wedge { fill:#181818; stroke: #ffffff; stroke-width:4; cursor:pointer; transition:filter .15s, fill .2s; }
|
||||||
|
.wedge:hover { filter:brightness(1.08); }
|
||||||
|
.wedge.selected { fill: #0d351c; }
|
||||||
|
/* .wedge.correct { fill:#1db954; } */
|
||||||
|
.wedge.correct { fill: var(--accent) !important; } /* grün */
|
||||||
|
.wedge.wrong { fill: #e22134 !important; } /* rot */
|
||||||
|
.wedge.disabled { pointer-events:none; opacity:.85; }
|
||||||
|
.wedge-label { fill:#fff; font-weight:700; font-size:35px; pointer-events:none; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
|
<div class="brand">
|
||||||
|
<div class="logo" aria-hidden="true">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M12 2 2 7l10 5 10-5-10-5Zm10 7-10 5v9l10-5V9ZM2 9v9l10 5v-9L2 9Z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
<h1>Spotify Roulette</h1>
|
<h1>Spotify Roulette</h1>
|
||||||
<div>Spiel-Code: <span id="gameId" class="game-code"></span></div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="pill">Spiel-Code: <b id="gameId">—</b></span>
|
||||||
|
<button id="toggleSongList" class="btn btn-ghost" title="Zuletzt gehörte Songs">Songliste</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
|
||||||
<section>
|
<div class="grid">
|
||||||
<div class="section-title">Teilnehmer</div>
|
<!-- LEFT: Now Playing + Runde -->
|
||||||
<ul id="playersList"></ul>
|
<section class="card">
|
||||||
</section>
|
<div class="card-hd">
|
||||||
<div id="controls">
|
<h2 style="margin:0">Now Playing</h2>
|
||||||
<button id="startRound">Runde starten</button>
|
<div class="row">
|
||||||
|
<button id="startRound" class="btn btn-ghost">Runde starten</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-bd">
|
||||||
<section id="roundArea" hidden>
|
<section id="roundArea" hidden>
|
||||||
<div class="section-title">Wer hat’s gehört?</div>
|
<div id="songEmbed" style="margin-bottom:12px"></div>
|
||||||
<div id="songEmbed"></div>
|
<div id="options"></div>
|
||||||
<div class="round-controls" id="options">
|
<div id="result" class="muted" style="margin-top:10px"></div>
|
||||||
<!-- Buttons zu Auswahl verteilen: JS erzeugt <button class="player-option">Name</button> -->
|
<button id="nextRound" class="btn btn-primary" hidden>Nächster Song</button>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<div id="result"></div>
|
|
||||||
<button id="nextRound" hidden>Weiter</button>
|
|
||||||
</section>
|
</section>
|
||||||
<section>
|
|
||||||
<div class="section-title">Scoreboard</div>
|
<!-- RIGHT: Teilnehmer + Scoreboard -->
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-hd">
|
||||||
|
<h2 style="margin:0">Teilnehmer</h2>
|
||||||
|
<button id="refreshPlayers" class="btn btn-ghost">Aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-bd">
|
||||||
|
<ul id="playersList"></ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-hd"><h2 style="margin:0">Scoreboard</h2></div>
|
||||||
|
<div class="card-bd">
|
||||||
<ul id="scoreboard"></ul>
|
<ul id="scoreboard"></ul>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</div>
|
||||||
<aside>
|
|
||||||
<div class="section-title">Geladene Songs</div>
|
<!-- ASIDE: Geladene Songs + Wiedergabegerät -->
|
||||||
<ul id="songList"></ul>
|
|
||||||
<div id="deviceSelectArea"></div>
|
<div id="deviceSelectArea"></div>
|
||||||
</aside>
|
</aside>
|
||||||
<footer> Spotify Roulette – alles ausser arbeiten </footer>
|
</div>
|
||||||
|
|
||||||
|
<!-- Drawer -->
|
||||||
|
<aside class="drawer" id="songDrawer" aria-label="Zuletzt gehörte Songs">
|
||||||
|
<header>
|
||||||
|
<h3 style="margin:0">Zuletzt gehört</h3>
|
||||||
|
<button class="btn btn-ghost" id="closeDrawer">Schließen</button>
|
||||||
|
</header>
|
||||||
|
<div class="list" id="songListArea"></div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Toast -->
|
||||||
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<!-- Module -->
|
||||||
<script type="module" src="/js/utils.js"></script>
|
<script type="module" src="/js/utils.js"></script>
|
||||||
<script type="module" src="/js/start-round.js"></script>
|
<script type="module" src="/js/start-round.js"></script>
|
||||||
|
<script type="module" src="/js/device-select.js"></script> <!-- füllt #deviceSelectArea -->
|
||||||
<script type="module" src="/js/game.js"></script>
|
<script type="module" src="/js/game.js"></script>
|
||||||
<script type="module" src="/js/device-select.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,77 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Spotify – Zuletzt gehörte Songs</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Spotify Roulette – Login</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#121212; --card:#181818; --border:#282828; --text:#fff;
|
||||||
|
--muted:#b3b3b3; --accent:#1db954; --accent-press:#169e47; --glow:rgba(29,185,84,.25);
|
||||||
|
--radius:16px; --shadow:0 10px 30px rgba(0,0,0,.35)
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{height:100%}
|
||||||
|
body{
|
||||||
|
margin:0; font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
|
||||||
|
color:var(--text);
|
||||||
|
background: radial-gradient(1000px 500px at 20% -10%, rgba(29,185,84,.14), transparent 60%),
|
||||||
|
radial-gradient(900px 450px at 110% -20%, rgba(29,185,84,.12), transparent 70%),
|
||||||
|
var(--bg);
|
||||||
|
display:grid; place-items:center; padding:24px;
|
||||||
|
}
|
||||||
|
.shell{width:100%; max-width:560px}
|
||||||
|
header{display:flex; align-items:center; gap:12px; margin-bottom:18px}
|
||||||
|
.logo{width:40px; height:40px; border-radius:50%; background:var(--accent);
|
||||||
|
display:grid; place-content:center; color:#121212; box-shadow:0 0 0 6px var(--glow)}
|
||||||
|
h1{margin:0; font-size:24px; letter-spacing:.2px}
|
||||||
|
.card{background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow)}
|
||||||
|
.card .bd{padding:22px}
|
||||||
|
p.muted{color:var(--muted); margin:8px 0 0}
|
||||||
|
label{display:block; color:var(--muted); margin-bottom:6px}
|
||||||
|
input[type="text"]{
|
||||||
|
width:100%; background:#0f0f0f; color:var(--text);
|
||||||
|
border:1px solid var(--border); border-radius:12px; padding:12px 14px; outline:none;
|
||||||
|
}
|
||||||
|
input:focus{border-color:var(--accent); box-shadow:0 0 0 3px var(--glow)}
|
||||||
|
.row{display:flex; gap:12px; align-items:center; margin-top:14px}
|
||||||
|
.btn{border:0; padding:12px 16px; border-radius:999px; font-weight:700; cursor:pointer;
|
||||||
|
transition:transform .06s ease, filter .15s ease, background .2s ease}
|
||||||
|
.btn:active{transform:translateY(1px)}
|
||||||
|
.btn-primary{background:var(--accent); color:#0a0a0a}
|
||||||
|
.btn-primary:hover{filter:brightness(1.05)}
|
||||||
|
.btn-primary:active{background:var(--accent-press)}
|
||||||
|
.hint{display:flex; gap:8px; align-items:center; margin-top:14px; color:var(--muted); font-size:14px}
|
||||||
|
.pill{display:inline-flex; gap:8px; align-items:center; padding:6px 10px; border-radius:999px;
|
||||||
|
border:1px solid var(--border); background:#101010; color:var(--muted); font-size:12px}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Spotify – Zuletzt gehörte Songs</h1>
|
<div class="shell">
|
||||||
<form id="usernameForm">
|
<header>
|
||||||
<label for="username">Benutzername:</label>
|
<div class="logo" aria-hidden="true">
|
||||||
<input type="text" id="username" name="username" required>
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2 2 7l10 5 10-5-10-5Zm10 7-10 5v9l10-5V9ZM2 9v9l10 5v-9L2 9Z"/></svg>
|
||||||
<button type="submit">Weiter mit Spotify</button>
|
</div>
|
||||||
|
<h1>Spotify Roulette</h1>
|
||||||
|
<span class="pill" style="margin-left:auto">Status: <b style="color:#fff">Abgemeldet</b></span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="card" aria-label="Login">
|
||||||
|
<div class="bd">
|
||||||
|
<form id="usernameForm" autocomplete="on">
|
||||||
|
<label for="username">Benutzername</label>
|
||||||
|
<input id="username" name="username" type="text" placeholder="Dein Name" required />
|
||||||
|
<div class="row">
|
||||||
|
<button class="btn btn-primary" type="submit">Weiter mit Spotify</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<ul id="tracks"></ul>
|
<p class="hint">Du wirst zu Spotify weitergeleitet, um die Berechtigungen zu bestätigen.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/js/login.js"></script>
|
<script type="module" src="/js/login.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,31 @@ import { setupStartRound } from "./start-round.js";
|
||||||
const gameId = getParam("gameId");
|
const gameId = getParam("gameId");
|
||||||
const username = getParam("username");
|
const username = getParam("username");
|
||||||
|
|
||||||
|
// --- kleine Helper ---
|
||||||
|
function showToast(msg, ms = 2200) {
|
||||||
|
const t = document.getElementById("toast");
|
||||||
|
if (!t) return;
|
||||||
|
t.textContent = msg;
|
||||||
|
t.classList.add("show");
|
||||||
|
setTimeout(() => t.classList.remove("show"), ms);
|
||||||
|
}
|
||||||
|
function esc(s) {
|
||||||
|
const d = document.createElement("div");
|
||||||
|
d.textContent = String(s ?? "");
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
// 1) Parameter prüfen
|
// 1) Parameter prüfen
|
||||||
if (!gameId || !username) {
|
if (!gameId || !username) {
|
||||||
alert("Ungültige oder fehlende URL-Parameter!");
|
alert("Ungültige oder fehlende URL-Parameter!");
|
||||||
throw new Error("Missing gameId or username");
|
throw new Error("Missing gameId or username");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Copy to clipboard (unverändert)
|
// 2) Copy to clipboard (unverändert, aber mit Toast)
|
||||||
(function copyCodeToClipboard(code) {
|
(function copyCodeToClipboard(code) {
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
navigator.clipboard.writeText(code)
|
navigator.clipboard.writeText(code)
|
||||||
.then(() => console.log(`GameCode ${code} copied to clipboard`))
|
.then(() => showToast(`Spiel-Code ${code} kopiert`))
|
||||||
.catch(err => console.error("Clipboard write failed:", err));
|
.catch(err => console.error("Clipboard write failed:", err));
|
||||||
} else {
|
} else {
|
||||||
const ta = document.createElement("textarea");
|
const ta = document.createElement("textarea");
|
||||||
|
|
@ -28,7 +42,7 @@ if (!gameId || !username) {
|
||||||
ta.select();
|
ta.select();
|
||||||
try {
|
try {
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
console.log(`GameCode ${code} copied via execCommand`);
|
showToast(`Spiel-Code ${code} kopiert`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Fallback copy failed:", err);
|
console.error("Fallback copy failed:", err);
|
||||||
}
|
}
|
||||||
|
|
@ -36,16 +50,14 @@ if (!gameId || !username) {
|
||||||
}
|
}
|
||||||
})(gameId);
|
})(gameId);
|
||||||
|
|
||||||
// 3) Visuelles Feedback (unverändert)
|
// 3) Spiel-Code ins DOM schreiben
|
||||||
const notice = document.createElement("div");
|
|
||||||
notice.textContent = `Spiel-Code ${gameId} in die Zwischenablage kopiert!`;
|
|
||||||
Object.assign(notice.style, { /* … Styles … */ });
|
|
||||||
document.body.append(notice);
|
|
||||||
setTimeout(() => notice.remove(), 3000);
|
|
||||||
|
|
||||||
// 4) Spiel-Code ins DOM schreiben
|
|
||||||
document.getElementById("gameId").textContent = gameId;
|
document.getElementById("gameId").textContent = gameId;
|
||||||
|
|
||||||
|
// 4) Drawer toggles für Songliste
|
||||||
|
const drawer = document.getElementById("songDrawer");
|
||||||
|
document.getElementById("toggleSongList")?.addEventListener("click", () => drawer?.classList.toggle("open"));
|
||||||
|
document.getElementById("closeDrawer")?.addEventListener("click", () => drawer?.classList.remove("open"));
|
||||||
|
|
||||||
// 5) WebSocket mit Reconnect-Logik
|
// 5) WebSocket mit Reconnect-Logik
|
||||||
let socket;
|
let socket;
|
||||||
function connectWebSocket() {
|
function connectWebSocket() {
|
||||||
|
|
@ -57,7 +69,7 @@ function connectWebSocket() {
|
||||||
socket.addEventListener("open", () => {
|
socket.addEventListener("open", () => {
|
||||||
console.log("WebSocket connected. Requesting player list...");
|
console.log("WebSocket connected. Requesting player list...");
|
||||||
socket.send(JSON.stringify({ type: "requestPlayers" }));
|
socket.send(JSON.stringify({ type: "requestPlayers" }));
|
||||||
setupStartRound(socket);
|
setupStartRound(socket); // deaktiviert den Start-Button beim Klicken
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addEventListener("message", async ({ data }) => {
|
socket.addEventListener("message", async ({ data }) => {
|
||||||
|
|
@ -66,7 +78,6 @@ function connectWebSocket() {
|
||||||
|
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "players":
|
case "players":
|
||||||
console.log("Empfangene Spieler:", msg.players);
|
|
||||||
renderList("#playersList", msg.players, username);
|
renderList("#playersList", msg.players, username);
|
||||||
break;
|
break;
|
||||||
case "reload":
|
case "reload":
|
||||||
|
|
@ -106,13 +117,25 @@ const optionsDiv = document.getElementById("options");
|
||||||
const resultP = document.getElementById("result");
|
const resultP = document.getElementById("result");
|
||||||
const scoreboard = document.getElementById("scoreboard");
|
const scoreboard = document.getElementById("scoreboard");
|
||||||
|
|
||||||
// 8) Funktion zum Anzeigen einer neuen Runde
|
// --- SVG helpers für Pie-Slices ---
|
||||||
//let playLock = false;
|
const SVGNS = "http://www.w3.org/2000/svg";
|
||||||
|
const VB = 1000; // viewBox 0..1000
|
||||||
|
const CX = 500, CY = 500, R = 480;
|
||||||
|
|
||||||
async function handleRoundStart({ownerOptions, songUri, allTracks, trackInfos}) {
|
function polar(cx, cy, r, deg){
|
||||||
|
const rad = (deg - 90) * Math.PI/180; // 0° oben
|
||||||
|
return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
|
||||||
|
}
|
||||||
|
function wedgePath(cx, cy, r, a0, a1){
|
||||||
|
const p0 = polar(cx, cy, r, a0);
|
||||||
|
const p1 = polar(cx, cy, r, a1);
|
||||||
|
const largeArc = ((a1 - a0 + 360) % 360) > 180 ? 1 : 0;
|
||||||
|
return `M ${cx} ${cy} L ${p0.x} ${p0.y} A ${r} ${r} 0 ${largeArc} 1 ${p1.x} ${p1.y} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Neue Runde anzeigen
|
||||||
|
async function handleRoundStart({ ownerOptions, songUri, trackInfos }) {
|
||||||
// UI zurücksetzen
|
// UI zurücksetzen
|
||||||
//if (playLock) return; // Verhindert parallele Ausführung
|
|
||||||
//playLock = true;
|
|
||||||
resultP.textContent = "";
|
resultP.textContent = "";
|
||||||
optionsDiv.innerHTML = "";
|
optionsDiv.innerHTML = "";
|
||||||
songEmbed.innerHTML = "";
|
songEmbed.innerHTML = "";
|
||||||
|
|
@ -126,54 +149,97 @@ async function handleRoundStart({ownerOptions, songUri, allTracks, trackInfos})
|
||||||
allow="encrypted-media">
|
allow="encrypted-media">
|
||||||
</iframe>`;
|
</iframe>`;
|
||||||
|
|
||||||
|
// Spotify-Playback (optional, wenn vorhanden)
|
||||||
if (window.playOnSpotify && typeof window.playOnSpotify === "function") {
|
if (window.playOnSpotify && typeof window.playOnSpotify === "function") {
|
||||||
await window.playOnSpotify(songUri, username); // Warten bis fertig
|
await window.playOnSpotify(songUri, username);
|
||||||
}
|
}
|
||||||
|
|
||||||
//if (window.playOnSpotify && typeof window.playOnSpotify === "function") {
|
// Optionen als Tortenstücke (SVG) rendern
|
||||||
// window.playOnSpotify(songUri, username);
|
const svg = document.createElementNS(SVGNS, "svg");
|
||||||
//}
|
svg.setAttribute("viewBox", `0 0 ${VB} ${VB}`);
|
||||||
|
svg.setAttribute("class", "options-svg");
|
||||||
// Dynamische Kreisverteilung der Buttons
|
optionsDiv.innerHTML = "";
|
||||||
// Warten, bis #options gerendert ist
|
optionsDiv.appendChild(svg);
|
||||||
setTimeout(() => {
|
|
||||||
const optsRect = optionsDiv.getBoundingClientRect();
|
|
||||||
const radius = Math.min(optsRect.width, optsRect.height) / 2 - 50; // 50px Abstand zum Rand
|
|
||||||
|
|
||||||
|
const n = ownerOptions.length;
|
||||||
ownerOptions.forEach((user, i) => {
|
ownerOptions.forEach((user, i) => {
|
||||||
const btn = document.createElement("button");
|
const a0 = (360 / n) * i;
|
||||||
btn.textContent = user;
|
const a1 = (360 / n) * (i + 1);
|
||||||
btn.classList.add("player-option");
|
|
||||||
const angle = 360 * i / ownerOptions.length;
|
const path = document.createElementNS(SVGNS, "path");
|
||||||
btn.style.transform = `rotate(${angle}deg) translateY(-${radius}px) rotate(${-angle}deg)`;
|
path.setAttribute("d", wedgePath(CX, CY, R, a0, a1));
|
||||||
btn.addEventListener("click", () => {
|
path.setAttribute("class", "wedge");
|
||||||
|
path.setAttribute("data-user", user);
|
||||||
|
path.addEventListener("click", () => {
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
type: "guess",
|
type: "guess",
|
||||||
username: username,
|
username: username,
|
||||||
guess: user
|
guess: user
|
||||||
}));
|
}));
|
||||||
optionsDiv.querySelectorAll("button").forEach(b => b.disabled = true);
|
svg.querySelectorAll(".wedge").forEach(w => w.classList.add("disabled"));
|
||||||
|
path.classList.add("selected");
|
||||||
});
|
});
|
||||||
optionsDiv.appendChild(btn);
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
|
// Label mittig im Segment
|
||||||
|
const mid = (a0 + a1) / 2;
|
||||||
|
const P = polar(CX, CY, R * 0.58, mid);
|
||||||
|
const text = document.createElementNS(SVGNS, "text");
|
||||||
|
|
||||||
|
const span = a1 - a0; // Winkelbreite des Segments
|
||||||
|
const font = Math.max(18, Math.min(42, 26 * (span / 60)));
|
||||||
|
text.setAttribute("font-size", font);
|
||||||
|
text.setAttribute("font-weight", "800");
|
||||||
|
// Bessere Lesbarkeit auf dunklem Slice
|
||||||
|
text.setAttribute("paint-order", "stroke");
|
||||||
|
text.setAttribute("stroke", "#000");
|
||||||
|
text.setAttribute("stroke-width", "2");
|
||||||
|
text.setAttribute("fill", "#fff");
|
||||||
|
// überschreibt CSS-Größe
|
||||||
|
text.setAttribute("x", P.x);
|
||||||
|
text.setAttribute("y", P.y);
|
||||||
|
text.setAttribute("class", "wedge-label");
|
||||||
|
text.setAttribute("text-anchor", "middle");
|
||||||
|
text.setAttribute("dominant-baseline", "middle");
|
||||||
|
text.textContent = user;
|
||||||
|
|
||||||
|
svg.appendChild(path);
|
||||||
|
svg.appendChild(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start-Button ausblenden + Rundensektion einblenden
|
||||||
startBtn.hidden = true;
|
startBtn.hidden = true;
|
||||||
|
startBtn.disabled = true;
|
||||||
roundArea.hidden = false;
|
roundArea.hidden = false;
|
||||||
|
|
||||||
|
// Geladene Songs (beide Orte befüllen: Aside + Drawer)
|
||||||
const songList = document.getElementById("songList");
|
const songList = document.getElementById("songList");
|
||||||
songList.innerHTML = "";
|
const songListArea = document.getElementById("songListArea");
|
||||||
//trackinfos ist eine map bestehend aus aus Spielername und Liste von Track-Infos
|
|
||||||
const userTracks = trackInfos?.[username] ?? [];
|
const userTracks = trackInfos?.[username] ?? [];
|
||||||
userTracks.forEach(trackInfo => {
|
|
||||||
|
if (songList) {
|
||||||
|
songList.innerHTML = "";
|
||||||
|
userTracks.forEach(t => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.textContent = trackInfo;
|
li.textContent = t;
|
||||||
songList.appendChild(li);
|
songList.appendChild(li);
|
||||||
});
|
});
|
||||||
//playLock = false;
|
}
|
||||||
|
if (songListArea) {
|
||||||
|
songListArea.innerHTML = "";
|
||||||
|
userTracks.forEach(t => {
|
||||||
|
// Erwartetes Format "Titel - Künstler"
|
||||||
|
const [title, artists] = String(t).split(" - ");
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "track";
|
||||||
|
row.innerHTML = `
|
||||||
|
<div class="tcover"></div>
|
||||||
|
<div class="meta"><b>${esc(title ?? t)}</b><span>${esc(artists ?? "")}</span></div>`;
|
||||||
|
songListArea.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9) Funktion zum Anzeigen des Ergebnisses
|
// 7) Ergebnis + Weiter-Button
|
||||||
function renderScoreboard(scores) {
|
function renderScoreboard(scores) {
|
||||||
scoreboard.innerHTML = "";
|
scoreboard.innerHTML = "";
|
||||||
Object.entries(scores).forEach(([user, pts]) => {
|
Object.entries(scores).forEach(([user, pts]) => {
|
||||||
|
|
@ -186,14 +252,35 @@ function renderScoreboard(scores) {
|
||||||
function handleRoundResult({ scores, guesses, owner }) {
|
function handleRoundResult({ scores, guesses, owner }) {
|
||||||
renderScoreboard(scores);
|
renderScoreboard(scores);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wedges = document.querySelectorAll("#options .wedge");
|
||||||
|
|
||||||
|
// Owner-Slice immer grün
|
||||||
|
wedges.forEach(w => {
|
||||||
|
if (w.getAttribute("data-user") === owner) w.classList.add("correct");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nur die EIGENE Auswahl einfärben: rot wenn falsch, sonst grün
|
||||||
|
const myGuess = guesses?.[username];
|
||||||
|
if (myGuess) {
|
||||||
|
const myWedge = Array.from(wedges).find(w => w.getAttribute("data-user") === myGuess);
|
||||||
|
if (myWedge) {
|
||||||
|
if (myGuess === owner) {
|
||||||
|
myWedge.classList.add("correct");
|
||||||
|
} else {
|
||||||
|
myWedge.classList.add("wrong"); // nur dieser wird rot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
resultP.innerHTML = "";
|
resultP.innerHTML = "";
|
||||||
Object.entries(guesses).forEach(([user, guess]) => {
|
Object.entries(guesses || {}).forEach(([user, guess]) => {
|
||||||
const correct = guess === owner;
|
const correct = guess === owner;
|
||||||
const icon = correct ? "✅" : "❌";
|
const icon = correct ? "✅" : "❌";
|
||||||
const delta = correct ? "+3" : "-1";
|
const delta = correct ? "+3" : "-1";
|
||||||
const msg = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"} [${delta}]`;
|
|
||||||
const p = document.createElement("p");
|
const p = document.createElement("p");
|
||||||
p.textContent = msg;
|
p.textContent = `${icon} ${user} hat auf ${guess} getippt${correct ? " (richtig!)" : " (falsch)"} [${delta}]`;
|
||||||
resultP.appendChild(p);
|
resultP.appendChild(p);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -211,18 +298,19 @@ function handleRoundResult({ scores, guesses, owner }) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 8) Spielende -> Start-Button zurück
|
||||||
function handleGameEnd({ winner }) {
|
function handleGameEnd({ winner }) {
|
||||||
resultP.textContent = `🎉 ${winner} hat gewonnen!`;
|
resultP.textContent = `🎉 ${winner} hat gewonnen!`;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
startBtn.hidden = false;
|
startBtn.hidden = false;
|
||||||
roundArea.hidden = true;
|
|
||||||
startBtn.disabled = false;
|
startBtn.disabled = false;
|
||||||
|
roundArea.hidden = true;
|
||||||
scoreboard.innerHTML = "";
|
scoreboard.innerHTML = "";
|
||||||
}, 6000);
|
}, 6000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spotify-Playback Funktion
|
// Spotify-Playback Funktion (unverändert)
|
||||||
// game.js
|
|
||||||
async function playOnSpotify(trackUri, username) {
|
async function playOnSpotify(trackUri, username) {
|
||||||
const deviceId = document.getElementById("deviceSelect")?.value;
|
const deviceId = document.getElementById("deviceSelect")?.value;
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,13 @@ document.getElementById("createGame").addEventListener("click", () => {
|
||||||
window.location.href = `/create-game.html?username=${encodeURIComponent(username)}`;
|
window.location.href = `/create-game.html?username=${encodeURIComponent(username)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("joinGame").addEventListener("click", () => {
|
//document.getElementById("joinGame").addEventListener("click", () => {
|
||||||
window.location.href = username
|
// window.location.href = username
|
||||||
? `/join-game.html?username=${encodeURIComponent(username)}`
|
// ? `/join-game.html?username=${encodeURIComponent(username)}`
|
||||||
: "/join-game.html";
|
// : "/join-game.html";
|
||||||
|
//});
|
||||||
|
// NEU: statt Redirect -> Fokus auf Eingabe
|
||||||
|
document.getElementById("joinGame")?.addEventListener("click", () => {
|
||||||
|
const input = document.getElementById("gameId");
|
||||||
|
if (input) { input.focus(); input.select(); }
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,231 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Lobby – Spotify Roulette</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Spotify Roulette – Lobby</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#121212; --card:#181818; --border:#282828; --text:#fff; --muted:#b3b3b3;
|
||||||
|
--accent:#1db954; --accent-press:#169e47; --glow:rgba(29,185,84,.25);
|
||||||
|
--radius:16px; --shadow:0 10px 30px rgba(0,0,0,.35)
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{height:100%}
|
||||||
|
body{
|
||||||
|
margin:0; font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial;
|
||||||
|
color:var(--text);
|
||||||
|
background: radial-gradient(1000px 500px at 20% -10%, rgba(29,185,84,.14), transparent 60%),
|
||||||
|
radial-gradient(900px 450px at 110% -20%, rgba(29,185,84,.12), transparent 70%),
|
||||||
|
var(--bg);
|
||||||
|
padding:24px;
|
||||||
|
}
|
||||||
|
.container{max-width:1100px; margin:0 auto}
|
||||||
|
header{display:flex; align-items:center; gap:12px; padding-bottom:12px; border-bottom:1px solid var(--border)}
|
||||||
|
.logo{width:40px; height:40px; border-radius:50%; background:var(--accent); color:#121212;
|
||||||
|
display:grid; place-content:center; box-shadow:0 0 0 6px var(--glow)}
|
||||||
|
h1{margin:0; font-size:24px}
|
||||||
|
.pill{margin-left:auto; display:inline-flex; gap:8px; align-items:center; padding:6px 10px; border-radius:999px;
|
||||||
|
border:1px solid var(--border); background:#101010; color:var(--muted); font-size:12px}
|
||||||
|
|
||||||
|
.grid{display:grid; grid-template-columns:1fr 1fr; gap:20px; margin-top:20px}
|
||||||
|
@media (max-width:900px){ .grid{grid-template-columns:1fr} }
|
||||||
|
|
||||||
|
.card{background:var(--card); border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow)}
|
||||||
|
.card .hd{padding:18px 18px 8px}
|
||||||
|
.card .bd{padding:0 18px 18px}
|
||||||
|
.muted{color:var(--muted)}
|
||||||
|
|
||||||
|
.btn{width:100%; border:0; border-radius:14px; padding:14px 16px; cursor:pointer; font-weight:700; display:flex; align-items:center; justify-content:center; gap:10px; transition:transform .06s ease, filter .15s ease, background .2s ease}
|
||||||
|
.btn:active{transform:translateY(1px)}
|
||||||
|
.btn-primary{background:var(--accent); color:#0a0a0a}
|
||||||
|
.btn-primary:hover{filter:brightness(1.05)}
|
||||||
|
.btn-primary:active{background:var(--accent-press)}
|
||||||
|
.btn-ghost{background:transparent; color:var(--text); border:1px solid var(--border)}
|
||||||
|
.btn-ghost:hover{border-color:var(--muted)}
|
||||||
|
|
||||||
|
.tile{display:flex; gap:14px; align-items:center; padding:14px; border:1px solid var(--border); border-radius:14px; background:#101010}
|
||||||
|
.tile .icon{width:44px; height:44px; border-radius:10px; display:grid; place-content:center; background:#0e0e0e; border:1px solid var(--border)}
|
||||||
|
.tile .txt{display:flex; flex-direction:column}
|
||||||
|
.tile .txt b{font-size:16px}
|
||||||
|
.tile .txt span{font-size:13px; color:var(--muted)}
|
||||||
|
|
||||||
|
/* Join form – Spotify-like Input */
|
||||||
|
.row{display:flex; gap:12px; align-items:flex-end; flex-wrap:wrap}
|
||||||
|
.field{display:flex; flex-direction:column; gap:6px}
|
||||||
|
label{color:var(--muted); font-size:13px}
|
||||||
|
|
||||||
|
.inputWrap{position:relative; min-width:220px}
|
||||||
|
.inputWrap .leading-icon{
|
||||||
|
position:absolute; left:12px; top:50%; transform:translateY(-50%); opacity:.9;
|
||||||
|
}
|
||||||
|
.inputWrap .chip{
|
||||||
|
position:absolute; right:10px; top:50%; transform:translateY(-50%);
|
||||||
|
font-size:11px; color:var(--muted);
|
||||||
|
border:1px solid var(--border); background:#0d0d0d;
|
||||||
|
padding:4px 8px; border-radius:999px;
|
||||||
|
}
|
||||||
|
input#gameId{
|
||||||
|
width:100%;
|
||||||
|
background:#0b0b0b; color:var(--text);
|
||||||
|
border:1px solid var(--border);
|
||||||
|
border-radius:999px;
|
||||||
|
padding:14px 78px 14px 42px; /* Platz für Icon + Chip */
|
||||||
|
outline:none;
|
||||||
|
letter-spacing:.12em; font-weight:600;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255,255,255,.02);
|
||||||
|
transition:border-color .2s, box-shadow .2s, background .2s, transform .06s;
|
||||||
|
}
|
||||||
|
input#gameId::placeholder{color:#777}
|
||||||
|
input#gameId:focus{
|
||||||
|
border-color:var(--accent);
|
||||||
|
box-shadow:0 0 0 4px var(--glow), inset 0 1px 0 rgba(255,255,255,.02);
|
||||||
|
background:#0e0e0e;
|
||||||
|
}
|
||||||
|
input#gameId:active{transform:translateY(1px)}
|
||||||
|
|
||||||
|
/* Icon-only clipboard button */
|
||||||
|
.icon-btn{width:auto; padding:10px; border-radius:999px; display:inline-flex; align-items:center; justify-content:center}
|
||||||
|
.icon-btn svg{display:block}
|
||||||
|
.icon-btn--accent{
|
||||||
|
background:var(--accent); color:#0a0a0a;
|
||||||
|
box-shadow:0 0 0 0 var(--glow);
|
||||||
|
transition: box-shadow .2s, filter .15s, transform .06s, background .2s;
|
||||||
|
}
|
||||||
|
.icon-btn--accent:hover{ filter:brightness(1.05); box-shadow:0 0 0 6px var(--glow) }
|
||||||
|
.icon-btn--accent:active{ background:var(--accent-press); transform:translateY(1px) }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<div class="logo" aria-hidden="true">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2 2 7l10 5 10-5-10-5Zm10 7-10 5v9l10-5V9ZM2 9v9l10 5v-9L2 9Z"/></svg>
|
||||||
|
</div>
|
||||||
<h1>Lobby</h1>
|
<h1>Lobby</h1>
|
||||||
<p>Was möchtest du tun?</p>
|
<span class="pill">Angemeldet als <b id="uname" style="color:#fff">—</b></span>
|
||||||
<button id="createGame">Spiel erstellen</button>
|
</header>
|
||||||
<button id="joinGame">Spiel beitreten</button>
|
|
||||||
|
<section class="grid" aria-label="Aktionen">
|
||||||
|
<div class="card">
|
||||||
|
<div class="hd"><h2 style="margin:0">Neues Spiel</h2></div>
|
||||||
|
<div class="bd">
|
||||||
|
<div class="tile" style="margin-bottom:12px">
|
||||||
|
<div class="icon" aria-hidden="true">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="#1db954"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2Z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="txt"><b>Spiel erstellen</b><span>Starte eine neue Runde und teile den Code.</span></div>
|
||||||
|
</div>
|
||||||
|
<button id="createGame" class="btn btn-primary">Spiel erstellen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="hd"><h2 style="margin:0">Spiel beitreten</h2></div>
|
||||||
|
<div class="bd">
|
||||||
|
<div class="tile" style="margin-bottom:12px">
|
||||||
|
<div class="icon" aria-hidden="true">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="#1db954"><path d="M12 2a6 6 0 0 1 6 6v2h2v12H4V10h2V8a6 6 0 0 1 6-6Zm0 2a4 4 0 0 0-4 4v2h8V8a4 4 0 0 0-4-4Z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="txt"><b>Bestehendem Spiel beitreten</b><span>Du hast schon einen Code? Gib ihn unten ein.</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Direktes Join-Formular in der Lobby -->
|
||||||
|
<form id="joinForm" class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label for="gameId">Spiel-Code</label>
|
||||||
|
<div class="inputWrap">
|
||||||
|
<span class="leading-icon" aria-hidden="true">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="#1db954">
|
||||||
|
<path d="M3 11h18v10H3V11Zm9-8a5 5 0 0 1 5 5v3h-2V8a3 3 0 1 0-6 0v3H7V8a5 5 0 0 1 5-5Z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input id="gameId" name="gameId"
|
||||||
|
placeholder="••••"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="\d{4}"
|
||||||
|
maxlength="4"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
required />
|
||||||
|
<span class="chip">4-stellig</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Icon-Button: Zwischenablage (grün) -->
|
||||||
|
<button type="button" id="pasteCode"
|
||||||
|
class="btn icon-btn icon-btn--accent"
|
||||||
|
aria-label="Code aus Zwischenablage einfügen"
|
||||||
|
title="Aus Zwischenablage">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M9 2h6a2 2 0 0 1 2 2h1a2 2 0 0 1 2 2v13a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V6a2 2 0 0 1 2-2h1a2 2 0 0 1 2-2Zm0 2a1 1 0 0 0-1 1h8a1 1 0 0 0-1-1H9Z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary" style="width:auto">Beitreten</button>
|
||||||
|
|
||||||
|
<!-- (legacy) -->
|
||||||
|
<button id="joinGame" type="button" style="display:none">Join (legacy)</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="muted" style="margin-top:8px">Tipp: Code ist vierstellig (z. B. 6767).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Username-Anzeige + Clipboard-Button -->
|
||||||
|
<script type="module">
|
||||||
|
// Username-Badge
|
||||||
|
const p = new URLSearchParams(location.search);
|
||||||
|
const u = p.get('username');
|
||||||
|
if (u) document.querySelector('#uname').textContent = u;
|
||||||
|
|
||||||
|
// Clipboard-Paste für Spiel-Code
|
||||||
|
const input = document.getElementById('gameId');
|
||||||
|
const pasteBtn = document.getElementById('pasteCode');
|
||||||
|
|
||||||
|
pasteBtn?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
// Clipboard ist nur über HTTPS/localhost verfügbar
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
const digits = (text.match(/\d/g) || []).join('').slice(0, 4);
|
||||||
|
|
||||||
|
if (!digits) {
|
||||||
|
alert('Keinen 4-stelligen Code in der Zwischenablage gefunden.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.value = digits;
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
// Optional auto-submit, wenn 4 Ziffern vorhanden
|
||||||
|
if (digits.length === 4) {
|
||||||
|
if (input.form?.requestSubmit) input.form.requestSubmit();
|
||||||
|
else input.form?.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert('Zwischenablage nicht verfügbar. Öffne die Seite über HTTPS oder erlaube Zugriff.');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="module" src="/js/lobby.js"></script>
|
<script type="module" src="/js/lobby.js"></script>
|
||||||
|
<!-- Join-Handling wiederverwenden -->
|
||||||
|
<script type="module" src="/js/join-game.js"></script>
|
||||||
|
|
||||||
|
<!-- Optional: alter Join-Button (legacy) fokussiert nur das Feld -->
|
||||||
|
<script>
|
||||||
|
const btn = document.getElementById('joinGame');
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener('click', (ev) => {
|
||||||
|
ev.preventDefault(); ev.stopImmediatePropagation();
|
||||||
|
const input = document.getElementById('gameId');
|
||||||
|
if (input) { input.focus(); input.select(); }
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue