feat(ui): add MVP host/player web screens
This commit is contained in:
18
docs/UI_SMOKE.md
Normal file
18
docs/UI_SMOKE.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# UI smoke (MVP)
|
||||||
|
|
||||||
|
## Forudsætning
|
||||||
|
- Host er logget ind i Django.
|
||||||
|
- Mindst én aktiv kategori med spørgsmål findes.
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
1. Åbn host-siden på /lobby/ui/host og tryk Opret session.
|
||||||
|
2. Åbn player-siden i 3 faner/enheder på /lobby/ui/player.
|
||||||
|
3. Join alle spillere med sessionkode og nickname.
|
||||||
|
4. Host: vælg kategori, Start runde, Vis spørgsmål.
|
||||||
|
5. Spillere: brug round_question_id og submit løgn.
|
||||||
|
6. Host: Mix svar.
|
||||||
|
7. Spillere: submit gæt.
|
||||||
|
8. Host: Beregn score og Vis scoreboard.
|
||||||
|
9. Host: Næste runde eller Afslut spil.
|
||||||
|
|
||||||
|
Resultat: En fuld runde kan køres uden rå API-kald fra terminal.
|
||||||
34
lobby/templates/lobby/host_screen.html
Normal file
34
lobby/templates/lobby/host_screen.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="da"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>WPP Host</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Host panel (MVP)</h1>
|
||||||
|
<p>Kræver login som host-bruger.</p>
|
||||||
|
<button onclick="createSession()">1) Opret session</button>
|
||||||
|
<input id="code" placeholder="Sessionkode">
|
||||||
|
<select id="category">{% for c in categories %}<option value="{{ c.slug }}">{{ c.name }}</option>{% endfor %}</select>
|
||||||
|
<button onclick="startRound()">2) Start runde</button>
|
||||||
|
<button onclick="showQuestion()">3) Vis spørgsmål</button>
|
||||||
|
<input id="roundQuestionId" placeholder="Round question id">
|
||||||
|
<button onclick="mixAnswers()">4) Mix svar</button>
|
||||||
|
<button onclick="calcScores()">5) Beregn score</button>
|
||||||
|
<button onclick="showScoreboard()">6) Scoreboard</button>
|
||||||
|
<button onclick="nextRound()">7) Næste runde</button>
|
||||||
|
<button onclick="finishGame()">8) Afslut spil</button>
|
||||||
|
<button onclick="sessionDetail()">Session-status</button>
|
||||||
|
<pre id="out">Klar.</pre>
|
||||||
|
<script>
|
||||||
|
function csrf(){var m=document.cookie.match(/csrftoken=([^;]+)/);return m?m[1]:"";}
|
||||||
|
function code(){return document.getElementById("code").value.trim().toUpperCase();}
|
||||||
|
function rq(){return document.getElementById("roundQuestionId").value.trim();}
|
||||||
|
async function api(path,method,payload){var o={method:method||"GET",headers:{"Accept":"application/json"}};if(payload!==null){o.headers["Content-Type"]="application/json";o.headers["X-CSRFToken"]=csrf();o.body=JSON.stringify(payload);}var r=await fetch(path,o);var d=await r.json().catch(function(){return {};});document.getElementById("out").textContent=JSON.stringify({status:r.status,data:d},null,2);if(d.session&&d.session.code){document.getElementById("code").value=d.session.code;}if(d.round_question&&d.round_question.id){document.getElementById("roundQuestionId").value=d.round_question.id;}return d;}
|
||||||
|
function createSession(){return api("/lobby/sessions/create","POST",{});}
|
||||||
|
function sessionDetail(){return api("/lobby/sessions/"+code(),"GET",null);}
|
||||||
|
function startRound(){return api("/lobby/sessions/"+code()+"/rounds/start","POST",{category_slug:document.getElementById("category").value});}
|
||||||
|
function showQuestion(){return api("/lobby/sessions/"+code()+"/questions/show","POST",{});}
|
||||||
|
function mixAnswers(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/answers/mix","POST",{});}
|
||||||
|
function calcScores(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/scores/calculate","POST",{});}
|
||||||
|
function showScoreboard(){return api("/lobby/sessions/"+code()+"/scoreboard","GET",null);}
|
||||||
|
function nextRound(){return api("/lobby/sessions/"+code()+"/rounds/next","POST",{});}
|
||||||
|
function finishGame(){return api("/lobby/sessions/"+code()+"/finish","POST",{});}
|
||||||
|
</script>
|
||||||
|
</body></html>
|
||||||
26
lobby/templates/lobby/player_screen.html
Normal file
26
lobby/templates/lobby/player_screen.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="da"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>WPP Player</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Player panel (MVP)</h1>
|
||||||
|
<input id="code" placeholder="Sessionkode">
|
||||||
|
<input id="nickname" placeholder="Nickname">
|
||||||
|
<button onclick="joinSession()">1) Join</button>
|
||||||
|
<input id="playerId" placeholder="Player id">
|
||||||
|
<input id="roundQuestionId" placeholder="Round question id">
|
||||||
|
<input id="lieText" placeholder="Din løgn">
|
||||||
|
<button onclick="submitLie()">2) Submit løgn</button>
|
||||||
|
<input id="guessText" placeholder="Dit gæt">
|
||||||
|
<button onclick="submitGuess()">3) Submit gæt</button>
|
||||||
|
<button onclick="sessionDetail()">Opdater session-status</button>
|
||||||
|
<pre id="out">Klar.</pre>
|
||||||
|
<script>
|
||||||
|
function code(){return document.getElementById("code").value.trim().toUpperCase();}
|
||||||
|
function pid(){return document.getElementById("playerId").value.trim();}
|
||||||
|
function rq(){return document.getElementById("roundQuestionId").value.trim();}
|
||||||
|
async function api(path,method,payload){var o={method:method||"GET",headers:{"Accept":"application/json"}};if(payload!==null){o.headers["Content-Type"]="application/json";o.body=JSON.stringify(payload);}var r=await fetch(path,o);var d=await r.json().catch(function(){return {};});document.getElementById("out").textContent=JSON.stringify({status:r.status,data:d},null,2);if(d.player&&d.player.id){document.getElementById("playerId").value=d.player.id;}if(d.round_question&&d.round_question.id){document.getElementById("roundQuestionId").value=d.round_question.id;}return d;}
|
||||||
|
function joinSession(){return api("/lobby/sessions/join","POST",{code:code(),nickname:document.getElementById("nickname").value.trim()});}
|
||||||
|
function sessionDetail(){return api("/lobby/sessions/"+code(),"GET",null);}
|
||||||
|
function submitLie(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/lies/submit","POST",{player_id:parseInt(pid(),10),text:document.getElementById("lieText").value});}
|
||||||
|
function submitGuess(){return api("/lobby/sessions/"+code()+"/questions/"+rq()+"/guesses/submit","POST",{player_id:parseInt(pid(),10),selected_text:document.getElementById("guessText").value});}
|
||||||
|
</script>
|
||||||
|
</body></html>
|
||||||
@@ -664,3 +664,50 @@ class RevealRoundFlowTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
self.assertEqual(response.json()["error"], "Next round can only start from reveal phase")
|
self.assertEqual(response.json()["error"], "Next round can only start from reveal phase")
|
||||||
|
|
||||||
|
class UiScreenTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.host = User.objects.create_user(username="host_ui", password="secret123")
|
||||||
|
|
||||||
|
def test_host_screen_requires_login(self):
|
||||||
|
response = self.client.get(reverse("lobby:host_screen"))
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
def test_host_screen_renders_for_logged_in_user(self):
|
||||||
|
self.client.login(username="host_ui", password="secret123")
|
||||||
|
response = self.client.get(reverse("lobby:host_screen"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Host panel")
|
||||||
|
|
||||||
|
def test_player_screen_is_public(self):
|
||||||
|
response = self.client.get(reverse("lobby:player_screen"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Player panel")
|
||||||
|
|
||||||
|
|
||||||
|
class SessionDetailRoundQuestionTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.host = User.objects.create_user(username="host_detail", password="secret123")
|
||||||
|
self.session = GameSession.objects.create(host=self.host, code="ABCDE1", status=GameSession.Status.LIE)
|
||||||
|
self.category = Category.objects.create(name="Historie", slug="historie-2", is_active=True)
|
||||||
|
self.question = Question.objects.create(
|
||||||
|
category=self.category,
|
||||||
|
prompt="Hvem opfandt pæren?",
|
||||||
|
correct_answer="Edison",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_session_detail_includes_current_round_question_when_available(self):
|
||||||
|
round_question = RoundQuestion.objects.create(
|
||||||
|
session=self.session,
|
||||||
|
round_number=1,
|
||||||
|
question=self.question,
|
||||||
|
correct_answer=self.question.correct_answer,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("lobby:session_detail", kwargs={"code": self.session.code}))
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
payload = response.json()
|
||||||
|
self.assertEqual(payload["round_question"]["id"], round_question.id)
|
||||||
|
self.assertEqual(payload["round_question"]["prompt"], self.question.prompt)
|
||||||
|
|||||||
14
lobby/ui_views.py
Normal file
14
lobby/ui_views.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from fupogfakta.models import Category
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def host_screen(request):
|
||||||
|
categories = Category.objects.filter(is_active=True).order_by("name")
|
||||||
|
return render(request, "lobby/host_screen.html", {"categories": categories})
|
||||||
|
|
||||||
|
|
||||||
|
def player_screen(request):
|
||||||
|
return render(request, "lobby/player_screen.html")
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import ui_views, views
|
||||||
|
|
||||||
app_name = "lobby"
|
app_name = "lobby"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
path("ui/host", ui_views.host_screen, name="host_screen"),
|
||||||
|
path("ui/player", ui_views.player_screen, name="player_screen"),
|
||||||
path("sessions/create", views.create_session, name="create_session"),
|
path("sessions/create", views.create_session, name="create_session"),
|
||||||
path("sessions/join", views.join_session, name="join_session"),
|
path("sessions/join", views.join_session, name="join_session"),
|
||||||
path("sessions/<str:code>", views.session_detail, name="session_detail"),
|
path("sessions/<str:code>", views.session_detail, name="session_detail"),
|
||||||
@@ -34,4 +36,3 @@ urlpatterns = [
|
|||||||
path("sessions/<str:code>/finish", views.finish_game, name="finish_game"),
|
path("sessions/<str:code>/finish", views.finish_game, name="finish_game"),
|
||||||
path("sessions/<str:code>/rounds/next", views.start_next_round, name="start_next_round"),
|
path("sessions/<str:code>/rounds/next", views.start_next_round, name="start_next_round"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,22 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_round_question = (
|
||||||
|
RoundQuestion.objects.filter(session=session, round_number=session.current_round)
|
||||||
|
.select_related("question")
|
||||||
|
.order_by("-id")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
round_question_payload = None
|
||||||
|
if current_round_question:
|
||||||
|
round_question_payload = {
|
||||||
|
"id": current_round_question.id,
|
||||||
|
"round_number": current_round_question.round_number,
|
||||||
|
"prompt": current_round_question.question.prompt,
|
||||||
|
"shown_at": current_round_question.shown_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{
|
{
|
||||||
"session": {
|
"session": {
|
||||||
@@ -143,6 +159,7 @@ def session_detail(request: HttpRequest, code: str) -> JsonResponse:
|
|||||||
"players_count": len(players),
|
"players_count": len(players),
|
||||||
},
|
},
|
||||||
"players": players,
|
"players": players,
|
||||||
|
"round_question": round_question_payload,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user