✨ New Remote-Control page
This commit is contained in:
@@ -27,6 +27,7 @@ from classquiz.routers import (
|
|||||||
eximport,
|
eximport,
|
||||||
login,
|
login,
|
||||||
sitemap,
|
sitemap,
|
||||||
|
remote,
|
||||||
)
|
)
|
||||||
from classquiz.socket_server import sio
|
from classquiz.socket_server import sio
|
||||||
from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks
|
from classquiz.helpers import meilisearch_init, telemetry_ping, bg_tasks
|
||||||
@@ -80,6 +81,7 @@ async def auth_middleware_wrapper(request: Request, call_next):
|
|||||||
return await rememberme_middleware(request, call_next)
|
return await rememberme_middleware(request, call_next)
|
||||||
|
|
||||||
|
|
||||||
|
app.include_router(remote.router, tags=["remote"], prefix="/api/v1/remote", include_in_schema=True)
|
||||||
app.include_router(login.router, tags=["auth"], prefix="/api/v1/login", include_in_schema=True)
|
app.include_router(login.router, tags=["auth"], prefix="/api/v1/login", include_in_schema=True)
|
||||||
|
|
||||||
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
|
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
|
||||||
@@ -96,4 +98,5 @@ app.include_router(
|
|||||||
app.include_router(editor.router, tags=["editor"], prefix="/api/v1/editor", include_in_schema=True)
|
app.include_router(editor.router, tags=["editor"], prefix="/api/v1/editor", include_in_schema=True)
|
||||||
app.include_router(eximport.router, tags=["export", "import"], prefix="/api/v1/eximport", include_in_schema=True)
|
app.include_router(eximport.router, tags=["export", "import"], prefix="/api/v1/eximport", include_in_schema=True)
|
||||||
app.include_router(sitemap.router, tags=["sitemap"], prefix="/api/v1/sitemap", include_in_schema=True)
|
app.include_router(sitemap.router, tags=["sitemap"], prefix="/api/v1/sitemap", include_in_schema=True)
|
||||||
|
|
||||||
app.mount("/", ASGIApp(sio))
|
app.mount("/", ASGIApp(sio))
|
||||||
|
|||||||
@@ -240,3 +240,9 @@ class GameSession(BaseModel):
|
|||||||
class UpdatePassword(BaseModel):
|
class UpdatePassword(BaseModel):
|
||||||
old_password: str
|
old_password: str
|
||||||
new_password: str
|
new_password: str
|
||||||
|
|
||||||
|
|
||||||
|
class GameInLobby(BaseModel):
|
||||||
|
game_pin: str
|
||||||
|
quiz_title: str
|
||||||
|
game_id: uuid.UUID
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel, validator
|
||||||
from classquiz.config import settings, redis
|
from classquiz.config import settings, redis
|
||||||
@@ -155,17 +157,21 @@ async def get_game_user_count(game_pin: str, api_key: str, as_string: bool = Fal
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/players",
|
"/players",
|
||||||
)
|
)
|
||||||
async def get_game_session(game_pin: str, api_key: str):
|
async def get_game_session(game_pin: str, api_key: str | None = None, game_id: uuid.UUID | None = None):
|
||||||
|
if game_id is None and api_key is None:
|
||||||
|
raise HTTPException(status_code=401, detail="API-Key and Quiz-ID are missing")
|
||||||
user_id = await check_api_key(api_key)
|
user_id = await check_api_key(api_key)
|
||||||
redis_res = await redis.get(f"game_session:{game_pin}")
|
redis_res = await redis.get(f"game_session:{game_pin}")
|
||||||
if redis_res is None:
|
if redis_res is None:
|
||||||
game_pin = await redis.get(f"game_pin:{user_id}:{game_pin}")
|
game_pin = await redis.get(f"game_pin:{user_id}:{game_pin}")
|
||||||
redis_res = await redis.get(f"game_session:{game_pin}")
|
redis_res = await redis.get(f"game_session:{game_pin}")
|
||||||
if redis_res is None or user_id is None:
|
if redis_res is None:
|
||||||
raise HTTPException(status_code=404, detail="Game not found or API key not found")
|
raise HTTPException(status_code=404, detail="Game not found or API key not found")
|
||||||
data = GameSession.parse_raw(redis_res)
|
data = GameSession.parse_raw(redis_res)
|
||||||
|
if user_id is None and data.game_id != str(game_id):
|
||||||
|
raise HTTPException(status_code=401, detail="Game not found or API key not found")
|
||||||
game = PlayGame.parse_raw(await redis.get(f"game:{game_pin}"))
|
game = PlayGame.parse_raw(await redis.get(f"game:{game_pin}"))
|
||||||
if game.user_id != user_id:
|
if game.user_id != user_id and data.game_id != str(game_id):
|
||||||
raise HTTPException(status_code=404, detail="Game not found or API key not found")
|
raise HTTPException(status_code=404, detail="Game not found or API key not found")
|
||||||
for i in range(0, len(game.questions)):
|
for i in range(0, len(game.questions)):
|
||||||
res = await redis.get(f"game_session:{game_pin}:{i}")
|
res = await redis.get(f"game_session:{game_pin}:{i}")
|
||||||
@@ -195,7 +201,7 @@ async def set_next_question(game_pin: str, question_number: int, api_key: str):
|
|||||||
if game_data.user_id != user_id:
|
if game_data.user_id != user_id:
|
||||||
raise HTTPException(status_code=404, detail="Game not found or API key not found")
|
raise HTTPException(status_code=404, detail="Game not found or API key not found")
|
||||||
game_data.current_question = question_number
|
game_data.current_question = question_number
|
||||||
await redis.set(f"game:{game_pin}", game_data.json())
|
await redis.set(f"game:{game_pin}", game_data.json(), ex=18000)
|
||||||
await sio.emit(
|
await sio.emit(
|
||||||
"set_question_number",
|
"set_question_number",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import bleach
|
|||||||
|
|
||||||
from classquiz.auth import get_current_user
|
from classquiz.auth import get_current_user
|
||||||
from classquiz.config import redis, settings, storage, meilisearch
|
from classquiz.config import redis, settings, storage, meilisearch
|
||||||
from classquiz.db.models import Quiz, QuizInput, User, PlayGame
|
from classquiz.db.models import Quiz, QuizInput, User, PlayGame, GameInLobby
|
||||||
from classquiz.kahoot_importer.import_quiz import import_quiz
|
from classquiz.kahoot_importer.import_quiz import import_quiz
|
||||||
import html
|
import html
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@@ -129,6 +129,11 @@ async def start_quiz(
|
|||||||
)
|
)
|
||||||
await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000)
|
await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000)
|
||||||
await redis.set(f"game_pin:{user.id}:{quiz_id}", game_pin, ex=18000)
|
await redis.set(f"game_pin:{user.id}:{quiz_id}", game_pin, ex=18000)
|
||||||
|
await redis.set(
|
||||||
|
f"game_in_lobby:{user.id.hex}",
|
||||||
|
GameInLobby(game_id=game.game_id, game_pin=str(game_pin), quiz_title=quiz.title).json(),
|
||||||
|
ex=900,
|
||||||
|
)
|
||||||
return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})}
|
return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
|
from classquiz.auth import get_current_user
|
||||||
|
from classquiz.db.models import User, GameInLobby
|
||||||
|
from classquiz.config import redis
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/game_waiting")
|
||||||
|
async def get_game_in_lobby(user: User = Depends(get_current_user)):
|
||||||
|
game_in_lobby_raw = await redis.get(f"game_in_lobby:{user.id.hex}")
|
||||||
|
if game_in_lobby_raw is None:
|
||||||
|
raise HTTPException(status_code=404, detail="No game waiting")
|
||||||
|
game_in_lobby = GameInLobby.parse_raw(game_in_lobby_raw)
|
||||||
|
return game_in_lobby
|
||||||
@@ -116,7 +116,7 @@ async def join_game(sid: str, data: dict):
|
|||||||
)
|
)
|
||||||
redis_res = await redis.get(f"game_session:{data.game_pin}")
|
redis_res = await redis.get(f"game_session:{data.game_pin}")
|
||||||
redis_res = GameSession.parse_raw(redis_res)
|
redis_res = GameSession.parse_raw(redis_res)
|
||||||
await redis.set(f"game_session:{data.game_pin}:players:{data.username}", sid, ex=18000)
|
await redis.set(f"game_session:{data.game_pin}:players:{data.username}", sid, ex=7200)
|
||||||
await redis.sadd(f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=sid).json())
|
await redis.sadd(f"game_session:{data.game_pin}:players", GamePlayer(username=data.username, sid=sid).json())
|
||||||
if data.custom_field == "":
|
if data.custom_field == "":
|
||||||
data.custom_field = None
|
data.custom_field = None
|
||||||
@@ -131,7 +131,7 @@ async def join_game(sid: str, data: dict):
|
|||||||
await sio.emit(
|
await sio.emit(
|
||||||
"player_joined",
|
"player_joined",
|
||||||
{"username": data.username, "sid": sid},
|
{"username": data.username, "sid": sid},
|
||||||
room=redis_res.admin,
|
room=f"admin:{data.game_pin}",
|
||||||
)
|
)
|
||||||
# +++ Time-Sync +++
|
# +++ Time-Sync +++
|
||||||
encrypted_datetime = fernet.encrypt(datetime.now().isoformat().encode("utf-8")).decode("utf-8")
|
encrypted_datetime = fernet.encrypt(datetime.now().isoformat().encode("utf-8")).decode("utf-8")
|
||||||
@@ -146,7 +146,8 @@ async def start_game(sid: str, _data: dict):
|
|||||||
if session["admin"]:
|
if session["admin"]:
|
||||||
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
|
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
|
||||||
game_data.started = True
|
game_data.started = True
|
||||||
await redis.set(f"game:{session['game_pin']}", game_data.json())
|
await redis.set(f"game:{session['game_pin']}", game_data.json(), ex=7200)
|
||||||
|
await redis.delete(f"game_in_lobby:{game_data.user_id.hex}")
|
||||||
await sio.emit("start_game", room=session["game_pin"])
|
await sio.emit("start_game", room=session["game_pin"])
|
||||||
|
|
||||||
|
|
||||||
@@ -169,7 +170,7 @@ async def register_as_admin(sid: str, data: dict):
|
|||||||
await redis.set(
|
await redis.set(
|
||||||
f"game_session:{game_pin}",
|
f"game_session:{game_pin}",
|
||||||
GameSession(admin=sid, game_id=game_id, answers=[]).json(),
|
GameSession(admin=sid, game_id=game_id, answers=[]).json(),
|
||||||
ex=18000,
|
ex=7200,
|
||||||
)
|
)
|
||||||
|
|
||||||
await sio.emit(
|
await sio.emit(
|
||||||
@@ -180,7 +181,9 @@ async def register_as_admin(sid: str, data: dict):
|
|||||||
async with sio.session(sid) as session:
|
async with sio.session(sid) as session:
|
||||||
session["game_pin"] = game_pin
|
session["game_pin"] = game_pin
|
||||||
session["admin"] = True
|
session["admin"] = True
|
||||||
|
session["remote"] = False
|
||||||
sio.enter_room(sid, game_pin)
|
sio.enter_room(sid, game_pin)
|
||||||
|
sio.enter_room(sid, f"admin:{data.game_pin}")
|
||||||
else:
|
else:
|
||||||
await sio.emit("already_registered_as_admin", room=sid)
|
await sio.emit("already_registered_as_admin", room=sid)
|
||||||
|
|
||||||
@@ -227,8 +230,8 @@ async def set_question_number(sid, data: str):
|
|||||||
game_pin = session["game_pin"]
|
game_pin = session["game_pin"]
|
||||||
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
|
game_data = PlayGame.parse_raw(await redis.get(f"game:{session['game_pin']}"))
|
||||||
game_data.current_question = int(float(data))
|
game_data.current_question = int(float(data))
|
||||||
await redis.set(f"game:{session['game_pin']}", game_data.json())
|
await redis.set(f"game:{session['game_pin']}", game_data.json(), ex=7200)
|
||||||
await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat())
|
await redis.set(f"game:{session['game_pin']}:current_time", datetime.now().isoformat(), ex=7200)
|
||||||
temp_return = game_data.dict(include={"questions"})["questions"][int(float(data))]
|
temp_return = game_data.dict(include={"questions"})["questions"][int(float(data))]
|
||||||
if game_data.questions[int(float(data))].type == QuizQuestionType.SLIDE:
|
if game_data.questions[int(float(data))].type == QuizQuestionType.SLIDE:
|
||||||
return
|
return
|
||||||
@@ -322,7 +325,7 @@ async def submit_answer(sid: str, data: dict):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
).json(),
|
).json(),
|
||||||
ex=18000,
|
ex=7200,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
answers = _AnswerDataList.parse_raw(answers)
|
answers = _AnswerDataList.parse_raw(answers)
|
||||||
@@ -338,7 +341,7 @@ async def submit_answer(sid: str, data: dict):
|
|||||||
await redis.set(
|
await redis.set(
|
||||||
f"game_session:{session['game_pin']}:{data.question_index}",
|
f"game_session:{session['game_pin']}:{data.question_index}",
|
||||||
answers.json(),
|
answers.json(),
|
||||||
ex=18000,
|
ex=7200,
|
||||||
)
|
)
|
||||||
answers = _AnswerDataList.parse_raw(await redis.get(f"game_session:{session['game_pin']}:{data.question_index}"))
|
answers = _AnswerDataList.parse_raw(await redis.get(f"game_session:{session['game_pin']}:{data.question_index}"))
|
||||||
player_count = await redis.scard(f"game_session:{session['game_pin']}:players")
|
player_count = await redis.scard(f"game_session:{session['game_pin']}:players")
|
||||||
@@ -349,7 +352,9 @@ async def submit_answer(sid: str, data: dict):
|
|||||||
# room=session["game_pin"],
|
# room=session["game_pin"],
|
||||||
# )
|
# )
|
||||||
await sio.emit("everyone_answered", {})
|
await sio.emit("everyone_answered", {})
|
||||||
# await redis.set(f"game_data:{session['game_pin']}", json.dumps(data))
|
|
||||||
|
|
||||||
|
# await redis.set(f"game_data:{session['game_pin']}", json.dumps(data))
|
||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
@@ -363,14 +368,14 @@ async def get_final_results(sid: str, _data: dict):
|
|||||||
|
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
async def get_export_token(sid):
|
async def get_export_token(sid: str):
|
||||||
session = await sio.get_session(sid)
|
session = await sio.get_session(sid)
|
||||||
if not session["admin"]:
|
if not session["admin"]:
|
||||||
return
|
return
|
||||||
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
|
game_data = PlayGame(**json.loads(await redis.get(f"game:{session['game_pin']}")))
|
||||||
results = await generate_final_results(game_data, session["game_pin"])
|
results = await generate_final_results(game_data, session["game_pin"])
|
||||||
token = os.urandom(32).hex()
|
token = os.urandom(32).hex()
|
||||||
await redis.set(f"export_token:{token}", json.dumps(results))
|
await redis.set(f"export_token:{token}", json.dumps(results), ex=7200)
|
||||||
await sio.emit("export_token", token, room=sid)
|
await sio.emit("export_token", token, room=sid)
|
||||||
|
|
||||||
|
|
||||||
@@ -407,7 +412,6 @@ async def kick_player(sid: str, data: dict):
|
|||||||
return
|
return
|
||||||
|
|
||||||
session: dict = await sio.get_session(sid)
|
session: dict = await sio.get_session(sid)
|
||||||
print(sid)
|
|
||||||
if not session["admin"]:
|
if not session["admin"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -417,3 +421,46 @@ async def kick_player(sid: str, data: dict):
|
|||||||
)
|
)
|
||||||
sio.leave_room(player_sid, session["game_pin"])
|
sio.leave_room(player_sid, session["game_pin"])
|
||||||
await sio.emit("kick", room=player_sid)
|
await sio.emit("kick", room=player_sid)
|
||||||
|
|
||||||
|
|
||||||
|
class _RegisterAsRemoteInput(BaseModel):
|
||||||
|
game_pin: str
|
||||||
|
game_id: str
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def register_as_remote(sid: str, data: dict):
|
||||||
|
try:
|
||||||
|
data = _RegisterAsRemoteInput(**data)
|
||||||
|
except ValidationError as e:
|
||||||
|
await sio.emit("error", room=sid)
|
||||||
|
print(e)
|
||||||
|
return
|
||||||
|
await sio.emit(
|
||||||
|
"registered_as_admin",
|
||||||
|
{"game_id": data.game_id, "game": await redis.get(f"game:{data.game_pin}")},
|
||||||
|
room=sid,
|
||||||
|
)
|
||||||
|
await sio.emit("control_visibility", {"visible": False}, room=f"admin:{data.game_pin}")
|
||||||
|
async with sio.session(sid) as session:
|
||||||
|
session["game_pin"] = data.game_pin
|
||||||
|
session["admin"] = True
|
||||||
|
session["remote"] = True
|
||||||
|
sio.enter_room(sid, data.game_pin)
|
||||||
|
sio.enter_room(sid, f"admin:{data.game_pin}")
|
||||||
|
|
||||||
|
|
||||||
|
class _SetControlVisibilityInput(BaseModel):
|
||||||
|
visible: bool
|
||||||
|
|
||||||
|
|
||||||
|
@sio.event
|
||||||
|
async def set_control_visibility(sid: str, data: dict):
|
||||||
|
try:
|
||||||
|
data = _SetControlVisibilityInput(**data)
|
||||||
|
except ValidationError as e:
|
||||||
|
await sio.emit("error", room=sid)
|
||||||
|
print(e)
|
||||||
|
return
|
||||||
|
session: dict = await sio.get_session(sid)
|
||||||
|
await sio.emit("control_visibility", {"visible": data.visible}, room=f"admin:{session['game_pin']}")
|
||||||
|
|||||||
@@ -28,27 +28,38 @@
|
|||||||
let shown_question_now: number;
|
let shown_question_now: number;
|
||||||
let final_results_clicked = false;
|
let final_results_clicked = false;
|
||||||
let timer_interval;
|
let timer_interval;
|
||||||
|
export let control_visible: boolean;
|
||||||
|
|
||||||
export let player_scores;
|
export let player_scores;
|
||||||
|
|
||||||
export const set_question_number = (q_number: number) => {
|
export const set_question_number = (q_number: number) => {
|
||||||
|
socket.emit('set_question_number', q_number.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on('set_question_number', () => {
|
||||||
|
console.log('set_question_number');
|
||||||
|
});
|
||||||
|
socket.on('get_question_results', () => {
|
||||||
|
console.log('get_question_results');
|
||||||
|
});
|
||||||
|
socket.on('question_results', (_) => {
|
||||||
|
timer_res = '0';
|
||||||
|
clearInterval(timer_interval);
|
||||||
|
});
|
||||||
|
socket.on('set_question_number', (data) => {
|
||||||
timer_res = '0';
|
timer_res = '0';
|
||||||
clearInterval(timer_interval);
|
clearInterval(timer_interval);
|
||||||
// setTimeout(() => {
|
|
||||||
question_results = null;
|
question_results = null;
|
||||||
socket.emit('set_question_number', q_number.toString());
|
shown_question_now = data.question_index;
|
||||||
shown_question_now = q_number;
|
timer_res = quiz_data.questions[data.question_index].time;
|
||||||
timer_res = quiz_data.questions[q_number].time;
|
|
||||||
selected_question = selected_question + 1;
|
selected_question = selected_question + 1;
|
||||||
timer(timer_res);
|
timer(timer_res);
|
||||||
// }, 1000);
|
});
|
||||||
};
|
|
||||||
const get_question_results = () => {
|
const get_question_results = () => {
|
||||||
socket.emit('get_question_results', {
|
socket.emit('get_question_results', {
|
||||||
game_id: game_token,
|
game_id: game_token,
|
||||||
question_number: shown_question_now
|
question_number: shown_question_now
|
||||||
});
|
});
|
||||||
timer_res = '0';
|
|
||||||
};
|
};
|
||||||
const show_solutions = () => {
|
const show_solutions = () => {
|
||||||
socket.emit('show_solutions', {});
|
socket.emit('show_solutions', {});
|
||||||
@@ -57,12 +68,17 @@
|
|||||||
|
|
||||||
const get_final_results = () => {
|
const get_final_results = () => {
|
||||||
socket.emit('get_final_results', {});
|
socket.emit('get_final_results', {});
|
||||||
final_results_clicked = true;
|
|
||||||
timer_res = '0';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
socket.on('solutions', (_) => {
|
||||||
|
timer_res = '0';
|
||||||
|
clearInterval(timer_interval);
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('final_results', (data) => {
|
socket.on('final_results', (data) => {
|
||||||
// data = JSON.parse(data);
|
// data = JSON.parse(data);
|
||||||
|
final_results_clicked = true;
|
||||||
|
timer_res = '0';
|
||||||
final_results = data;
|
final_results = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,33 +121,49 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
{#if control_visible}
|
||||||
class="fixed top-0 w-full h-10 z-20 grid grid-cols-2"
|
<div
|
||||||
style="background: {bg_color ? bg_color : 'transparent'}"
|
class="fixed top-0 w-full h-10 z-20 grid grid-cols-2"
|
||||||
class:text-black={bg_color}
|
style="background: {bg_color ? bg_color : 'transparent'}"
|
||||||
>
|
class:text-black={bg_color}
|
||||||
<p class="mr-auto ml-0 col-start-1 col-end-1">
|
>
|
||||||
{selected_question === -1 ? '0' : selected_question + 1}
|
<p class="mr-auto ml-0 col-start-1 col-end-1">
|
||||||
/{quiz_data.questions.length}
|
{selected_question === -1 ? '0' : selected_question + 1}
|
||||||
</p>
|
/{quiz_data.questions.length}
|
||||||
<div class="justify-self-end ml-auto mr-0 col-start-3 col-end-3">
|
</p>
|
||||||
{#if selected_question + 1 === quiz_data.questions.length && ((timer_res === '0' && question_results !== null) || quiz_data?.questions?.[selected_question]?.type === QuizQuestionType.SLIDE)}
|
<div class="justify-self-end ml-auto mr-0 col-start-3 col-end-3">
|
||||||
{#if JSON.stringify(final_results) === JSON.stringify([null])}
|
{#if selected_question + 1 === quiz_data.questions.length && ((timer_res === '0' && question_results !== null) || quiz_data?.questions?.[selected_question]?.type === QuizQuestionType.SLIDE)}
|
||||||
<button on:click={get_final_results} class="admin-button"
|
{#if JSON.stringify(final_results) === JSON.stringify([null])}
|
||||||
>Get final results
|
<button on:click={get_final_results} class="admin-button"
|
||||||
</button>
|
>Get final results
|
||||||
{/if}
|
</button>
|
||||||
{:else if timer_res === '0' || selected_question === -1}
|
{/if}
|
||||||
{#if (selected_question + 1 !== quiz_data.questions.length && question_results !== null) || selected_question === -1}
|
{:else if timer_res === '0' || selected_question === -1}
|
||||||
<button
|
{#if (selected_question + 1 !== quiz_data.questions.length && question_results !== null) || selected_question === -1}
|
||||||
on:click={() => {
|
<button
|
||||||
set_question_number(selected_question + 1);
|
on:click={() => {
|
||||||
}}
|
set_question_number(selected_question + 1);
|
||||||
class="admin-button"
|
}}
|
||||||
>Next Question ({selected_question + 2})
|
class="admin-button"
|
||||||
</button>
|
>Next Question ({selected_question + 2})
|
||||||
{/if}
|
</button>
|
||||||
{#if question_results === null && selected_question !== -1}
|
{/if}
|
||||||
|
{#if question_results === null && selected_question !== -1}
|
||||||
|
{#if quiz_data.questions[selected_question].type === QuizQuestionType.SLIDE}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
set_question_number(selected_question + 1);
|
||||||
|
}}
|
||||||
|
class="admin-button"
|
||||||
|
>Next Question ({selected_question + 2})
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button on:click={get_question_results} class="admin-button"
|
||||||
|
>Show results
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{:else if selected_question !== -1}
|
||||||
{#if quiz_data.questions[selected_question].type === QuizQuestionType.SLIDE}
|
{#if quiz_data.questions[selected_question].type === QuizQuestionType.SLIDE}
|
||||||
<button
|
<button
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -141,45 +173,32 @@
|
|||||||
>Next Question ({selected_question + 2})
|
>Next Question ({selected_question + 2})
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<button on:click={get_question_results} class="admin-button"
|
<button on:click={show_solutions} class="admin-button"
|
||||||
>Show results</button
|
>Stop time and show solutions
|
||||||
>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{:else}
|
||||||
{:else if selected_question !== -1}
|
<!-- <button
|
||||||
{#if quiz_data.questions[selected_question].type === QuizQuestionType.SLIDE}
|
|
||||||
<button
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
set_question_number(selected_question + 1);
|
set_question_number(selected_question + 1);
|
||||||
}}
|
}}
|
||||||
class="admin-button"
|
class='admin-button'
|
||||||
>Next Question ({selected_question + 2})
|
>Next Question ({selected_question + 2}
|
||||||
</button>
|
)
|
||||||
{:else}
|
</button>-->
|
||||||
<button on:click={show_solutions} class="admin-button"
|
|
||||||
>Stop time and show solutions
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
</div>
|
||||||
<!-- <button
|
|
||||||
on:click={() => {
|
|
||||||
set_question_number(selected_question + 1);
|
|
||||||
}}
|
|
||||||
class='admin-button'
|
|
||||||
>Next Question ({selected_question + 2}
|
|
||||||
)
|
|
||||||
</button>-->
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{#if timer_res !== '0' && selected_question >= 0}
|
{#if timer_res !== '0' && selected_question >= 0}
|
||||||
<span
|
<span
|
||||||
class="fixed top-0 bg-red-500 h-8 transition-all mt-10"
|
class="fixed top-0 bg-red-500 h-8 transition-all"
|
||||||
|
class:mt-10={control_visible}
|
||||||
style="width: {(100 / quiz_data.questions[selected_question].time) * parseInt(timer_res)}vw"
|
style="width: {(100 / quiz_data.questions[selected_question].time) * parseInt(timer_res)}vw"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="w-full h-full pt-28">
|
<div class="w-full h-full" class:pt-28={control_visible} class:pt-12={!control_visible}>
|
||||||
{#if timer_res !== undefined && !final_results_clicked && !question_results}
|
{#if timer_res !== undefined && !final_results_clicked && !question_results}
|
||||||
{#if quiz_data.questions[selected_question].type === QuizQuestionType.SLIDE}
|
{#if quiz_data.questions[selected_question].type === QuizQuestionType.SLIDE}
|
||||||
{#await import('$lib/play/admin/slide.svelte')}
|
{#await import('$lib/play/admin/slide.svelte')}
|
||||||
|
|||||||
@@ -46,20 +46,39 @@
|
|||||||
copy_toast_open = false;
|
copy_toast_open = false;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let game_in_lobby: { game_pin: string; game_id: string; quiz_title: string } | undefined =
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
const get_game_in_lobby_fn = async () => {
|
||||||
|
const res = await fetch('/api/v1/remote/game_waiting');
|
||||||
|
console.log('Fetched.');
|
||||||
|
if (res.ok) {
|
||||||
|
game_in_lobby = await res.json();
|
||||||
|
}
|
||||||
|
};
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
document.body.addEventListener('keydown', close_start_game_if_esc_is_pressed);
|
document.body.addEventListener('keydown', close_start_game_if_esc_is_pressed);
|
||||||
});
|
});
|
||||||
$: console.log(quizzes, 'quizzes_here');
|
|
||||||
$: console.log(visibleImages, 'visible images');
|
get_game_in_lobby_fn();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if copy_toast_open}
|
{#if copy_toast_open || game_in_lobby}
|
||||||
<div class="fixed w-screen top-10 z-50 flex justify-center" transition:fly={{ y: -100 }}>
|
<div class="fixed w-screen top-10 z-50 flex justify-center" transition:fly={{ y: -100 }}>
|
||||||
<div
|
<div
|
||||||
class="flex items-center p-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
|
class="flex items-center p-4 w-full max-w-xs text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<div class="ml-3 text-sm font-normal">ID copied to clipboard!</div>
|
<div class="ml-3 text-sm font-normal">
|
||||||
|
{#if copy_toast_open}ID copied to clipboard!
|
||||||
|
{:else}A game is currently in the lobby. Click <a
|
||||||
|
class="underline"
|
||||||
|
href="/remote?game_pin={game_in_lobby.game_pin}&game_id={game_in_lobby.game_id}"
|
||||||
|
>here</a
|
||||||
|
> to join as a remote.
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
|
class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
@@ -67,6 +86,7 @@
|
|||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
copy_toast_open = false;
|
copy_toast_open = false;
|
||||||
|
game_in_lobby = undefined;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
let errorMessage = '';
|
let errorMessage = '';
|
||||||
let game_started = false;
|
let game_started = false;
|
||||||
let quiz_data: QuizData;
|
let quiz_data: QuizData;
|
||||||
|
let control_visible = true;
|
||||||
//let question_number = '0';
|
//let question_number = '0';
|
||||||
// let question_results = null;
|
// let question_results = null;
|
||||||
let final_results: Array<null> | Array<Array<PlayerAnswer>> = [null];
|
let final_results: Array<null> | Array<Array<PlayerAnswer>> = [null];
|
||||||
@@ -70,6 +71,14 @@
|
|||||||
errorMessage = $t('admin_page.already_registered_as_admin');
|
errorMessage = $t('admin_page.already_registered_as_admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('start_game', (_) => {
|
||||||
|
game_started = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('control_visibility', (data) => {
|
||||||
|
control_visible = data.visible;
|
||||||
|
});
|
||||||
|
|
||||||
/* socket.on('question_results', (int_data) => {
|
/* socket.on('question_results', (int_data) => {
|
||||||
try {
|
try {
|
||||||
int_data = JSON.parse(int_data);
|
int_data = JSON.parse(int_data);
|
||||||
@@ -83,7 +92,10 @@
|
|||||||
warnToLeave = false;
|
warnToLeave = false;
|
||||||
dataexport_download_a.href = `/api/v1/quiz/export_data/${int_data}?ts=${new Date().getTime()}&game_pin=${game_pin}`;
|
dataexport_download_a.href = `/api/v1/quiz/export_data/${int_data}?ts=${new Date().getTime()}&game_pin=${game_pin}`;
|
||||||
dataexport_download_a.click();
|
dataexport_download_a.click();
|
||||||
warnToLeave = true;
|
|
||||||
|
setTimeout(() => {
|
||||||
|
warnToLeave = true;
|
||||||
|
}, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
const confirmUnload = () => {
|
const confirmUnload = () => {
|
||||||
@@ -134,11 +146,13 @@
|
|||||||
class:text-black={bg_color}
|
class:text-black={bg_color}
|
||||||
>
|
>
|
||||||
{#if JSON.stringify(final_results) !== JSON.stringify([null])}
|
{#if JSON.stringify(final_results) !== JSON.stringify([null])}
|
||||||
<div class="w-screen flex justify-center mt-16">
|
{#if control_visible}
|
||||||
<button on:click={request_answer_export} class="admin-button"
|
<div class="w-screen flex justify-center mt-16">
|
||||||
>{$t('admin_page.export_results')}</button
|
<button on:click={request_answer_export} class="admin-button"
|
||||||
>
|
>{$t('admin_page.export_results')}</button
|
||||||
</div>
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<FinalResults bind:data={player_scores} bind:show_final_results />
|
<FinalResults bind:data={player_scores} bind:show_final_results />
|
||||||
{/if}
|
{/if}
|
||||||
{#if !success}
|
{#if !success}
|
||||||
@@ -181,7 +195,6 @@
|
|||||||
id="startGame"
|
id="startGame"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
socket.emit('start_game', '');
|
socket.emit('start_game', '');
|
||||||
game_started = true;
|
|
||||||
}}
|
}}
|
||||||
>{$t('admin_page.start_game')}
|
>{$t('admin_page.start_game')}
|
||||||
</button>
|
</button>
|
||||||
@@ -197,6 +210,7 @@
|
|||||||
bind:game_mode
|
bind:game_mode
|
||||||
bind:bg_color
|
bind:bg_color
|
||||||
bind:player_scores
|
bind:player_scores
|
||||||
|
bind:control_visible
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
<!--
|
||||||
|
- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { socket } from '$lib/socket';
|
||||||
|
import type { QuizData } from '$lib/quiz_types';
|
||||||
|
import { QuizQuestionType } from '$lib/quiz_types.js';
|
||||||
|
import Spinner from '$lib/Spinner.svelte';
|
||||||
|
import CircularTimer from '$lib/play/circular_progress.svelte';
|
||||||
|
import { getLocalization } from '$lib/i18n';
|
||||||
|
import { navbarVisible } from '$lib/stores';
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
game_pin: $page.url.searchParams.get('game_pin'),
|
||||||
|
game_id: $page.url.searchParams.get('game_id')
|
||||||
|
};
|
||||||
|
|
||||||
|
navbarVisible.set(false);
|
||||||
|
|
||||||
|
const { t } = getLocalization();
|
||||||
|
let timer_interval;
|
||||||
|
let timer_res = undefined;
|
||||||
|
let selected_question = -1;
|
||||||
|
let game_started = false;
|
||||||
|
let question_results;
|
||||||
|
let final_results = [null];
|
||||||
|
let warnToLeave = true;
|
||||||
|
let dataexport_download_a;
|
||||||
|
|
||||||
|
let players: Array<{ sid: string; username: string }> = [];
|
||||||
|
|
||||||
|
let game_data: QuizData;
|
||||||
|
let shown_question_now;
|
||||||
|
let control_visible = false;
|
||||||
|
|
||||||
|
if (!data.game_id || !data.game_pin) {
|
||||||
|
console.log('Error!');
|
||||||
|
} else {
|
||||||
|
socket.emit('register_as_remote', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = (time: string) => {
|
||||||
|
let seconds = Number(time);
|
||||||
|
timer_interval = setInterval(() => {
|
||||||
|
if (timer_res === '0') {
|
||||||
|
clearInterval(timer_interval);
|
||||||
|
// socket.emit('show_solutions', {});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
seconds--;
|
||||||
|
}
|
||||||
|
|
||||||
|
timer_res = seconds.toString();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmUnload = (e: Event) => {
|
||||||
|
if (warnToLeave) {
|
||||||
|
e.preventDefault();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
e.returnValue = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const set_question_number = (q_number: number) => {
|
||||||
|
socket.emit('set_question_number', q_number.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const start_game = () => {
|
||||||
|
socket.emit('start_game', '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const get_question_results = () => {
|
||||||
|
socket.emit('get_question_results', {
|
||||||
|
question_number: shown_question_now
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const show_solutions = () => {
|
||||||
|
socket.emit('show_solutions', {});
|
||||||
|
};
|
||||||
|
const get_final_results = () => {
|
||||||
|
socket.emit('get_final_results', {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const request_answer_export = async () => {
|
||||||
|
await socket.emit('get_export_token');
|
||||||
|
};
|
||||||
|
|
||||||
|
const get_already_joined_players = async () => {
|
||||||
|
console.log('GETTING PLAYERS');
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/v1/live/players?game_pin=${data.game_pin}&game_id=${data.game_id}`
|
||||||
|
);
|
||||||
|
if (res.ok) {
|
||||||
|
players = await res.json();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let circular_progress = 0;
|
||||||
|
$: {
|
||||||
|
try {
|
||||||
|
circular_progress =
|
||||||
|
1 -
|
||||||
|
((100 / game_data.questions[selected_question].time) * parseInt(timer_res)) / 100;
|
||||||
|
} catch {
|
||||||
|
circular_progress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.on('solutions', (_) => {
|
||||||
|
timer_res = '0';
|
||||||
|
clearInterval(timer_interval);
|
||||||
|
});
|
||||||
|
socket.on('question_results', (_) => {
|
||||||
|
timer_res = '0';
|
||||||
|
clearInterval(timer_interval);
|
||||||
|
});
|
||||||
|
socket.on('registered_as_admin', (data) => {
|
||||||
|
game_data = JSON.parse(data.game);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('start_game', (_) => {
|
||||||
|
game_started = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('control_visibility', (data) => {
|
||||||
|
control_visible = data.visible;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('question_results', (data) => {
|
||||||
|
try {
|
||||||
|
question_results = JSON.parse(data);
|
||||||
|
timer_res = '0';
|
||||||
|
} catch {
|
||||||
|
question_results = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.on('set_question_number', (data) => {
|
||||||
|
timer_res = '0';
|
||||||
|
clearInterval(timer_interval);
|
||||||
|
question_results = null;
|
||||||
|
shown_question_now = data.question_index;
|
||||||
|
timer_res = game_data.questions[data.question_index].time;
|
||||||
|
selected_question = selected_question + 1;
|
||||||
|
timer(timer_res);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('final_results', (data) => {
|
||||||
|
// data = JSON.parse(data);
|
||||||
|
final_results = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('everyone_answered', (_) => {
|
||||||
|
timer_res = '0';
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('export_token', (int_data) => {
|
||||||
|
warnToLeave = false;
|
||||||
|
dataexport_download_a.href = `/api/v1/quiz/export_data/${int_data}?ts=${new Date().getTime()}&game_pin=${
|
||||||
|
game_data.game_pin
|
||||||
|
}`;
|
||||||
|
dataexport_download_a.click();
|
||||||
|
setTimeout(() => {
|
||||||
|
warnToLeave = true;
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('player_joined', (data) => {
|
||||||
|
players = [...players, data];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:beforeunload={confirmUnload} />
|
||||||
|
{#if game_started}
|
||||||
|
{#if selected_question + 1 === game_data.questions.length && ((timer_res === '0' && question_results !== null) || game_data?.questions?.[selected_question]?.type === QuizQuestionType.SLIDE)}
|
||||||
|
{#if JSON.stringify(final_results) === JSON.stringify([null])}
|
||||||
|
<button on:click={get_final_results} class="admin-button">Get final results </button>
|
||||||
|
{:else}
|
||||||
|
<div class="w-screen flex justify-center mt-16">
|
||||||
|
<button on:click={request_answer_export} class="admin-button"
|
||||||
|
>{$t('admin_page.export_results')}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if timer_res === '0' || selected_question === -1}
|
||||||
|
{#if (selected_question + 1 !== game_data.questions.length && question_results !== null) || selected_question === -1}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
set_question_number(selected_question + 1);
|
||||||
|
}}
|
||||||
|
class="admin-button"
|
||||||
|
>Next Question ({selected_question + 2})
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#if question_results === null && selected_question !== -1}
|
||||||
|
{#if game_data.questions[selected_question].type === QuizQuestionType.SLIDE}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
set_question_number(selected_question + 1);
|
||||||
|
}}
|
||||||
|
class="admin-button"
|
||||||
|
>Next Question ({selected_question + 2})
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button on:click={get_question_results} class="admin-button">Show results </button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{:else if selected_question !== -1}
|
||||||
|
{#if game_data.questions[selected_question].type === QuizQuestionType.SLIDE}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
set_question_number(selected_question + 1);
|
||||||
|
}}
|
||||||
|
class="admin-button"
|
||||||
|
>Next Question ({selected_question + 2})
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button on:click={show_solutions} class="admin-button"
|
||||||
|
>Stop time and show solutions
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if selected_question === -1}
|
||||||
|
<h1>{game_data.title}</h1>
|
||||||
|
<p>{game_data.description}</p>
|
||||||
|
{:else if game_data.questions[selected_question].type === QuizQuestionType.SLIDE}
|
||||||
|
{#await import('$lib/play/admin/slide.svelte')}
|
||||||
|
<Spinner my_20={false} />
|
||||||
|
{:then c}
|
||||||
|
<svelte:component
|
||||||
|
this={c.default}
|
||||||
|
bind:question={game_data.questions[selected_question]}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col justify-center w-screen h-1/6">
|
||||||
|
<h1 class="text-6xl text-center">
|
||||||
|
{@html game_data.questions[selected_question].question}
|
||||||
|
</h1>
|
||||||
|
<!-- <span class='text-center py-2 text-lg'>{$t('admin_page.time_left')}: {timer_res}</span>-->
|
||||||
|
<div class="mx-auto my-2">
|
||||||
|
<CircularTimer
|
||||||
|
bind:text={timer_res}
|
||||||
|
bind:progress={circular_progress}
|
||||||
|
color="#ef4444"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if game_data.questions[selected_question].image !== null}
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={game_data.questions[selected_question].image}
|
||||||
|
class="max-h-[20vh] object-cover mx-auto mb-8 w-auto"
|
||||||
|
alt="Content for Question"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if game_data.questions[selected_question].type === QuizQuestionType.ABCD || game_data.questions[selected_question].type === QuizQuestionType.VOTING}
|
||||||
|
<div class="grid grid-cols-2 gap-2 w-full p-4">
|
||||||
|
{#each game_data.questions[selected_question].answers as answer, i}
|
||||||
|
<div
|
||||||
|
class="rounded-lg h-fit flex"
|
||||||
|
style="background-color: {answer.color ?? '#B45309'}"
|
||||||
|
class:opacity-50={!answer.right &&
|
||||||
|
game_data.questions[selected_question].type ===
|
||||||
|
QuizQuestionType.ABCD}
|
||||||
|
>
|
||||||
|
<span class="text-center text-2xl px-2 py-4 w-full text-black"
|
||||||
|
>{answer.answer}</span
|
||||||
|
>
|
||||||
|
<span class="pl-4 w-10" />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if timer_res === '0' && JSON.stringify(final_results) === JSON.stringify( [null] ) && game_data.questions[selected_question].type !== QuizQuestionType.SLIDE}
|
||||||
|
{#if game_data.questions[selected_question].type === QuizQuestionType.VOTING && question_results}
|
||||||
|
{#await import('$lib/play/admin/voting_results.svelte')}
|
||||||
|
<Spinner />
|
||||||
|
{:then c}
|
||||||
|
<svelte:component
|
||||||
|
this={c.default}
|
||||||
|
bind:data={question_results}
|
||||||
|
bind:question={game_data.questions[selected_question]}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<button on:click={start_game} class="admin-button">Start Game!</button>
|
||||||
|
|
||||||
|
<h2>Players:</h2>
|
||||||
|
{#await get_already_joined_players()}
|
||||||
|
<Spinner my_20={false} />
|
||||||
|
{:then _}
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{#each players as player}
|
||||||
|
<li>{player.username}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
<div class="fixed top-0 right-0">
|
||||||
|
{#if control_visible}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
socket.emit('set_control_visibility', { visible: false });
|
||||||
|
}}
|
||||||
|
>Hide Controls
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
socket.emit('set_control_visibility', { visible: true });
|
||||||
|
}}
|
||||||
|
>Show Controls
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
on:click|preventDefault={request_answer_export}
|
||||||
|
href="#"
|
||||||
|
bind:this={dataexport_download_a}
|
||||||
|
class="absolute -top-3/4 -left-3/4 opacity-0 hidden">Download</a
|
||||||
|
>
|
||||||
Reference in New Issue
Block a user