Initial Commit
This commit is contained in:
33
public/404.html
Normal file
33
public/404.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Page Not Found</title>
|
||||
|
||||
<style media="screen">
|
||||
body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
|
||||
#message { background: white; max-width: 360px; margin: 100px auto 16px; padding: 32px 24px 16px; border-radius: 3px; }
|
||||
#message h3 { color: #888; font-weight: normal; font-size: 16px; margin: 16px 0 12px; }
|
||||
#message h2 { color: #ffa100; font-weight: bold; font-size: 16px; margin: 0 0 8px; }
|
||||
#message h1 { font-size: 22px; font-weight: 300; color: rgba(0,0,0,0.6); margin: 0 0 16px;}
|
||||
#message p { line-height: 140%; margin: 16px 0 24px; font-size: 14px; }
|
||||
#message a { display: block; text-align: center; background: #039be5; text-transform: uppercase; text-decoration: none; color: white; padding: 16px; border-radius: 4px; }
|
||||
#message, #message a { box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); }
|
||||
#load { color: rgba(0,0,0,0.4); text-align: center; font-size: 13px; }
|
||||
@media (max-width: 600px) {
|
||||
body, #message { margin-top: 0; background: white; box-shadow: none; }
|
||||
body { border-top: 16px solid #ffa100; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="message">
|
||||
<h2>404</h2>
|
||||
<h1>Page Not Found</h1>
|
||||
<p>The specified file was not found on this website. Please check the URL for mistakes and try again.</p>
|
||||
<h3>Why am I seeing this?</h3>
|
||||
<p>This page was generated by the Firebase Command-Line Interface. To modify it, edit the <code>404.html</code> file in your project's configured <code>public</code> directory.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
566
public/index.html
Normal file
566
public/index.html
Normal file
@ -0,0 +1,566 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Imposter Game v4</title>
|
||||
<style>
|
||||
/* --- Dark Theme & Visual Polish --- */
|
||||
:root {
|
||||
--bg-dark-primary: #1a1a1a; --bg-dark-secondary: #2c2c2c; --bg-dark-tertiary: #3f3f3f;
|
||||
--text-light-primary: #e0e0e0; --text-light-secondary: #b0b0b0; --border-dark: #555555;
|
||||
--accent-blue: #3b82f6; --accent-blue-hover: #2563eb; --accent-green: #22c55e;
|
||||
--accent-green-hover: #16a34a; --accent-red: #ef4444; --accent-yellow: #facc15;
|
||||
--accent-yellow-text: #422006; --accent-imposter-reveal-bg: #450a0a;
|
||||
--accent-imposter-reveal-text: #fecaca; --accent-imposter-reveal-border: #7f1d1d;
|
||||
--disabled-bg: #4b5563; --disabled-text: #9ca3af;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
display: flex; justify-content: center; align-items: flex-start; min-height: 100vh;
|
||||
background-color: var(--bg-dark-primary); color: var(--text-light-primary); margin: 0;
|
||||
padding: 20px; box-sizing: border-box; line-height: 1.6;
|
||||
}
|
||||
.container {
|
||||
background-color: var(--bg-dark-secondary); padding: 30px; border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); text-align: center; max-width: 650px;
|
||||
width: 100%; margin: 20px auto; border: 1px solid var(--border-dark);
|
||||
}
|
||||
.view { display: none; } .view.active { display: block; }
|
||||
h1, h2, h3 { color: var(--text-light-primary); margin-bottom: 20px; margin-top: 15px;}
|
||||
h4 { color: var(--text-light-secondary); margin-bottom: 12px; margin-top: 20px; text-transform: uppercase; font-size: 0.9em; letter-spacing: 0.5px;}
|
||||
input[type="text"], input[type="number"] {
|
||||
padding: 12px; margin: 12px 0; border: 1px solid var(--border-dark); border-radius: 6px;
|
||||
width: calc(100% - 26px); box-sizing: border-box; font-size: 1em;
|
||||
background-color: var(--bg-dark-tertiary); color: var(--text-light-primary);
|
||||
}
|
||||
input[type="text"]:focus, input[type="number"]:focus { outline: none; border-color: var(--accent-blue); box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3); }
|
||||
button {
|
||||
padding: 12px 25px; margin: 10px 5px; border: none; border-radius: 6px;
|
||||
background-color: var(--accent-blue); color: white; font-size: 16px; font-weight: 500;
|
||||
cursor: pointer; transition: background-color 0.3s ease, box-shadow 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
button:hover { background-color: var(--accent-blue-hover); box-shadow: 0 4px 8px rgba(0,0,0,0.3); }
|
||||
button:disabled { background-color: var(--disabled-bg); color: var(--disabled-text); cursor: not-allowed; box-shadow: none; }
|
||||
#player-list, #answers-display, #voting-options, #results-display, #score-list { list-style: none; padding: 0; margin-top: 20px; text-align: left; }
|
||||
#player-list li, #answers-display li, #voting-options button, #results-display div, #score-list li {
|
||||
background-color: var(--bg-dark-tertiary); padding: 10px 15px; margin-bottom: 10px; border-radius: 6px;
|
||||
display: flex; justify-content: space-between; align-items: center; border: 1px solid var(--border-dark); color: var(--text-light-primary);
|
||||
}
|
||||
.player-score { font-weight: bold; margin-left: 15px; background-color: var(--bg-dark-secondary); color: var(--text-light-secondary); padding: 3px 8px; border-radius: 4px;}
|
||||
#voting-options button { width: 100%; justify-content: center; background-color: var(--accent-green); color: white; }
|
||||
#voting-options button:hover { background-color: var(--accent-green-hover); }
|
||||
#voting-options button:disabled { background-color: var(--disabled-bg); color: var(--disabled-text); }
|
||||
#question-area, #revealed-question-area {
|
||||
background-color: var(--bg-dark-tertiary); color: var(--text-light-primary); padding: 20px;
|
||||
border: 1px solid var(--border-dark); border-left: 4px solid var(--accent-blue); border-radius: 8px;
|
||||
margin: 25px 0; font-size: 1.15em; min-height: 60px; text-align: left; box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
#revealed-question-area { border-left-color: var(--accent-yellow); }
|
||||
.imposter-question-reveal {
|
||||
background-color: var(--accent-imposter-reveal-bg); color: var(--accent-imposter-reveal-text);
|
||||
border-color: var(--accent-imposter-reveal-border); padding: 10px 15px; margin-top: 15px;
|
||||
border-radius: 6px; border: 1px solid var(--accent-imposter-reveal-border); font-size: 0.95em; text-align: left;
|
||||
}
|
||||
#status-message, #lobby-status, #waiting-for-answers, #vote-status, #loading-status, #loading-status-game {
|
||||
margin-top: 20px; font-weight: 500; color: var(--text-light-secondary); padding: 10px 15px;
|
||||
background-color: var(--bg-dark-tertiary); border-radius: 6px; display: inline-block;
|
||||
border: 1px solid var(--border-dark); min-height: 2em;
|
||||
}
|
||||
#loading-status, #loading-status-game {
|
||||
color: var(--accent-blue); border-color: var(--accent-blue); font-weight: bold;
|
||||
}
|
||||
#login-error { color: var(--accent-red); font-weight: bold;}
|
||||
.answer-item { background-color: #1e3a8a !important; color: #bfdbfe !important; border-color: #2563eb !important; }
|
||||
.result-item { margin-bottom: 12px !important; padding: 12px !important; background-color: var(--bg-dark-tertiary) !important; border: 1px solid var(--border-dark) !important; }
|
||||
.highlight-imposter { color: var(--accent-red); font-weight: bold; }
|
||||
.highlight-guess { border: 2px solid var(--accent-blue) !important; }
|
||||
.winner-message { font-size: 1.6em; color: var(--accent-green); font-weight: bold; margin: 25px 0; }
|
||||
#game-over-status { margin-top: 15px; color: var(--text-light-secondary); }
|
||||
</style>
|
||||
|
||||
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script>
|
||||
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-database.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Imposter Game</h1>
|
||||
|
||||
<div id="login-view" class="view active">
|
||||
<h2>Join Game</h2>
|
||||
<input type="text" id="player-name" placeholder="Enter your name">
|
||||
<input type="text" id="game-id-input" placeholder="Enter Game ID (leave blank to create)">
|
||||
<button id="join-game-btn">Join / Create Game</button>
|
||||
<p id="login-error"></p>
|
||||
</div>
|
||||
<div id="lobby-view" class="view">
|
||||
<h2>Lobby</h2>
|
||||
<p>Game ID: <strong id="display-game-id"></strong></p>
|
||||
<h3>Players</h3>
|
||||
<ul id="player-list"></ul>
|
||||
<div id="loading-status" style="display: none;">Starting Round...</div>
|
||||
<div id="lobby-status">Initializing...</div>
|
||||
<button id="start-game-btn" disabled>Start Game</button>
|
||||
</div>
|
||||
<div id="game-view" class="view">
|
||||
<div id="loading-status-game" style="display: none; margin-bottom: 20px;">Starting Round...</div>
|
||||
|
||||
<h2 id="round-title">Round X</h2>
|
||||
<h3 id="score-title" style="display: none;">Scores</h3>
|
||||
<ul id="score-list" style="display: none;"></ul>
|
||||
<div id="question-area" style="display: none;"></div>
|
||||
<div id="answer-section" style="display: none;">
|
||||
<h4>Your Answer</h4>
|
||||
<input type="text" id="answer-input" placeholder="Type your answer here...">
|
||||
<button id="submit-answer-btn">Submit Answer</button>
|
||||
</div>
|
||||
<div id="waiting-for-answers" style="display: none;"></div>
|
||||
<div id="voting-section" style="display: none;">
|
||||
<div id="revealed-question-area"></div>
|
||||
<h4>Answers Submitted</h4>
|
||||
<ul id="answers-display"></ul>
|
||||
<h4>Who is the Imposter?</h4>
|
||||
<div id="voting-options"></div>
|
||||
<div id="vote-status"></div>
|
||||
</div>
|
||||
<div id="results-section" style="display: none;">
|
||||
<h2>Round Results</h2>
|
||||
<div id="results-display"></div>
|
||||
<button id="next-round-btn" style="display: none;">Start Next Round</button>
|
||||
</div>
|
||||
<div id="game-over-section" style="display: none;">
|
||||
<h2>Game Over!</h2>
|
||||
<div class="winner-message" id="winner-display"></div>
|
||||
<button id="play-again-btn" style="display: none;">Play Again (New Game)</button>
|
||||
<div id="game-over-status" style="display: none;">Waiting for host to start a new game...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="status-message" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- YOUR FIREBASE CONFIG ---
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDMI_J2d0LSysErw6qZx1WdPsX1Pj18Dsk",
|
||||
authDomain: "imposter-e327f.firebaseapp.com",
|
||||
databaseURL: "https://imposter-e327f-default-rtdb.firebaseio.com",
|
||||
projectId: "imposter-e327f",
|
||||
storageBucket: "imposter-e327f.appspot.com",
|
||||
messagingSenderId: "669524997189",
|
||||
appId: "1:669524997189:web:312b2333ab554a2c1f5f2c"
|
||||
};
|
||||
// ---------------------------
|
||||
// --- IMPORTANT: Ensure your Firebase Realtime Database rules are secure! ---
|
||||
|
||||
// --- Game Questions (used for main question & fallback) ---
|
||||
const questions = [ /* ... same large list as previous response ... */
|
||||
{type: "song", q: "Name a song you'd like to be played at your wedding"},
|
||||
{type: "song", q: "What is your pump up song?"},
|
||||
{type: "song", q: "What song makes you sad?"},
|
||||
{type: "song", q: "Name a song that is currently popular."},
|
||||
{ type: "song", q: "Name a classic song everyone seems to know." },
|
||||
{type: "song", q: "What is your go-to Karaoke song?"},
|
||||
{type: "one-item", q: "You can only have one thing during a zombie apocolypse, what would it be?"},
|
||||
{type: "one-item", q: "What one thing would you like with you on a deserted island?"},
|
||||
{type: "one-item", q: "What one thing would you like with you in a desert?"},
|
||||
{ type: "one-item", q: "If your house was on fire, what one non-living thing would you save (assuming people/pets are safe)?" },
|
||||
{ type: "one-item", q: "What's one tool you find most useful?" },
|
||||
{type: "country", q: "What country you would most want to visit?"},
|
||||
{type: "country", q: "What is the most dangerous country in the world?"},
|
||||
{type: "country", q: "Name a country you would hate to visit"},
|
||||
{ type: "country", q: "Name a country known for its cuisine." },
|
||||
{ type: "country", q: "What country has the best food in the world?" },
|
||||
{ type: "country", q: "What country has the most attractive people?" },
|
||||
{type: "actor", q: "Name a famous Oscar award winning actor or actress"},
|
||||
{ type: "actor", q: "Name an actor/actress known for comedy roles." },
|
||||
{type: "actor", q: "Who is your favorite actor or actress?"},
|
||||
{type: "actor", q: "Name a actor or actress in a recent movie"},
|
||||
{type: "food", q: "Name a popular food item in the USA"},
|
||||
{ type: "food", q: "Name a common fruit." },
|
||||
{ type: "food", q: "Name a healthy food item." },
|
||||
{ type: "food", q: "Name an unhealthy food item." },
|
||||
{type: "food", q: "Name a popular food item where you live"},
|
||||
{ type: "food", q: "Name a type of dessert." },
|
||||
{type: "food", q: "What do you typically eat for breakfast"},
|
||||
{type: "food", q: "What is your favorite food"},
|
||||
{type: "food", q: "What is a food everyone loves that you hate?"},
|
||||
{type: "food", q: "What is a food everyone hates that you love?"},
|
||||
{type: "video-game", q: "What is your favorite video game?"},
|
||||
{type: "video-game", q: "What is your least favorite video game?"},
|
||||
{ type: "video-game", q: "Name a popular online multiplayer game." },
|
||||
{type: "video-game", q: "What is an underrated video game?"},
|
||||
{type: "video-game", q: "What is an overrated video game?"},
|
||||
{type: "video-game", q: "What is the last game you played?"},
|
||||
{ type: "animal", q: "Name your favorite animal." },
|
||||
{ type: "animal", q: "Name an animal often found in a zoo." },
|
||||
{ type: "animal", q: "Name an animal native to your region" },
|
||||
{ type: "animal", q: "Name a dangerous animal." },
|
||||
{ type: "animal", q: "Name a common farm animal." },
|
||||
{ type: "animal", q: "What animal would you want by your side during the apocalypse?" },
|
||||
{ type: "animal", q: "What animal would you want to turn into?" },
|
||||
{ type: "animal", q: "What animal is the tastiest?" },
|
||||
{ type: "animal", q: "Which animal would you want to exterminate?" },
|
||||
{ type: "animal", q: "What is the most ugliest animal?" },
|
||||
{ type: "animal", q: "What is the craziest animal you would eat?" },
|
||||
{ type: "animal", q: "What is a wild animal that you would want to have as a pet?" },
|
||||
{ type: "movie", q: "Name your favorite movie." },
|
||||
{ type: "movie", q: "Name a movie that made you laugh out loud." },
|
||||
{ type: "movie", q: "Name a movie that scared you." },
|
||||
{ type: "movie", q: "Name the scariest movie." },
|
||||
{ type: "movie", q: "Name a science fiction movie." },
|
||||
{ type: "movie", q: "Name a movie you watched recently." },
|
||||
{ type: "movie", q: "What movie world would you most want to live in?" },
|
||||
{ type: "movie", q: "What do you think is the most overrated movie?" },
|
||||
{ type: "movie", q: "What do you think is the most underrated movie?" },
|
||||
{ type: "tv-show", q: "What tv-show world would you most want to live in?" },
|
||||
{ type: "tv-show", q: "Name a TV show you are currently watching." },
|
||||
{ type: "tv-show", q: "What is your favorite TV show of all time?" },
|
||||
{ type: "tv-show", q: "Name a popular reality TV show." },
|
||||
{ type: "tv-show", q: "Name a classic sitcom." },
|
||||
{ type: "tv-show", q: "Name a TV show you watched as a child." },
|
||||
{ type: "tv-show", q: "What TV show would you show your kids?" },
|
||||
{ type: "tv-show", q: "Name a TV show available on Netflix (or another major streamer)." },
|
||||
{ type: "clothing-item", q: "Name an item of clothing typically worn in warm weather." },
|
||||
{ type: "clothing-item", q: "Name something you wear on your feet." },
|
||||
{ type: "clothing-item", q: "Name an item often made of denim." },
|
||||
{ type: "clothing-item", q: "Name something you might wear to a formal event." },
|
||||
{ type: "clothing-item", q: "Name a common piece of clothing." },
|
||||
{ type: "1-10", q: "On a scale of 1 to 10, how much do you enjoy very spicy food?" },
|
||||
{ type: "1-10", q: "Rate your current mood on a scale of 1 (terrible) to 10 (fantastic)." },
|
||||
{ type: "1-10", q: "On a scale of 1 to 10, how important is being on time to you?" },
|
||||
{ type: "1-10", q: "How would you rate your sense of direction on a scale of 1 (easily lost) to 10 (human GPS)?" },
|
||||
{ type: "1-10", q: "On a scale of 1 to 10, how adventurous are you with trying new things?" },
|
||||
{ type: "1-10", q: "Roughly how many times do you eat out or order takeaway in a typical week?" },
|
||||
{ type: "1-10", q: "How many different messaging apps (WhatsApp, Messenger, etc.) do you use regularly?" },
|
||||
{ type: "1-10", q: "Approximately how many hours did you sleep last night? (Round to the nearest whole number" },
|
||||
{ type: "number", q: "What is a good age to have your first kid?" },
|
||||
{ type: "number", q: "What is a good age to get married?" },
|
||||
{ type: "number", q: "Average age to have your first kiss?" },
|
||||
{ type: "number", q: "At what age did you have your first crush?" },
|
||||
{ type: "number", q: "At what age (roughly) did you get your first mobile phone?" },
|
||||
{ type: "number", q: "If you could be one age for the rest of your life, what would it be?" },
|
||||
{ type: "number", q: "If you won the lottery, what % would you give your parents? (0-100 without the % sign)" },
|
||||
{ type: "number", q: "How much do you typically tip at a restaurant? (no % or $ sign)" },
|
||||
{ type: "number", q: "Most number of alcoholic drinks you've had in one night?" },
|
||||
{ type: "famous-person", q: "Name someone famously associated with drugs." },
|
||||
{ type: "famous-person", q: "Name a celebrity frequently discussed in entertainment news or gossip." },
|
||||
{ type: "famous-person", q: "Name someone who has won a major international award in *any* field." },
|
||||
{ type: "famous-person", q: "Name a famous person known for a distinct look, style, or physical trait." },
|
||||
{ type: "famous-person", q: "Name someone famous who is no longer living." },
|
||||
{ type: "famous-person", q: "Name a famous person you personally admire (for any reason)." },
|
||||
{ type: "famous-person", q: "Name someone whose face is recognized by millions globally today." },
|
||||
{ type: "famous-person", q: "Name a famous person often considered controversial." },
|
||||
{ type: "famous-person", q: "Name a famous person who has been charged with a crime." },
|
||||
{ type: "famous-person", q: "Name someone infamous." },
|
||||
{ type: "famous-person", q: "What celebrity do you want to be stuck on a deserted island with?" },
|
||||
{ type: "famous-person", q: "Who is a famouse person that inspires you?" },
|
||||
{ type: "famous-person", q: "Who is your least favorite celebrity?" },
|
||||
{ type: "fictional-person", q: "What celebrity would you most want to switch lives with?" },
|
||||
{ type: "fictional-character", q: "What fictional character do you wish was real?" },
|
||||
{ type: "fictional-character", q: "What fictional character would you switch lives with?" },
|
||||
{ type: "fictional-character", q: "What fictional character has been thru the most?" },
|
||||
{ type: "fictional-character", q: "Who is the best fictional villain?" },
|
||||
{ type: "fictional-character", q: "Who is the best fictional hero?" },
|
||||
{ type: "fictional-character", q: "Who is your favorite superhero?" },
|
||||
{ type: "fictional-character", q: "Who is your favorite villain?" },
|
||||
{ type: "fictional-character", q: "Who is your favorite fictional character?" },
|
||||
{ type: "fictional-character", q: "Which fictional character would make the best roomate?" }
|
||||
];
|
||||
// --------------------
|
||||
|
||||
// Initialize Firebase
|
||||
if (typeof firebase !== 'undefined') {
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
const database = firebase.database();
|
||||
|
||||
// --- Constants ---
|
||||
const MIN_PLAYERS = 3;
|
||||
const WINNING_SCORE = 10;
|
||||
|
||||
// --- Global State ---
|
||||
let currentGameId = null;
|
||||
let currentPlayerId = null;
|
||||
let currentPlayerName = '';
|
||||
let gameDataSubscription = null;
|
||||
let isHost = false;
|
||||
let usedQuestionIndices = new Set(); // Track used questions
|
||||
|
||||
// --- UI Elements ---
|
||||
const views = document.querySelectorAll('.view');
|
||||
const loginView = document.getElementById('login-view');
|
||||
const lobbyView = document.getElementById('lobby-view');
|
||||
const gameView = document.getElementById('game-view');
|
||||
const playerNameInput = document.getElementById('player-name');
|
||||
const gameIdInput = document.getElementById('game-id-input');
|
||||
const joinGameBtn = document.getElementById('join-game-btn');
|
||||
const loginError = document.getElementById('login-error');
|
||||
const displayGameId = document.getElementById('display-game-id');
|
||||
const playerList = document.getElementById('player-list');
|
||||
const lobbyStatus = document.getElementById('lobby-status');
|
||||
const loadingStatusLobby = document.getElementById('loading-status');
|
||||
const startGameBtn = document.getElementById('start-game-btn');
|
||||
const roundTitle = document.getElementById('round-title');
|
||||
const scoreTitle = document.getElementById('score-title');
|
||||
const scoreList = document.getElementById('score-list');
|
||||
const loadingStatusGame = document.getElementById('loading-status-game');
|
||||
const questionArea = document.getElementById('question-area');
|
||||
const answerSection = document.getElementById('answer-section');
|
||||
const answerInput = document.getElementById('answer-input');
|
||||
const submitAnswerBtn = document.getElementById('submit-answer-btn');
|
||||
const waitingForAnswers = document.getElementById('waiting-for-answers');
|
||||
const votingSection = document.getElementById('voting-section');
|
||||
const revealedQuestionArea = document.getElementById('revealed-question-area');
|
||||
const answersDisplay = document.getElementById('answers-display');
|
||||
const votingOptions = document.getElementById('voting-options');
|
||||
const voteStatus = document.getElementById('vote-status');
|
||||
const resultsSection = document.getElementById('results-section');
|
||||
const resultsDisplay = document.getElementById('results-display');
|
||||
const nextRoundBtn = document.getElementById('next-round-btn');
|
||||
const gameOverSection = document.getElementById('game-over-section');
|
||||
const winnerDisplay = document.getElementById('winner-display');
|
||||
const playAgainBtn = document.getElementById('play-again-btn');
|
||||
const gameOverStatus = document.getElementById('game-over-status');
|
||||
const statusMessage = document.getElementById('status-message');
|
||||
|
||||
|
||||
// --- Utility Functions ---
|
||||
function setActiveView(viewId) {
|
||||
views.forEach(view => view.classList.toggle('active', view.id === viewId));
|
||||
loginError.textContent = '';
|
||||
statusMessage.style.display = 'none';
|
||||
if (viewId !== 'game-view') resetGameViewState(); // reset happens when LEAVING game view
|
||||
}
|
||||
|
||||
function generateGameId(length = 5) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRandomElement(arr) {
|
||||
if (!arr || arr.length === 0) return null;
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
currentPlayerId = `player_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
||||
|
||||
// --- Event Listeners ---
|
||||
// Ensure elements exist before adding listeners ('defer' in HTML helps)
|
||||
if (joinGameBtn) joinGameBtn.addEventListener('click', handleJoinOrCreateGame);
|
||||
if (startGameBtn) startGameBtn.addEventListener('click', () => handleStartGameClick(false));
|
||||
if (submitAnswerBtn) submitAnswerBtn.addEventListener('click', handleSubmitAnswer);
|
||||
if (nextRoundBtn) nextRoundBtn.addEventListener('click', () => handleStartGameClick(false));
|
||||
if (playAgainBtn) playAgainBtn.addEventListener('click', () => handleStartGameClick(true));
|
||||
|
||||
|
||||
// --- Core Game Logic Functions ---
|
||||
|
||||
async function handleJoinOrCreateGame() {
|
||||
currentPlayerName = playerNameInput.value.trim(); const inputGameId = gameIdInput.value.trim().toUpperCase(); if (!currentPlayerName) { loginError.textContent = 'Please enter your name.'; return; } loginError.textContent = ''; joinGameBtn.disabled = true; statusMessage.textContent = 'Connecting...'; statusMessage.style.display = 'inline-block'; try { if (inputGameId) { const gameRef = database.ref(`games/${inputGameId}`); const snapshot = await gameRef.once('value'); if (snapshot.exists()) { const gameData = snapshot.val(); if (gameData.state !== 'lobby' && gameData.state !== 'game_over') { throw new Error('Game has already started or is in progress.'); } currentGameId = inputGameId; isHost = false; await addPlayerToGame(currentGameId, currentPlayerId, currentPlayerName); subscribeToGameUpdates(currentGameId); } else { throw new Error('Game ID not found.'); } } else { currentGameId = generateGameId(); isHost = true; await createNewGame(currentGameId, currentPlayerId, currentPlayerName); subscribeToGameUpdates(currentGameId); } statusMessage.style.display = 'none'; } catch (error) { loginError.textContent = error.message; statusMessage.style.display = 'none'; } finally { joinGameBtn.disabled = false; }
|
||||
}
|
||||
async function createNewGame(gameId, playerId, playerName) {
|
||||
const gameRef = database.ref(`games/${gameId}`); const initialPlayerData = { [playerId]: { name: playerName, isHost: true, score: 0 } }; await gameRef.set({ state: 'lobby', players: initialPlayerData, hostId: playerId, round: 0 });
|
||||
}
|
||||
async function addPlayerToGame(gameId, playerId, playerName) {
|
||||
const playerRef = database.ref(`games/${gameId}/players/${playerId}`); await playerRef.set({ name: playerName, isHost: false, score: 0 }); const hostRef = database.ref(`games/${gameId}/hostId`); const hostSnapshot = await hostRef.once('value'); if (hostSnapshot.val() === playerId) { await playerRef.child('isHost').set(true); }
|
||||
}
|
||||
function subscribeToGameUpdates(gameId) {
|
||||
if (gameDataSubscription) gameDataSubscription.off(); const gameRef = database.ref(`games/${gameId}`); gameDataSubscription = gameRef.on('value', (snapshot) => { if (!snapshot.exists()) { alert('Game not found or has been deleted.'); resetToLogin(); return; } const gameData = snapshot.val(); updateUI(gameData); }, (error) => { console.error("Firebase read failed: ", error); statusMessage.textContent = `Error connecting: ${error.message}`; statusMessage.style.display = 'inline-block'; resetToLogin(); });
|
||||
}
|
||||
|
||||
// --- Start Game Click Handler (remains async for state setting) ---
|
||||
async function handleStartGameClick(forceResetScores = false) {
|
||||
if (!isHost || !currentGameId) return;
|
||||
startGameBtn.disabled = true; nextRoundBtn.disabled = true; playAgainBtn.disabled = true;
|
||||
lobbyStatus.textContent = "Starting round...";
|
||||
try {
|
||||
await database.ref(`games/${currentGameId}/state`).set('starting_round');
|
||||
startGameLogic(currentGameId, forceResetScores); // Call synchronous logic
|
||||
} catch (error) {
|
||||
console.error("Error initiating start game:", error); statusMessage.textContent = `Error starting: ${error.message}`; statusMessage.style.display = 'inline-block'; try { await database.ref(`games/${currentGameId}`).update({ state: 'lobby', lobbyMessage: `Error starting round: ${error.message}` }); } catch (revertError) { console.error("Failed to revert state after error:", revertError); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Core Start Game Logic (Synchronous, handles uniqueness & fallback type check) ---
|
||||
function startGameLogic(gameId, forceResetScores = false) { // Renamed, synchronous
|
||||
const gameRef = database.ref(`games/${gameId}`);
|
||||
let mainQuestionObj = null; let mainQuestionIndex = -1; let imposterQuestionText = null;
|
||||
let mainQuestionText = null; let suitableFallbackIndices = [];
|
||||
|
||||
if (forceResetScores) { console.log("Resetting used questions."); usedQuestionIndices.clear(); }
|
||||
|
||||
let availableIndices = questions.map((_, index) => index).filter(index => !usedQuestionIndices.has(index));
|
||||
if (availableIndices.length === 0) { console.log("All questions used! Resetting pool."); usedQuestionIndices.clear(); availableIndices = questions.map((_, index) => index); if (availableIndices.length === 0) { throw new Error("No questions available!"); } }
|
||||
|
||||
let foundSuitableQuestion = false; let attempts = 0; const maxAttempts = availableIndices.length + 5;
|
||||
|
||||
while (!foundSuitableQuestion && attempts < maxAttempts && availableIndices.length > 0) {
|
||||
attempts++; const randomPoolIndex = Math.floor(Math.random() * availableIndices.length);
|
||||
const potentialIndex = availableIndices[randomPoolIndex]; const potentialQuestion = questions[potentialIndex];
|
||||
suitableFallbackIndices = availableIndices.filter(index => index !== potentialIndex && questions[index].type === potentialQuestion.type);
|
||||
if (suitableFallbackIndices.length > 0) { mainQuestionIndex = potentialIndex; mainQuestionObj = potentialQuestion; mainQuestionText = mainQuestionObj.q; foundSuitableQuestion = true; }
|
||||
else { console.log(`Skipping Q Index ${potentialIndex} (Type: ${potentialQuestion.type}) - no unused alternatives.`); availableIndices.splice(randomPoolIndex, 1); }
|
||||
}
|
||||
|
||||
if (!foundSuitableQuestion) { throw new Error("Failed to find a question with available fallback options."); }
|
||||
usedQuestionIndices.add(mainQuestionIndex);
|
||||
|
||||
console.log("Selecting imposter question from fallback list.");
|
||||
const fallbackIndex = getRandomElement(suitableFallbackIndices);
|
||||
if (fallbackIndex !== null) { const fallbackQuestionObj = questions[fallbackIndex]; imposterQuestionText = fallbackQuestionObj.q; usedQuestionIndices.add(fallbackIndex); }
|
||||
else { console.warn("Fallback index not found! Using main question."); imposterQuestionText = mainQuestionText; }
|
||||
|
||||
gameRef.transaction(gameData => {
|
||||
const finalPlayerIds = Object.keys(gameData?.players || {});
|
||||
if (!gameData || finalPlayerIds.length < MIN_PLAYERS) { console.log("Player count dropped. Reverting to lobby."); gameData = gameData || {}; gameData.state = 'lobby'; gameData.lobbyMessage = `Player left, need ${MIN_PLAYERS}.`; return gameData; }
|
||||
const imposterId = getRandomElement(finalPlayerIds); gameData.state = 'asking'; gameData.mainQuestionText = mainQuestionText; gameData.imposterQuestionText = imposterQuestionText; gameData.questionType = mainQuestionObj.type; gameData.imposterId = imposterId; gameData.round = forceResetScores ? 1 : (gameData.round || 0) + 1; gameData.winnerName = null; gameData.lobbyMessage = null; gameData.players = gameData.players || {};
|
||||
finalPlayerIds.forEach(pid => { gameData.players[pid] = { ...(gameData.players[pid] || { name: "Unknown", score: 0 }), answer: null, vote: null, score: forceResetScores ? 0 : (gameData.players[pid]?.score || 0) }; });
|
||||
return gameData;
|
||||
}).catch(error => { console.error("Firebase transaction failed:", error); statusMessage.textContent = `Error starting round transaction: ${error.message}`; statusMessage.style.display = 'inline-block'; });
|
||||
}
|
||||
|
||||
|
||||
// --- Other Core Functions ---
|
||||
async function handleSubmitAnswer() {
|
||||
const answer = answerInput.value.trim(); if (!answer || !currentGameId || !currentPlayerId) return; submitAnswerBtn.disabled = true; answerInput.disabled = true; try { await database.ref(`games/${currentGameId}/players/${currentPlayerId}/answer`).set(answer); checkAllAnswered(currentGameId); } catch(error) { console.error("Failed to submit answer:", error); statusMessage.textContent = "Error submitting answer."; statusMessage.style.display = 'inline-block'; submitAnswerBtn.disabled = false; answerInput.disabled = false; }
|
||||
}
|
||||
async function checkAllAnswered(gameId) {
|
||||
const gameRef = database.ref(`games/${gameId}`); try { await gameRef.transaction(gameData => { if (gameData && gameData.state === 'asking') { const p = gameData.players || {}; let allA = true; for (const pid in p) { if (!p[pid].answer) { allA = false; break; } } if (allA) { gameData.state = 'voting'; } } return gameData; }); } catch (error) { console.error("Error checking answers:", error); }
|
||||
}
|
||||
async function submitVote(votedPlayerId) {
|
||||
if (!currentGameId || !currentPlayerId) return; votingOptions.querySelectorAll('button').forEach(btn => btn.disabled = true); const gameRef = database.ref(`games/${currentGameId}`); try { await gameRef.transaction(gameData => { if (!gameData || gameData.state !== 'voting' || !gameData.players || !gameData.players[currentPlayerId]) { return; } gameData.players[currentPlayerId].vote = votedPlayerId; let allV = true; for (const pid in gameData.players) { if (!gameData.players[pid].vote) { allV = false; break; } } if (allV) { gameData.state = 'results'; const { players, imposterId } = gameData; const vr = {}; Object.keys(players).forEach(pid => { vr[pid] = 0; }); Object.keys(players).forEach(vPid => { const vt = players[vPid].vote; if (vt && vr[vt] !== undefined) { vr[vt]++; } }); let mV = 0; let aPI = []; Object.keys(vr).forEach(pid => { if (vr[pid] > mV) { mV = vr[pid]; aPI = [pid]; } else if (vr[pid] === mV && mV > 0) { aPI.push(pid); } }); const iWA = aPI.includes(imposterId); const iCG = iWA && aPI.length === 1; let gHW = false; let wN = []; Object.keys(players).forEach(pid => { if (iCG) { if (pid !== imposterId) players[pid].score = (players[pid].score || 0) + 1; } else { if (pid === imposterId) players[pid].score = (players[pid].score || 0) + 1; } if (players[pid].score >= WINNING_SCORE) { gHW = true; wN.push(players[pid].name); } }); if (gHW) { gameData.state = 'game_over'; gameData.winnerName = wN.join(' & '); } } return gameData; }); } catch (error) { console.error("Error submitting vote:", error); statusMessage.textContent = "Error submitting vote."; statusMessage.style.display = 'inline-block'; }
|
||||
}
|
||||
|
||||
// --- UI Update Function ---
|
||||
function updateUI(gameData) {
|
||||
if (!gameData || !currentPlayerId) return;
|
||||
const { state, players, hostId, round, mainQuestionText, imposterQuestionText, imposterId, winnerName, lobbyMessage } = gameData;
|
||||
const playerIds = players ? Object.keys(players) : []; const numPlayers = playerIds.length;
|
||||
const myPlayerData = players ? players[currentPlayerId] : null; isHost = (currentPlayerId === hostId);
|
||||
statusMessage.style.display = 'none'; displayGameId.textContent = currentGameId || 'N/A';
|
||||
setActiveView(state === 'lobby' ? 'lobby-view' : 'game-view');
|
||||
|
||||
switch (state) {
|
||||
case 'lobby':
|
||||
// Also reset game view state when going explicitly to lobby
|
||||
resetGameViewState();
|
||||
loadingStatusLobby.style.display = 'none';
|
||||
playerList.innerHTML = playerIds.map(pid => `<li>${players[pid].name} ${players[pid].isHost ? '(Host)' : ''}</li>`).join('');
|
||||
startGameBtn.disabled = !isHost || numPlayers < MIN_PLAYERS;
|
||||
if (lobbyMessage) { lobbyStatus.textContent = lobbyMessage; lobbyStatus.style.color = 'var(--accent-red)'; }
|
||||
else if (numPlayers < MIN_PLAYERS) { lobbyStatus.textContent = `Waiting for players (${numPlayers}/${MIN_PLAYERS})...`; lobbyStatus.style.color = 'var(--text-light-secondary)'; }
|
||||
else if (isHost) { lobbyStatus.textContent = `Ready (${numPlayers} players)!`; lobbyStatus.style.color = 'var(--accent-green)'; }
|
||||
else { lobbyStatus.textContent = `Waiting for host (${numPlayers} players)...`; lobbyStatus.style.color = 'var(--text-light-secondary)'; }
|
||||
lobbyStatus.style.display = 'inline-block';
|
||||
break;
|
||||
case 'starting_round':
|
||||
resetGameViewState(); // Clear previous round elements explicitly here
|
||||
roundTitle.textContent = `Round ${round || 1}`;
|
||||
if (playerIds.length > 0) {
|
||||
scoreTitle.style.display = 'block'; scoreList.style.display = 'block';
|
||||
scoreList.innerHTML = playerIds.map(pid => `<li>${players[pid].name}<span class="player-score">${players[pid].score || 0}</span></li>`).join('');
|
||||
}
|
||||
loadingStatusGame.textContent = "Starting Next Round..."; loadingStatusGame.style.display = 'inline-block';
|
||||
break;
|
||||
case 'asking':
|
||||
roundTitle.textContent = `Round ${round || 1}`; scoreTitle.style.display = 'block'; scoreList.style.display = 'block';
|
||||
scoreList.innerHTML = playerIds.map(pid => `<li>${players[pid].name}<span class="player-score">${players[pid].score || 0}</span></li>`).join('');
|
||||
const isImposter = (currentPlayerId === imposterId); questionArea.style.display = 'block'; questionArea.textContent = isImposter ? imposterQuestionText : mainQuestionText;
|
||||
if (myPlayerData?.answer) {
|
||||
answerSection.style.display = 'none'; const ansC = playerIds.filter(pid => players[pid].answer).length; const remC = numPlayers - ansC;
|
||||
if (remC > 0) { waitingForAnswers.textContent = `Waiting for ${remC} more player(s)...`; waitingForAnswers.style.display = 'inline-block'; }
|
||||
else { waitingForAnswers.style.display = 'none'; }
|
||||
} else {
|
||||
answerSection.style.display = 'block';
|
||||
// Input value is NOT cleared here anymore - fix applied
|
||||
answerInput.disabled = false; submitAnswerBtn.disabled = false;
|
||||
waitingForAnswers.textContent = 'Submit your answer!'; waitingForAnswers.style.display = 'inline-block';
|
||||
}
|
||||
break;
|
||||
case 'voting':
|
||||
roundTitle.textContent = `Round ${round || 1}`; scoreTitle.style.display = 'block'; scoreList.style.display = 'block';
|
||||
scoreList.innerHTML = playerIds.map(pid => `<li>${players[pid].name}<span class="player-score">${players[pid].score || 0}</span></li>`).join('');
|
||||
votingSection.style.display = 'block'; revealedQuestionArea.innerHTML = `<strong>The actual question was:</strong> ${mainQuestionText || 'N/A'}`; displayAllAnswersForVoting(players); renderVotingOptions(players, currentPlayerId);
|
||||
if (myPlayerData?.vote) { votingOptions.style.display = 'none'; const votC = playerIds.filter(pid => players[pid].vote).length; const remC = numPlayers - votC; if(remC > 0) { voteStatus.textContent = `You voted. Waiting for ${remC} more vote(s)...`; } else { voteStatus.textContent = 'All votes submitted! Calculating results...'; } voteStatus.style.display = 'inline-block'; }
|
||||
else { votingOptions.style.display = 'block'; voteStatus.textContent = 'Place your vote!'; voteStatus.style.display = 'inline-block'; }
|
||||
break;
|
||||
case 'results':
|
||||
roundTitle.textContent = `Round ${round || 1}`; scoreTitle.style.display = 'block'; scoreList.style.display = 'block';
|
||||
scoreList.innerHTML = playerIds.map(pid => `<li>${players[pid].name}<span class="player-score">${players[pid].score || 0}</span></li>`).join('');
|
||||
resultsSection.style.display = 'block'; displayResults(gameData); nextRoundBtn.style.display = isHost ? 'inline-block' : 'none'; nextRoundBtn.disabled = false;
|
||||
break;
|
||||
case 'game_over':
|
||||
roundTitle.textContent = `Round ${round || 1}`; scoreTitle.style.display = 'block'; scoreList.style.display = 'block';
|
||||
scoreList.innerHTML = playerIds.map(pid => `<li>${players[pid].name}<span class="player-score">${players[pid].score || 0}</span></li>`).join('');
|
||||
resultsSection.style.display = 'block'; displayResults(gameData); gameOverSection.style.display = 'block'; winnerDisplay.textContent = `${winnerName || 'Someone'} wins!`; playAgainBtn.style.display = isHost ? 'inline-block' : 'none'; playAgainBtn.disabled = false;
|
||||
gameOverStatus.style.display = isHost ? 'none' : 'block';
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unhandled state: ${state}`); setActiveView('login-view');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Utility and Display Functions ---
|
||||
function resetGameViewState() { // Clears dynamic elements between rounds/states
|
||||
questionArea.style.display = 'none'; answerSection.style.display = 'none';
|
||||
waitingForAnswers.style.display = 'none'; votingSection.style.display = 'none';
|
||||
voteStatus.style.display = 'none'; resultsSection.style.display = 'none';
|
||||
gameOverSection.style.display = 'none'; scoreTitle.style.display = 'none';
|
||||
scoreList.style.display = 'none'; loadingStatusLobby.style.display = 'none';
|
||||
loadingStatusGame.style.display = 'none';
|
||||
// Clear input field ONLY during full reset of game elements
|
||||
answerInput.value = '';
|
||||
answerInput.disabled = false; submitAnswerBtn.disabled = false;
|
||||
revealedQuestionArea.textContent = ''; answersDisplay.innerHTML = '';
|
||||
votingOptions.innerHTML = ''; resultsDisplay.innerHTML = '';
|
||||
nextRoundBtn.style.display = 'none'; playAgainBtn.style.display = 'none';
|
||||
gameOverStatus.style.display = 'none';
|
||||
}
|
||||
|
||||
function displayAllAnswersForVoting(players) {
|
||||
answersDisplay.innerHTML = ''; if (!players) return; Object.keys(players).forEach(pid => { const p = players[pid]; const li = document.createElement('li'); li.classList.add('answer-item'); li.textContent = `${p.name}: ${p.answer || '(No answer submitted)'}`; answersDisplay.appendChild(li); });
|
||||
}
|
||||
|
||||
function renderVotingOptions(players, selfPlayerId) {
|
||||
votingOptions.innerHTML = ''; if (!players) return; Object.keys(players).forEach(pid => { if (pid !== selfPlayerId) { const p = players[pid]; const btn = document.createElement('button'); btn.textContent = `Vote for ${p.name}`; btn.dataset.playerId = pid; btn.onclick = () => submitVote(pid); votingOptions.appendChild(btn); } });
|
||||
}
|
||||
|
||||
function displayResults(gameData) {
|
||||
const { players, imposterId, imposterQuestionText } = gameData; resultsDisplay.innerHTML = ''; if (!players) return; const vr = {}; let aPI = []; let mV = 0; Object.keys(players).forEach(pid => { vr[pid] = 0; }); Object.keys(players).forEach(vPid => { const vt = players[vPid].vote; if (vt && vr[vt] !== undefined) { vr[vt]++; } }); Object.keys(vr).forEach(pid => { if (vr[pid] > mV) { mV = vr[pid]; aPI = [pid]; } else if (vr[pid] === mV && mV > 0) { aPI.push(pid); } }); const iP = players[imposterId]; const iCG = aPI.length === 1 && aPI[0] === imposterId; const iDiv = document.createElement('div'); iDiv.classList.add('result-item'); iDiv.innerHTML = `The Imposter was: <strong class="highlight-imposter">${iP.name}</strong>. `; if (iCG) { iDiv.innerHTML += `<span style="color: var(--accent-green);">(Correctly Guessed!)</span>`; } else { iDiv.innerHTML += `<span style="color: var(--accent-red);">(Not Guessed Correctly!)</span>`; } resultsDisplay.appendChild(iDiv); const iQDiv = document.createElement('div'); iQDiv.classList.add('imposter-question-reveal'); iQDiv.innerHTML = `The Imposter's question was: <strong>${imposterQuestionText || 'N/A'}</strong>`; resultsDisplay.appendChild(iQDiv); const ansTitle = document.createElement('h4'); ansTitle.textContent = "Player Answers & Votes Received:"; resultsDisplay.appendChild(ansTitle); Object.keys(players).forEach(pid => { const p = players[pid]; const pDiv = document.createElement('div'); pDiv.classList.add('result-item'); let vt = `(${vr[pid]} ${vr[pid] === 1 ? 'vote' : 'votes'})`; pDiv.innerHTML = `<strong>${p.name}</strong>: ${p.answer || "(No answer)"} ${vt}`; if (pid === imposterId) pDiv.querySelector('strong').classList.add('highlight-imposter'); if (aPI.includes(pid)) pDiv.classList.add('highlight-guess'); resultsDisplay.appendChild(pDiv); });
|
||||
}
|
||||
|
||||
// --- Cleanup and Reset ---
|
||||
function resetToLogin() {
|
||||
if (gameDataSubscription) { gameDataSubscription.off(); gameDataSubscription = null; }
|
||||
currentGameId = null;
|
||||
currentPlayerId = `player_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`;
|
||||
currentPlayerName = ''; isHost = false;
|
||||
playerNameInput.value = ''; gameIdInput.value = ''; joinGameBtn.disabled = false;
|
||||
usedQuestionIndices.clear(); // Clear used questions on full reset
|
||||
setActiveView('login-view');
|
||||
}
|
||||
|
||||
// --- Initial Setup ---
|
||||
setActiveView('login-view');
|
||||
|
||||
// Handle page unload/close attempt
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (currentGameId && currentPlayerId) { const pr = database.ref(`games/${currentGameId}/players/${currentPlayerId}`); pr.remove().catch(e => console.log("Error removing player on unload:", e)); }
|
||||
});
|
||||
|
||||
} else {
|
||||
// Fallback if Firebase SDK fails to load
|
||||
console.error("Firebase SDK not loaded. Game cannot start.");
|
||||
document.body.innerHTML = '<h1 style="color: red; text-align: center; margin-top: 50px;">Error: Could not load Firebase SDK. Please check your internet connection and script tags.</h1>';
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user