|
@@ -8,214 +8,324 @@ await init();
|
|
|
|
|
|
|
|
const currentUserName = getUserFromRequest(Astro.request);
|
|
const currentUserName = getUserFromRequest(Astro.request);
|
|
|
if (!currentUserName) {
|
|
if (!currentUserName) {
|
|
|
- return Astro.redirect("/login?redirect=/play");
|
|
|
|
|
|
|
+ return Astro.redirect("/login?redirect=/play");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const currentUser = await getUser(currentUserName);
|
|
const currentUser = await getUser(currentUserName);
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-<Layout title="Play" currentUser={currentUserName} currentRating={currentUser?.rating ?? null}>
|
|
|
|
|
- <Nav currentUser={currentUserName} currentRating={currentUser?.rating ?? null} activePage="play" />
|
|
|
|
|
-
|
|
|
|
|
- <main class="container play-page">
|
|
|
|
|
- <h1 class="page-title" style="margin: 32px 0 24px">Start a Game</h1>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Mode selector -->
|
|
|
|
|
- <div class="mode-tabs">
|
|
|
|
|
- <button class="mode-tab active" id="tab-inperson" data-mode="inperson">
|
|
|
|
|
- <span class="mode-icon">🏓</span>
|
|
|
|
|
- <span class="mode-label">In Person</span>
|
|
|
|
|
- <span class="mode-desc">Track score for a physical game</span>
|
|
|
|
|
- </button>
|
|
|
|
|
- <button class="mode-tab" id="tab-virtual" data-mode="virtual">
|
|
|
|
|
- <span class="mode-icon">🌐</span>
|
|
|
|
|
- <span class="mode-label">Virtual</span>
|
|
|
|
|
- <span class="mode-desc">Play online with PeerJS</span>
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="play-grid">
|
|
|
|
|
- <!-- Create Game -->
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h2 class="card-title">Create Game</h2>
|
|
|
|
|
- <p class="card-desc" id="create-desc">Generate a link and share it with your opponent.</p>
|
|
|
|
|
-
|
|
|
|
|
- <button id="create-btn" class="btn" style="width:100%; padding:12px; margin-top:8px">
|
|
|
|
|
- Create Game Room
|
|
|
|
|
- </button>
|
|
|
|
|
-
|
|
|
|
|
- <div id="created-result" style="display:none; margin-top:24px">
|
|
|
|
|
- <div class="created-link-box">
|
|
|
|
|
- <p class="section-label">Game Link</p>
|
|
|
|
|
- <div class="link-row">
|
|
|
|
|
- <input type="text" id="game-link" readonly />
|
|
|
|
|
- <button id="copy-btn" class="btn btn-secondary" style="flex-shrink:0; width:auto; padding:10px 14px">Copy</button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+<Layout
|
|
|
|
|
+ title="Play"
|
|
|
|
|
+ currentUser={currentUserName}
|
|
|
|
|
+ currentRating={currentUser?.rating ?? null}
|
|
|
|
|
+>
|
|
|
|
|
+ <Nav
|
|
|
|
|
+ currentUser={currentUserName}
|
|
|
|
|
+ currentRating={currentUser?.rating ?? null}
|
|
|
|
|
+ activePage="play"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <main class="container play-page">
|
|
|
|
|
+ <h1 class="page-title" style="margin: 32px 0 24px">Start a Game</h1>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Mode selector -->
|
|
|
|
|
+ <div class="mode-tabs">
|
|
|
|
|
+ <button
|
|
|
|
|
+ class="mode-tab active"
|
|
|
|
|
+ id="tab-inperson"
|
|
|
|
|
+ data-mode="inperson"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="mode-icon">🏓</span>
|
|
|
|
|
+ <span class="mode-label">In Person</span>
|
|
|
|
|
+ <span class="mode-desc">Track score for a physical game</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button class="mode-tab" id="tab-virtual" data-mode="virtual">
|
|
|
|
|
+ <span class="mode-icon">🌐</span>
|
|
|
|
|
+ <span class="mode-label">Virtual</span>
|
|
|
|
|
+ <span class="mode-desc">Play online with PeerJS</span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div class="qr-section">
|
|
|
|
|
- <p class="section-label">QR Code</p>
|
|
|
|
|
- <div id="qr-container" class="qr-box">
|
|
|
|
|
- <img id="qr-img" src="" alt="QR code" />
|
|
|
|
|
|
|
+ <div class="play-grid">
|
|
|
|
|
+ <!-- Create Game -->
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <h2 class="card-title">Create Game</h2>
|
|
|
|
|
+ <p class="card-desc" id="create-desc">
|
|
|
|
|
+ Generate a link and share it with your opponent.
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ id="create-btn"
|
|
|
|
|
+ class="btn"
|
|
|
|
|
+ style="width:100%; padding:12px; margin-top:8px"
|
|
|
|
|
+ >
|
|
|
|
|
+ Create Game Room
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <div id="created-result" style="display:none; margin-top:24px">
|
|
|
|
|
+ <div class="created-link-box">
|
|
|
|
|
+ <p class="section-label">Game Link</p>
|
|
|
|
|
+ <div class="link-row">
|
|
|
|
|
+ <input type="text" id="game-link" readonly />
|
|
|
|
|
+ <button
|
|
|
|
|
+ id="copy-btn"
|
|
|
|
|
+ class="btn btn-secondary"
|
|
|
|
|
+ style="flex-shrink:0; width:auto; padding:10px 14px"
|
|
|
|
|
+ >Copy</button
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="qr-section">
|
|
|
|
|
+ <p class="section-label">QR Code</p>
|
|
|
|
|
+ <div id="qr-container" class="qr-box">
|
|
|
|
|
+ <img id="qr-img" src="" alt="QR code" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <a
|
|
|
|
|
+ id="open-game-btn"
|
|
|
|
|
+ href="#"
|
|
|
|
|
+ class="btn"
|
|
|
|
|
+ style="width:100%; padding:12px; display:block; text-align:center; margin-top:16px"
|
|
|
|
|
+ >
|
|
|
|
|
+ Open Game Room →
|
|
|
|
|
+ </a>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- <a id="open-game-btn" href="#" class="btn" style="width:100%; padding:12px; display:block; text-align:center; margin-top:16px">
|
|
|
|
|
- Open Game Room →
|
|
|
|
|
- </a>
|
|
|
|
|
|
|
+ <!-- Join Game -->
|
|
|
|
|
+ <div class="card">
|
|
|
|
|
+ <h2 class="card-title">Join Game</h2>
|
|
|
|
|
+ <p class="card-desc">
|
|
|
|
|
+ Have a game ID or link? Enter it below to join.
|
|
|
|
|
+ </p>
|
|
|
|
|
+
|
|
|
|
|
+ <form id="join-form" style="margin-top:8px">
|
|
|
|
|
+ <div class="form-group">
|
|
|
|
|
+ <label for="join-input">Game ID or Link</label>
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ id="join-input"
|
|
|
|
|
+ placeholder="e.g. abc123 or full URL"
|
|
|
|
|
+ autocomplete="off"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div id="join-error" class="error-msg" style="display:none">
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ class="btn"
|
|
|
|
|
+ style="width:100%; padding:12px"
|
|
|
|
|
+ >
|
|
|
|
|
+ Join Game →
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <!-- Join Game -->
|
|
|
|
|
- <div class="card">
|
|
|
|
|
- <h2 class="card-title">Join Game</h2>
|
|
|
|
|
- <p class="card-desc">Have a game ID or link? Enter it below to join.</p>
|
|
|
|
|
-
|
|
|
|
|
- <form id="join-form" style="margin-top:8px">
|
|
|
|
|
- <div class="form-group">
|
|
|
|
|
- <label for="join-input">Game ID or Link</label>
|
|
|
|
|
- <input type="text" id="join-input" placeholder="e.g. abc123 or full URL" autocomplete="off" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div id="join-error" class="error-msg" style="display:none"></div>
|
|
|
|
|
- <button type="submit" class="btn" style="width:100%; padding:12px">
|
|
|
|
|
- Join Game →
|
|
|
|
|
- </button>
|
|
|
|
|
- </form>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </main>
|
|
|
|
|
|
|
+ </main>
|
|
|
</Layout>
|
|
</Layout>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
- let selectedMode = "inperson";
|
|
|
|
|
-
|
|
|
|
|
- const tabs = document.querySelectorAll(".mode-tab");
|
|
|
|
|
- tabs.forEach(tab => {
|
|
|
|
|
- tab.addEventListener("click", () => {
|
|
|
|
|
- tabs.forEach(t => t.classList.remove("active"));
|
|
|
|
|
- tab.classList.add("active");
|
|
|
|
|
- selectedMode = (tab as HTMLElement).dataset.mode!;
|
|
|
|
|
- // Hide any previous result when switching modes
|
|
|
|
|
- const result = document.getElementById("created-result") as HTMLElement;
|
|
|
|
|
- result.style.display = "none";
|
|
|
|
|
|
|
+ let selectedMode = "inperson";
|
|
|
|
|
+
|
|
|
|
|
+ const tabs = document.querySelectorAll(".mode-tab");
|
|
|
|
|
+ tabs.forEach((tab) => {
|
|
|
|
|
+ tab.addEventListener("click", () => {
|
|
|
|
|
+ tabs.forEach((t) => t.classList.remove("active"));
|
|
|
|
|
+ tab.classList.add("active");
|
|
|
|
|
+ selectedMode = (tab as HTMLElement).dataset.mode!;
|
|
|
|
|
+ // Hide any previous result when switching modes
|
|
|
|
|
+ const result = document.getElementById(
|
|
|
|
|
+ "created-result",
|
|
|
|
|
+ ) as HTMLElement;
|
|
|
|
|
+ result.style.display = "none";
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const createBtn = document.getElementById("create-btn") as HTMLButtonElement;
|
|
|
|
|
- const createdResult = document.getElementById("created-result") as HTMLElement;
|
|
|
|
|
- const gameLink = document.getElementById("game-link") as HTMLInputElement;
|
|
|
|
|
- const copyBtn = document.getElementById("copy-btn") as HTMLButtonElement;
|
|
|
|
|
- const qrImg = document.getElementById("qr-img") as HTMLImageElement;
|
|
|
|
|
- const openGameBtn = document.getElementById("open-game-btn") as HTMLAnchorElement;
|
|
|
|
|
-
|
|
|
|
|
- createBtn.addEventListener("click", async () => {
|
|
|
|
|
- createBtn.disabled = true;
|
|
|
|
|
- createBtn.textContent = "Creating...";
|
|
|
|
|
-
|
|
|
|
|
- const res = await fetch("/api/game", {
|
|
|
|
|
- method: "POST",
|
|
|
|
|
- headers: { "Content-Type": "application/json" },
|
|
|
|
|
- body: JSON.stringify({ mode: selectedMode }),
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const createBtn = document.getElementById(
|
|
|
|
|
+ "create-btn",
|
|
|
|
|
+ ) as HTMLButtonElement;
|
|
|
|
|
+ const createdResult = document.getElementById(
|
|
|
|
|
+ "created-result",
|
|
|
|
|
+ ) as HTMLElement;
|
|
|
|
|
+ const gameLink = document.getElementById("game-link") as HTMLInputElement;
|
|
|
|
|
+ const copyBtn = document.getElementById("copy-btn") as HTMLButtonElement;
|
|
|
|
|
+ const qrImg = document.getElementById("qr-img") as HTMLImageElement;
|
|
|
|
|
+ const openGameBtn = document.getElementById(
|
|
|
|
|
+ "open-game-btn",
|
|
|
|
|
+ ) as HTMLAnchorElement;
|
|
|
|
|
+
|
|
|
|
|
+ createBtn.addEventListener("click", async () => {
|
|
|
|
|
+ createBtn.disabled = true;
|
|
|
|
|
+ createBtn.textContent = "Creating...";
|
|
|
|
|
+
|
|
|
|
|
+ const res = await fetch("/api/game", {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
|
|
+ body: JSON.stringify({ mode: selectedMode }),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!res.ok) {
|
|
|
|
|
+ createBtn.disabled = false;
|
|
|
|
|
+ createBtn.textContent = "Create Game Room";
|
|
|
|
|
+ alert("Failed to create game. Are you signed in?");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ const url = `${location.origin}/game/${data.id}`;
|
|
|
|
|
+
|
|
|
|
|
+ gameLink.value = url;
|
|
|
|
|
+ openGameBtn.href = `/game/${data.id}`;
|
|
|
|
|
+ qrImg.src = `/api/qr/${data.id}`;
|
|
|
|
|
+ createdResult.style.display = "block";
|
|
|
|
|
+ createBtn.textContent = "Create Another";
|
|
|
|
|
+ createBtn.disabled = false;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- if (!res.ok) {
|
|
|
|
|
- createBtn.disabled = false;
|
|
|
|
|
- createBtn.textContent = "Create Game Room";
|
|
|
|
|
- alert("Failed to create game. Are you signed in?");
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const data = await res.json();
|
|
|
|
|
- const url = `${location.origin}/game/${data.id}`;
|
|
|
|
|
-
|
|
|
|
|
- gameLink.value = url;
|
|
|
|
|
- openGameBtn.href = `/game/${data.id}`;
|
|
|
|
|
- qrImg.src = `/api/qr/${data.id}`;
|
|
|
|
|
- createdResult.style.display = "block";
|
|
|
|
|
- createBtn.textContent = "Create Another";
|
|
|
|
|
- createBtn.disabled = false;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- copyBtn.addEventListener("click", () => {
|
|
|
|
|
- navigator.clipboard.writeText(gameLink.value);
|
|
|
|
|
- copyBtn.textContent = "Copied!";
|
|
|
|
|
- setTimeout(() => { copyBtn.textContent = "Copy"; }, 2000);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const joinForm = document.getElementById("join-form") as HTMLFormElement;
|
|
|
|
|
- const joinInput = document.getElementById("join-input") as HTMLInputElement;
|
|
|
|
|
- const joinError = document.getElementById("join-error") as HTMLElement;
|
|
|
|
|
-
|
|
|
|
|
- joinForm.addEventListener("submit", (e) => {
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
- joinError.style.display = "none";
|
|
|
|
|
- let val = joinInput.value.trim();
|
|
|
|
|
- if (!val) return;
|
|
|
|
|
- let id = val;
|
|
|
|
|
- try {
|
|
|
|
|
- const url = new URL(val);
|
|
|
|
|
- const parts = url.pathname.split("/").filter(Boolean);
|
|
|
|
|
- if (parts.length >= 2 && parts[0] === "game") id = parts[1];
|
|
|
|
|
- } catch {}
|
|
|
|
|
- if (!id) { joinError.textContent = "Invalid game ID"; joinError.style.display = "block"; return; }
|
|
|
|
|
- location.href = `/game/${id}`;
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ copyBtn.addEventListener("click", () => {
|
|
|
|
|
+ navigator.clipboard.writeText(gameLink.value);
|
|
|
|
|
+ copyBtn.textContent = "Copied!";
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ copyBtn.textContent = "Copy";
|
|
|
|
|
+ }, 2000);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const joinForm = document.getElementById("join-form") as HTMLFormElement;
|
|
|
|
|
+ const joinInput = document.getElementById("join-input") as HTMLInputElement;
|
|
|
|
|
+ const joinError = document.getElementById("join-error") as HTMLElement;
|
|
|
|
|
+
|
|
|
|
|
+ joinForm.addEventListener("submit", (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ joinError.style.display = "none";
|
|
|
|
|
+ let val = joinInput.value.trim();
|
|
|
|
|
+ if (!val) return;
|
|
|
|
|
+ let id = val;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const url = new URL(val);
|
|
|
|
|
+ const parts = url.pathname.split("/").filter(Boolean);
|
|
|
|
|
+ if (parts.length >= 2 && parts[0] === "game") id = parts[1];
|
|
|
|
|
+ } catch {}
|
|
|
|
|
+ if (!id) {
|
|
|
|
|
+ joinError.textContent = "Invalid game ID";
|
|
|
|
|
+ joinError.style.display = "block";
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ location.href = `/game/${id}`;
|
|
|
|
|
+ });
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style>
|
|
<style>
|
|
|
- .play-page { padding-bottom: 48px; }
|
|
|
|
|
-
|
|
|
|
|
- .mode-tabs {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 12px;
|
|
|
|
|
- margin-bottom: 24px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .mode-tab {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- background: var(--card);
|
|
|
|
|
- border: 2px solid var(--border);
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- padding: 16px;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- text-align: left;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
- transition: border-color 0.15s, background 0.15s;
|
|
|
|
|
- font-family: 'DM Mono', monospace;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .mode-tab:hover { border-color: var(--border-light); }
|
|
|
|
|
-
|
|
|
|
|
- .mode-tab.active {
|
|
|
|
|
- border-color: var(--green);
|
|
|
|
|
- background: rgba(129, 182, 76, 0.06);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .mode-icon { font-size: 20px; }
|
|
|
|
|
-
|
|
|
|
|
- .mode-label {
|
|
|
|
|
- font-family: 'Bebas Neue', sans-serif;
|
|
|
|
|
- font-size: 1.3rem;
|
|
|
|
|
- letter-spacing: 0.05em;
|
|
|
|
|
- color: var(--text);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .mode-desc {
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
- color: var(--text-muted);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .play-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
|
|
|
|
|
- .card-title { font-family: 'Bebas Neue', sans-serif; font-size: 1.5rem; letter-spacing: 0.05em; margin-bottom: 8px; }
|
|
|
|
|
- .card-desc { color: var(--text-muted); font-size: 13px; margin-bottom: 0; line-height: 1.6; }
|
|
|
|
|
- .created-link-box { margin-bottom: 20px; }
|
|
|
|
|
- .link-row { display: flex; gap: 8px; }
|
|
|
|
|
- .link-row input { flex: 1; font-size: 12px; }
|
|
|
|
|
- .qr-section { margin-bottom: 8px; }
|
|
|
|
|
- .qr-box { background: white; border-radius: 4px; display: inline-block; padding: 12px; }
|
|
|
|
|
- .qr-box img { display: block; width: 160px; height: 160px; }
|
|
|
|
|
- .error-msg { color: var(--red); font-size: 12px; margin-bottom: 12px; padding: 8px 12px; background: rgba(224,110,110,0.1); border-radius: 3px; border: 1px solid rgba(224,110,110,0.2); }
|
|
|
|
|
- @media (max-width: 640px) { .play-grid { grid-template-columns: 1fr; } .mode-tabs { flex-direction: column; } }
|
|
|
|
|
|
|
+ .play-page {
|
|
|
|
|
+ padding-bottom: 48px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mode-tabs {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mode-tab {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ background: var(--card);
|
|
|
|
|
+ border: 2px solid var(--border);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ text-align: left;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ transition:
|
|
|
|
|
+ border-color 0.15s,
|
|
|
|
|
+ background 0.15s;
|
|
|
|
|
+ font-family: "DM Mono", monospace;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mode-tab:hover {
|
|
|
|
|
+ border-color: var(--border-light);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mode-tab.active {
|
|
|
|
|
+ border-color: var(--green);
|
|
|
|
|
+ background: rgba(129, 182, 76, 0.06);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mode-icon {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mode-label {
|
|
|
|
|
+ font-family: "Bebas Neue", sans-serif;
|
|
|
|
|
+ font-size: 1.3rem;
|
|
|
|
|
+ letter-spacing: 0.05em;
|
|
|
|
|
+ color: var(--text);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .mode-desc {
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: var(--text-muted);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .play-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
|
|
+ gap: 24px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .card-title {
|
|
|
|
|
+ font-family: "Bebas Neue", sans-serif;
|
|
|
|
|
+ font-size: 1.5rem;
|
|
|
|
|
+ letter-spacing: 0.05em;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .card-desc {
|
|
|
|
|
+ color: var(--text-muted);
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ }
|
|
|
|
|
+ .created-link-box {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .link-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .link-row input {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .qr-section {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .qr-box {
|
|
|
|
|
+ background: white;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .qr-box img {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: 160px;
|
|
|
|
|
+ height: 160px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .error-msg {
|
|
|
|
|
+ color: var(--red);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ background: rgba(224, 110, 110, 0.1);
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ border: 1px solid rgba(224, 110, 110, 0.2);
|
|
|
|
|
+ }
|
|
|
|
|
+ @media (max-width: 640px) {
|
|
|
|
|
+ .play-grid {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+ .mode-tabs {
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
</style>
|
|
</style>
|