mirror of
https://github.com/AlecM33/Werewolf.git
synced 2026-01-01 08:49:44 +01:00
refactor of networking when first reaching game page
This commit is contained in:
@@ -9,7 +9,6 @@ export const globals = {
|
||||
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
|
||||
COMMANDS: {
|
||||
FETCH_GAME_STATE: 'fetchGameState',
|
||||
GET_ENVIRONMENT: 'getEnvironment',
|
||||
START_GAME: 'startGame',
|
||||
PAUSE_TIMER: 'pauseTimer',
|
||||
RESUME_TIMER: 'resumeTimer',
|
||||
@@ -18,8 +17,7 @@ export const globals = {
|
||||
REVEAL_PLAYER: 'revealPlayer',
|
||||
TRANSFER_MODERATOR: 'transferModerator',
|
||||
CHANGE_NAME: 'changeName',
|
||||
END_GAME: 'endGame',
|
||||
FETCH_IN_PROGRESS_STATE: 'fetchInitialInProgressState'
|
||||
END_GAME: 'endGame'
|
||||
},
|
||||
STATUS: {
|
||||
LOBBY: 'lobby',
|
||||
|
||||
@@ -105,6 +105,10 @@ export class GameCreationStepManager {
|
||||
window.location = ('/game/' + res.content);
|
||||
}
|
||||
}).catch((e) => {
|
||||
let button = document.getElementById("create-game");
|
||||
button.innerText = "Create Game";
|
||||
button.classList.remove('submitted');
|
||||
button.addEventListener('click', this.steps["4"].forwardHandler);
|
||||
if (e.status === 429) {
|
||||
toast('You\'ve sent this request too many times.', 'error', true, true, 6);
|
||||
}
|
||||
@@ -395,6 +399,9 @@ function showButtons (back, forward, forwardHandler, backHandler, builtGame = nu
|
||||
createButton.setAttribute('id', 'create-game');
|
||||
createButton.classList.add('app-button');
|
||||
createButton.addEventListener('click', () => {
|
||||
createButton.removeEventListener('click', forwardHandler);
|
||||
createButton.classList.add('submitted');
|
||||
createButton.innerText = 'Creating...'
|
||||
forwardHandler(
|
||||
builtGame.deck.filter((card) => card.quantity > 0),
|
||||
builtGame.hasTimer,
|
||||
|
||||
@@ -254,7 +254,7 @@ function renderPotentialMods (gameState, group, transferModHandlers, socket) {
|
||||
if ((member.out || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) {
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('potential-moderator');
|
||||
container.setAttribute("tabindex", "0");
|
||||
container.setAttribute('tabindex', '0');
|
||||
container.dataset.pointer = member.id;
|
||||
container.innerText = member.name;
|
||||
transferModHandlers[member.id] = (e) => {
|
||||
|
||||
@@ -8,82 +8,105 @@ import { ModalManager } from '../modules/ModalManager.js';
|
||||
import { stateBucket } from '../modules/StateBucket.js';
|
||||
import { io } from 'socket.io-client';
|
||||
import { injectNavbar } from '../modules/Navbar.js';
|
||||
import { XHRUtility } from '../modules/XHRUtility.js';
|
||||
|
||||
const game = () => {
|
||||
injectNavbar();
|
||||
const timerWorker = new Worker(new URL('../modules/Timer.js', import.meta.url));
|
||||
const socket = io('/in-game');
|
||||
socket.on('disconnect', () => {
|
||||
toast('Disconnected. Attempting reconnect...', 'error', true, false);
|
||||
});
|
||||
socket.on('connect', () => {
|
||||
console.log('connect event fired');
|
||||
socket.emit(globals.COMMANDS.GET_ENVIRONMENT, function (returnedEnvironment) {
|
||||
prepareGamePage(returnedEnvironment, socket, timerWorker);
|
||||
XHRUtility.xhr(
|
||||
'/api/games/environment',
|
||||
'GET',
|
||||
null,
|
||||
null
|
||||
)
|
||||
.then((res) => {
|
||||
joinGame(res);
|
||||
}).catch((res) => {
|
||||
toast(res.content, 'error', true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function prepareGamePage (environment, socket, timerWorker) {
|
||||
let userId = UserUtility.validateAnonUserSignature(environment);
|
||||
function joinGame (environmentResponse) {
|
||||
let cookie = UserUtility.validateAnonUserSignature(environmentResponse.content);
|
||||
const splitUrl = window.location.href.split('/game/');
|
||||
const accessCode = splitUrl[1];
|
||||
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
|
||||
socket.emit(globals.COMMANDS.FETCH_GAME_STATE, accessCode, userId, function (gameState) {
|
||||
stateBucket.currentGameState = gameState;
|
||||
document.querySelector('.spinner-container')?.remove();
|
||||
document.querySelector('.spinner-background')?.remove();
|
||||
|
||||
if (gameState === null) {
|
||||
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
|
||||
} else {
|
||||
document.getElementById('game-content').innerHTML = templates.INITIAL_GAME_DOM;
|
||||
toast('You are connected.', 'success', true, true, 2);
|
||||
userId = gameState.client.cookie;
|
||||
UserUtility.setAnonymousUserId(userId, environment);
|
||||
XHRUtility.xhr(
|
||||
'/api/games/' + accessCode + '/players',
|
||||
'PATCH',
|
||||
null,
|
||||
JSON.stringify({ cookie: cookie })
|
||||
)
|
||||
.then((res) => {
|
||||
cookie = res.content;
|
||||
UserUtility.setAnonymousUserId(res.content, environmentResponse.content);
|
||||
const timerWorker = new Worker(new URL('../modules/Timer.js', import.meta.url));
|
||||
const socket = io('/in-game');
|
||||
const gameTimerManager = new GameTimerManager(stateBucket, socket);
|
||||
const gameStateRenderer = new GameStateRenderer(stateBucket, socket);
|
||||
let gameTimerManager;
|
||||
if (stateBucket.currentGameState.timerParams) {
|
||||
gameTimerManager = new GameTimerManager(stateBucket, socket);
|
||||
socket.on('disconnect', () => {
|
||||
toast('Disconnected. Attempting reconnect...', 'error', true, false);
|
||||
});
|
||||
socket.on('connect', () => {
|
||||
prepareGamePage(
|
||||
environmentResponse.content,
|
||||
socket,
|
||||
timerWorker,
|
||||
cookie,
|
||||
accessCode,
|
||||
gameStateRenderer,
|
||||
gameTimerManager
|
||||
);
|
||||
});
|
||||
setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager);
|
||||
}).catch((res) => {
|
||||
if (res.status === 404) {
|
||||
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
|
||||
} else if (res.status >= 500) {
|
||||
toast(
|
||||
'The server is experiencing problems. Please try again later',
|
||||
'error',
|
||||
true
|
||||
);
|
||||
}
|
||||
initializeGame(stateBucket, socket, timerWorker, userId, gameStateRenderer, gameTimerManager);
|
||||
|
||||
if (!gameState.client.hasEnteredName) {
|
||||
document.getElementById('prompt').innerHTML = templates.NAME_CHANGE_MODAL;
|
||||
document.getElementById('change-name-form').onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('player-new-name').value;
|
||||
if (validateName(name)) {
|
||||
socket.emit(globals.COMMANDS.CHANGE_NAME, gameState.accessCode, {
|
||||
name: name,
|
||||
personId: gameState.client.id
|
||||
}, (result) => {
|
||||
switch (result) {
|
||||
case 'taken':
|
||||
toast('This name is already taken.', 'error', true, true, 8);
|
||||
break;
|
||||
case 'changed':
|
||||
ModalManager.dispelModal('change-name-modal', 'change-name-modal-background');
|
||||
toast('Name set.', 'success', true, true, 5);
|
||||
propagateNameChange(stateBucket.currentGameState, name, stateBucket.currentGameState.client.id);
|
||||
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toast('Name must be between 1 and 30 characters.', 'error', true, true, 8);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initializeGame (stateBucket, socket, timerWorker, userId, gameStateRenderer, gameTimerManager) {
|
||||
setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager);
|
||||
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
function prepareGamePage (environment, socket, timerWorker, cookie, accessCode, gameStateRenderer, gameTimerManager) {
|
||||
socket.emit(globals.COMMANDS.FETCH_GAME_STATE, accessCode, cookie, function (gameState) {
|
||||
stateBucket.currentGameState = gameState;
|
||||
document.querySelector('.spinner-container')?.remove();
|
||||
document.querySelector('.spinner-background')?.remove();
|
||||
document.getElementById('game-content').innerHTML = templates.INITIAL_GAME_DOM;
|
||||
toast('You are connected.', 'success', true, true, 2);
|
||||
processGameState(stateBucket.currentGameState, cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
if (!gameState.client.hasEnteredName) {
|
||||
document.getElementById('prompt').innerHTML = templates.NAME_CHANGE_MODAL;
|
||||
document.getElementById('change-name-form').onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('player-new-name').value;
|
||||
if (validateName(name)) {
|
||||
socket.emit(globals.COMMANDS.CHANGE_NAME, gameState.accessCode, {
|
||||
name: name,
|
||||
personId: gameState.client.id
|
||||
}, (result) => {
|
||||
switch (result) {
|
||||
case 'taken':
|
||||
toast('This name is already taken.', 'error', true, true, 8);
|
||||
break;
|
||||
case 'changed':
|
||||
ModalManager.dispelModal('change-name-modal', 'change-name-modal-background');
|
||||
toast('Name set.', 'success', true, true, 5);
|
||||
propagateNameChange(stateBucket.currentGameState, name, stateBucket.currentGameState.client.id);
|
||||
processGameState(stateBucket.currentGameState, cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toast('Name must be between 1 and 30 characters.', 'error', true, true, 8);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processGameState (currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker, refreshPrompt = true) {
|
||||
@@ -163,79 +186,72 @@ function displayClientInfo (name, userType) {
|
||||
}
|
||||
|
||||
function setClientSocketHandlers (stateBucket, gameStateRenderer, socket, timerWorker, gameTimerManager) {
|
||||
if (!socket.hasListeners(globals.EVENTS.PLAYER_JOINED)) {
|
||||
socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => {
|
||||
toast(player.name + ' joined!', 'success', false, true, 3);
|
||||
stateBucket.currentGameState.people.push(player);
|
||||
stateBucket.currentGameState.isFull = gameIsFull;
|
||||
socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => {
|
||||
toast(player.name + ' joined!', 'success', false, true, 3);
|
||||
stateBucket.currentGameState.people.push(player);
|
||||
stateBucket.currentGameState.isFull = gameIsFull;
|
||||
gameStateRenderer.renderLobbyPlayers();
|
||||
if (
|
||||
gameIsFull
|
||||
&& (
|
||||
stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|
||||
|| stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|
||||
)
|
||||
) {
|
||||
displayStartGamePromptForModerators(stateBucket.currentGameState, gameStateRenderer);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(globals.EVENTS.PLAYER_LEFT, (player) => {
|
||||
removeStartGameFunctionalityIfPresent(gameStateRenderer);
|
||||
toast(player.name + ' has left!', 'error', false, true, 3);
|
||||
const index = stateBucket.currentGameState.people.findIndex(person => person.id === player.id);
|
||||
if (index >= 0) {
|
||||
stateBucket.currentGameState.people.splice(
|
||||
index,
|
||||
1
|
||||
);
|
||||
gameStateRenderer.renderLobbyPlayers();
|
||||
if (
|
||||
gameIsFull
|
||||
&& (
|
||||
stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|
||||
|| stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|
||||
)
|
||||
) {
|
||||
displayStartGamePromptForModerators(stateBucket.currentGameState, gameStateRenderer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!socket.hasListeners(globals.EVENTS.PLAYER_LEFT)) {
|
||||
socket.on(globals.EVENTS.PLAYER_LEFT, (player) => {
|
||||
removeStartGameFunctionalityIfPresent(gameStateRenderer);
|
||||
toast(player.name + ' has left!', 'error', false, true, 3);
|
||||
const index = stateBucket.currentGameState.people.findIndex(person => person.id === player.id);
|
||||
if (index >= 0) {
|
||||
stateBucket.currentGameState.people.splice(
|
||||
index,
|
||||
1
|
||||
socket.on(globals.EVENTS.START_GAME, () => {
|
||||
socket.emit(
|
||||
globals.COMMANDS.FETCH_GAME_STATE,
|
||||
stateBucket.currentGameState.accessCode,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
function (gameState) {
|
||||
stateBucket.currentGameState = gameState;
|
||||
processGameState(
|
||||
stateBucket.currentGameState,
|
||||
gameState.client.cookie,
|
||||
socket,
|
||||
gameStateRenderer,
|
||||
gameTimerManager,
|
||||
timerWorker
|
||||
);
|
||||
gameStateRenderer.renderLobbyPlayers();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!socket.hasListeners(globals.EVENTS.START_GAME)) {
|
||||
socket.on(globals.EVENTS.START_GAME, () => {
|
||||
socket.emit(
|
||||
globals.COMMANDS.FETCH_IN_PROGRESS_STATE,
|
||||
stateBucket.currentGameState.accessCode,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
function (gameState) {
|
||||
stateBucket.currentGameState = gameState;
|
||||
processGameState(
|
||||
stateBucket.currentGameState,
|
||||
gameState.client.cookie,
|
||||
socket,
|
||||
gameStateRenderer,
|
||||
gameTimerManager,
|
||||
timerWorker
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
if (!socket.hasListeners(globals.EVENTS.SYNC_GAME_STATE)) {
|
||||
socket.on(globals.EVENTS.SYNC_GAME_STATE, () => {
|
||||
socket.emit(
|
||||
globals.COMMANDS.FETCH_IN_PROGRESS_STATE,
|
||||
stateBucket.currentGameState.accessCode,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
function (gameState) {
|
||||
stateBucket.currentGameState = gameState;
|
||||
processGameState(
|
||||
stateBucket.currentGameState,
|
||||
gameState.client.cookie,
|
||||
socket,
|
||||
gameStateRenderer,
|
||||
gameTimerManager,
|
||||
timerWorker
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
socket.on(globals.EVENTS.SYNC_GAME_STATE, () => {
|
||||
socket.emit(
|
||||
globals.COMMANDS.FETCH_GAME_STATE,
|
||||
stateBucket.currentGameState.accessCode,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
function (gameState) {
|
||||
stateBucket.currentGameState = gameState;
|
||||
processGameState(
|
||||
stateBucket.currentGameState,
|
||||
gameState.client.cookie,
|
||||
socket,
|
||||
gameStateRenderer,
|
||||
gameTimerManager,
|
||||
timerWorker
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (timerWorker && gameTimerManager) {
|
||||
gameTimerManager.attachTimerSocketListeners(socket, timerWorker, gameStateRenderer);
|
||||
|
||||
@@ -21,7 +21,7 @@ function roomCodeIsValid (code) {
|
||||
|
||||
function attemptToJoinGame (code) {
|
||||
XHRUtility.xhr(
|
||||
'/api/games/availability/' + code.toLowerCase().trim(),
|
||||
'/api/games/' + code.toLowerCase().trim() + 'availability',
|
||||
'GET',
|
||||
null,
|
||||
null
|
||||
|
||||
@@ -11,10 +11,6 @@ th, thead, tr, tt, u, ul, var {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'signika-negative';
|
||||
src: url("../webfonts/Diavlo_LIGHT_II_37.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'signika-negative';
|
||||
@@ -164,6 +160,11 @@ button {
|
||||
border: 2px solid #8a1c1c !important;
|
||||
}
|
||||
|
||||
.submitted {
|
||||
filter: opacity(0.5);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Create A Game</title>
|
||||
<meta name="description" content="This resource was not found.">
|
||||
<meta property="og:title" content="Not Found">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://playwerewolf.uk.r.appspot.com/not-found">
|
||||
<meta property="og:description" content="The page you are looking for could not be found.">
|
||||
<meta property="og:image" content="image.png">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/styles/GLOBAL.css">
|
||||
<link rel="stylesheet" href="/styles/create.css">
|
||||
<link rel="stylesheet" href="/styles/modal.css">
|
||||
<link rel="stylesheet" href="/styles/hamburgers.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="mobile-menu-background-overlay"></div>
|
||||
<div id="navbar"></div>
|
||||
<span>
|
||||
<h1>404</h1>
|
||||
<h3>The game or other resource that you are looking for could not be found, or you don't have permission to access it.
|
||||
If this error is unexpected, the application may have restarted.</h3>
|
||||
</span>
|
||||
<style>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Not Found</title>
|
||||
<meta name="description" content="This resource was not found.">
|
||||
<meta property="og:title" content="Not Found">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://playwerewolf.uk.r.appspot.com/not-found">
|
||||
<meta property="og:description" content="The page you are looking for could not be found.">
|
||||
<meta property="og:image" content="image.png">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/styles/GLOBAL.css">
|
||||
<link rel="stylesheet" href="/styles/create.css">
|
||||
<link rel="stylesheet" href="/styles/modal.css">
|
||||
<link rel="stylesheet" href="/styles/hamburgers.css">
|
||||
<style>
|
||||
|
||||
h1 {
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h3 {
|
||||
max-width: 40em;
|
||||
font-size: 20px;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h3 {
|
||||
max-width: 40em;
|
||||
font-size: 20px;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
#not-found-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media(min-width: 701px) {
|
||||
h1 {
|
||||
font-size: 75px;
|
||||
}
|
||||
}
|
||||
@media(max-width: 700px) {
|
||||
h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="/dist/notFound-bundle.js"></script>
|
||||
</body>
|
||||
@media(min-width: 701px) {
|
||||
h1 {
|
||||
font-size: 75px;
|
||||
}
|
||||
}
|
||||
@media(max-width: 700px) {
|
||||
h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mobile-menu-background-overlay"></div>
|
||||
<div id="navbar"></div>
|
||||
<div id="not-found-container">
|
||||
<h1>404</h1>
|
||||
<h3>The game or other resource that you are looking for could not be found, or you don't have permission to access it.
|
||||
If this error is unexpected, the application may have restarted.</h3>
|
||||
</div>
|
||||
<script src="/dist/notFound-bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
<link rel="stylesheet" href="/styles/game.css">
|
||||
<link rel="stylesheet" href="/styles/modal.css">
|
||||
<link rel="stylesheet" href="/styles/hamburgers.css">
|
||||
<link rel="preload" as="font" href="/webfonts/SignikaNegative-Light.woff2" crossorigin/>
|
||||
<link rel="preload" as="font" href="/webfonts/Diavlo_LIGHT_II_37.woff2" crossorigin/>
|
||||
<link rel="preload" href="/webfonts/SignikaNegative-Light.woff2" as="font" type="font/woff2" crossorigin>
|
||||
</head>
|
||||
<body>
|
||||
<div id="prompt"></div>
|
||||
|
||||
Reference in New Issue
Block a user