feat: improve draft admin UI, draft state sync, and styling

Major refactor of Draft admin and participant Websocket state sync
Use consistent state dict serialization in DraftStateManager (to_dict, dict-like access, etc.)
Always include up-to-date participants and draft status in sync payloads
Draft phase/order summary now sent as objects instead of calling .get_summary()
UI/UX updates:
Updated DraftAdmin.jsx:
Connects DraftParticipant panel for real-time participant state
Centralizes phase advance, bidding, and sync controls
Moves phase selector into a dedicated panel
Refine markup/extends in room_admin.dj.html (use block body, fix root data attribute)
Minor fixes to DraftCountdownClock.jsx to robustly handle NaN time
CSS/layout:
Refactor .draft-participant styling to .wrapper within #draft-participant-root and #draft-admin-root for better responsive layout and code clarity
Server code:
Simplify draft consumer/manager state interaction, drop unused cache keys, update order determination and phase management, and ensure DRY status object responses
Small code style and consistency cleanups
Misc:
Add debugpy launch task in code-workspace and clean workspace JSON (style/consistency)
Minor formatting and error handling improvements
This commit is contained in:
2025-08-15 11:06:27 -05:00
parent 71f0f01abc
commit 9ddc8663a9
9 changed files with 193 additions and 167 deletions

View File

@@ -7,6 +7,18 @@
"launch": { "launch": {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "Debug current file with debugpy",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false,
"args": [],
"env": {
"PYTHONPATH": "${workspaceFolder}"
}
},
{ {
"name": "Run Django Server", "name": "Run Django Server",
"type": "debugpy", "type": "debugpy",
@@ -22,7 +34,7 @@
"type": "debugpy", "type": "debugpy",
"request": "launch", "request": "launch",
"module": "uvicorn", "module": "uvicorn",
"args": ["boxofficefantasy_project.asgi:application", "--reload",], "args": ["boxofficefantasy_project.asgi:application", "--reload"],
"django": true, "django": true,
"console": "integratedTerminal", "console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env" "envFile": "${workspaceFolder}/.env"
@@ -32,10 +44,7 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"runtimeExecutable": "npm", "runtimeExecutable": "npm",
"args": [ "args": ["run", "dev"],
"run",
"dev"
],
"cwd": "${workspaceFolder}/frontend", "cwd": "${workspaceFolder}/frontend",
"console": "integratedTerminal", "console": "integratedTerminal",
"internalConsoleOptions": "neverOpen" "internalConsoleOptions": "neverOpen"
@@ -62,7 +71,11 @@
"compounds": [ "compounds": [
{ {
"name": "Django + Chrome + Webpack", "name": "Django + Chrome + Webpack",
"configurations": ["Run Django Server", "Launch Chrome", "Start Webpack Dev Server"], "configurations": [
"Run Django Server",
"Launch Chrome",
"Start Webpack Dev Server"
],
"type": "compound" "type": "compound"
} }
] ]
@@ -152,7 +165,7 @@
"editor.defaultFormatter": "ms-python.black-formatter" "editor.defaultFormatter": "ms-python.black-formatter"
}, },
"[django-html]": { "[django-html]": {
"editor.defaultFormatter": "monosans.djlint", "editor.defaultFormatter": "monosans.djlint"
}, },
"emmet.includeLanguages": { "emmet.includeLanguages": {
"django-html": "html" "django-html": "html"
@@ -161,15 +174,13 @@
"*.dj.html": "django-html" "*.dj.html": "django-html"
}, },
"files.exclude": { "files.exclude": {
"**/__pycache__":true, "**/__pycache__": true,
".venv":false ".venv": false
}, },
"auto-close-tag.activationOnLanguage": [ "auto-close-tag.activationOnLanguage": ["django-html"],
"django-html"
],
"terminal.integrated.env.osx": { "terminal.integrated.env.osx": {
"VSCODE_HISTFILE":"${workspaceFolder}/.venv/.term_history" "VSCODE_HISTFILE": "${workspaceFolder}/.venv/.term_history"
}, }
// "html.autoClosingTags": true, // "html.autoClosingTags": true,
} }
} }

View File

@@ -19,7 +19,7 @@
<script src="https://cdn.datatables.net/2.3.2/js/dataTables.js"></script> <script src="https://cdn.datatables.net/2.3.2/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.3.2/js/dataTables.bootstrap5.js"></script> <script src="https://cdn.datatables.net/2.3.2/js/dataTables.bootstrap5.js"></script>
</head> </head>
<body> <body class="d-flex flex-column vh-100">
<nav class="navbar justify-content-ends pe-2"> <nav class="navbar justify-content-ends pe-2">
<div> <div>
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="/">
@@ -50,18 +50,22 @@
<li class="breadcrumb-item {% if forloop.last %}active{% endif %}" <li class="breadcrumb-item {% if forloop.last %}active{% endif %}"
aria-current="page"> aria-current="page">
{% if not forloop.last %} {% if not forloop.last %}
<a href="{{ crumb.url }}">{{ crumb.label }}</a>{% else %}{{ crumb.label }}{% endif %} <a href="{{ crumb.url }}">{{ crumb.label }}</a>
</li> {% else %}
{% endfor %} {{ crumb.label }}
</ol> {% endif %}
{% endif %} </li>
</nav> {% endfor %}
{% endblock breadcrumbs %} </ol>
{% block content %}{% endblock content %} {% endif %}
{% endblock body %} </nav>
{% endblock breadcrumbs %}
{% block content %}
{% endblock content %}
</main> </main>
<footer class="text-muted text-center mt-5"> {% endblock body %}
<small>&copy; Sack Lunch</small> <footer class="text-muted text-center mt-5">
</footer> <small>&copy; Sack Lunch</small>
</body> </footer>
</html> </body>
</html>

View File

@@ -4,7 +4,6 @@ from django.core.exceptions import PermissionDenied
from boxofficefantasy.models import League, Season from boxofficefantasy.models import League, Season
from boxofficefantasy.views import parse_season_slug from boxofficefantasy.views import parse_season_slug
from draft.models import DraftSession, DraftSessionParticipant from draft.models import DraftSession, DraftSessionParticipant
from django.core.cache import cache
import asyncio import asyncio
from django.contrib.auth.models import User from django.contrib.auth.models import User
from draft.constants import ( from draft.constants import (
@@ -12,7 +11,7 @@ from draft.constants import (
DraftPhase, DraftPhase,
DraftGroupChannelNames, DraftGroupChannelNames,
) )
from draft.state import DraftCacheKeys, DraftStateManager from draft.state import DraftStateManager
from typing import Any from typing import Any
import logging import logging
@@ -24,7 +23,6 @@ import random
class DraftConsumerBase(AsyncJsonWebsocketConsumer): class DraftConsumerBase(AsyncJsonWebsocketConsumer):
group_names: DraftGroupChannelNames group_names: DraftGroupChannelNames
cache_keys: DraftCacheKeys
draft_state: DraftStateManager draft_state: DraftStateManager
user: User user: User
@@ -39,7 +37,6 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
) )
self.group_names = DraftGroupChannelNames(draft_hashid) self.group_names = DraftGroupChannelNames(draft_hashid)
self.cache_keys = DraftCacheKeys(draft_hashid)
self.draft_state = DraftStateManager(self.draft_session) self.draft_state = DraftStateManager(self.draft_session)
self.user = self.scope["user"] self.user = self.scope["user"]
@@ -49,8 +46,8 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
{ {
"type": "direct.message", "type": "direct.message",
"subtype": DraftMessage.PARTICIPANT_JOIN_REJECT, "subtype": DraftMessage.PARTICIPANT_JOIN_REJECT,
"payload":{"current_user": self.user.username} "payload": {"current_user": self.user.username},
} },
) )
await self.close() await self.close()
await self.channel_layer.group_send( await self.channel_layer.group_send(
@@ -58,7 +55,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
{ {
"type": "broadcast.admin", "type": "broadcast.admin",
"subtype": DraftMessage.PARTICIPANT_JOIN_REJECT, "subtype": DraftMessage.PARTICIPANT_JOIN_REJECT,
"payload":{"user": self.user.username} "payload": {"user": self.user.username},
}, },
) )
return return
@@ -80,7 +77,13 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
{ {
"type": "direct.message", "type": "direct.message",
"subtype": DraftMessage.STATUS_SYNC_INFORM, "subtype": DraftMessage.STATUS_SYNC_INFORM,
"payload": self.get_draft_status(), "payload": {
**self.draft_state,
"user": self.user.username,
"participants": [
user.username for user in self.draft_participants
],
},
}, },
) )
await self.channel_layer.send( await self.channel_layer.send(
@@ -121,11 +124,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
# === Methods === # === Methods ===
def get_draft_status(self) -> dict[str, Any]: def get_draft_status(self) -> dict[str, Any]:
return { return
**self.draft_state.get_summary(),
"user": self.user.username,
"participants": [user.username for user in self.draft_participants],
}
# === DB Access === # === DB Access ===
@database_sync_to_async @database_sync_to_async
@@ -133,8 +132,8 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
draft_session_id = DraftSession.decode_id(draft_session_id_hashed) draft_session_id = DraftSession.decode_id(draft_session_id_hashed)
if draft_session_id: if draft_session_id:
draft_session = DraftSession.objects.select_related( draft_session = DraftSession.objects.select_related(
"season", "season__league", "settings" "season", "season__league", "settings",
).get(pk=draft_session_id) ).prefetch_related("participants").get(pk=draft_session_id)
else: else:
raise Exception() raise Exception()
@@ -178,13 +177,13 @@ class DraftAdminConsumer(DraftConsumerBase):
{ {
"type": "broadcast.session", "type": "broadcast.session",
"subtype": DraftMessage.DRAFT_INDEX_ADVANCE_CONFIRM, "subtype": DraftMessage.DRAFT_INDEX_ADVANCE_CONFIRM,
"payload": self.draft_state.get_summary(), "payload": {**self.draft_state},
}, },
) )
if event_type == DraftMessage.NOMINATION_SUBMIT_REQUEST: if event_type == DraftMessage.NOMINATION_SUBMIT_REQUEST:
movie_id = content.get('payload',{}).get('movie_id') movie_id = content.get("payload", {}).get("movie_id")
user = content.get('payload',{}).get('user') user = content.get("payload", {}).get("user")
self.draft_state.start_nomination(movie_id) self.draft_state.start_nomination(movie_id)
await self.channel_layer.group_send( await self.channel_layer.group_send(
self.group_names.session, self.group_names.session,
@@ -192,25 +191,23 @@ class DraftAdminConsumer(DraftConsumerBase):
"type": "broadcast.session", "type": "broadcast.session",
"subtype": DraftMessage.NOMINATION_CONFIRM, "subtype": DraftMessage.NOMINATION_CONFIRM,
"payload": { "payload": {
"current_movie": self.draft_state.get_summary()['current_movie'], "current_movie": self.draft_state[
"nominating_participant": user "current_movie"
} ],
} "nominating_participant": user,
},
},
) )
if event_type == DraftMessage.BID_START_REQUEST: if event_type == DraftMessage.BID_START_REQUEST:
self.draft_state.start_timer()
self.draft_state.start_bidding()
await self.channel_layer.group_send( await self.channel_layer.group_send(
self.group_names.session, self.group_names.session,
{ {
"type": "broadcast.session", "type": "broadcast.session",
"subtype": DraftMessage.BID_START_INFORM, "subtype": DraftMessage.BID_START_INFORM,
"payload": { "payload": self.get_draft_status(),
"current_movie": self.draft_state.get_summary()['current_movie'], },
"bidding_duration": self.draft_state.settings.bidding_duration,
"bidding_timer_end": self.draft_state.get_timer_end(),
"bidding_timer_start": self.draft_state.get_timer_start()
}
}
) )
def should_accept_user(self): def should_accept_user(self):
@@ -229,9 +226,7 @@ class DraftAdminConsumer(DraftConsumerBase):
) )
async def determine_draft_order(self): async def determine_draft_order(self):
draft_order = self.draft_state.determine_draft_order(self.draft_participants) self.draft_state.determine_draft_order()
self.draft_state.draft_index = 0
await self.set_draft_phase(DraftPhase.DETERMINE_ORDER)
next_picks = self.draft_state.next_picks(include_current=True) next_picks = self.draft_state.next_picks(include_current=True)
await self.channel_layer.group_send( await self.channel_layer.group_send(
@@ -239,12 +234,7 @@ class DraftAdminConsumer(DraftConsumerBase):
{ {
"type": "broadcast.session", "type": "broadcast.session",
"subtype": DraftMessage.ORDER_DETERMINE_CONFIRM, "subtype": DraftMessage.ORDER_DETERMINE_CONFIRM,
"payload": { "payload": {**self.draft_state},
"draft_order": draft_order,
"draft_index": self.draft_state.draft_index,
"current_pick": next_picks[0],
"next_picks": next_picks[1:]
},
}, },
) )
@@ -311,7 +301,7 @@ class DraftParticipantConsumer(DraftConsumerBase):
async def receive_json(self, content): async def receive_json(self, content):
await super().receive_json(content) await super().receive_json(content)
event_type = content.get('type') event_type = content.get("type")
if event_type == DraftMessage.NOMINATION_SUBMIT_REQUEST: if event_type == DraftMessage.NOMINATION_SUBMIT_REQUEST:
await self.channel_layer.group_send( await self.channel_layer.group_send(
self.group_names.admin, self.group_names.admin,
@@ -319,13 +309,12 @@ class DraftParticipantConsumer(DraftConsumerBase):
"type": "broadcast.admin", "type": "broadcast.admin",
"subtype": event_type, "subtype": event_type,
"payload": { "payload": {
"movie_id": content.get('payload',{}).get('id'), "movie_id": content.get("payload", {}).get("id"),
"user": content.get('payload',{}).get('user') "user": content.get("payload", {}).get("user"),
} },
} },
) )
# === Broadcast handlers === # === Broadcast handlers ===
async def broadcast_participant(self, event): async def broadcast_participant(self, event):

View File

@@ -10,6 +10,10 @@ from dataclasses import dataclass
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple
import random import random
class DraftStateException(Exception):
"""Raised when an action is not allowed due to the current draft state or phase."""
pass
class DraftCacheKeys: class DraftCacheKeys:
def __init__(self, id): def __init__(self, id):
self.prefix = f"draft:{id}" self.prefix = f"draft:{id}"
@@ -79,59 +83,62 @@ class DraftStateManager:
def __init__(self, session: DraftSession): def __init__(self, session: DraftSession):
self.session_id = session.hashid self.session_id = session.hashid
self.cache = cache self.cache = cache
self.keys = DraftCacheKeys(self.session_id) self.cache_keys = DraftCacheKeys(self.session_id)
self._initial_phase = self.cache.get(self.keys.phase, DraftPhase.WAITING.value) self._initial_phase = self.cache.get(self.cache_keys.phase, DraftPhase.WAITING.value)
self.settings = session.settings self.settings = session.settings
self.participants = list(session.participants.all())
# === Phase Management === # === Phase Management ===
@property @property
def phase(self) -> str: def phase(self) -> str:
return str(self.cache.get(self.keys.phase, self._initial_phase)) return str(self.cache.get(self.cache_keys.phase, self._initial_phase))
@phase.setter @phase.setter
def phase(self, new_phase: DraftPhase): def phase(self, new_phase: DraftPhase):
self.cache.set(self.keys.phase, new_phase.value) self.cache.set(self.cache_keys.phase, new_phase.value)
# === Connected Users === # === Connected Users ===
@property @property
def connected_participants(self) -> list[str]: def connected_participants(self) -> list[str]:
return json.loads(self.cache.get(self.keys.connected_users) or "[]") return json.loads(self.cache.get(self.cache_keys.connected_users) or "[]")
def connect_participant(self, username: str): def connect_participant(self, username: str):
users = set(self.connected_participants) users = set(self.connected_participants)
users.add(username) users.add(username)
self.cache.set(self.keys.connected_users, json.dumps(list(users))) self.cache.set(self.cache_keys.connected_users, json.dumps(list(users)))
def disconnect_participant(self, username: str): def disconnect_participant(self, username: str):
users = set(self.connected_participants) users = set(self.connected_participants)
users.discard(username) users.discard(username)
self.cache.set(self.keys.connected_users, json.dumps(list(users))) self.cache.set(self.cache_keys.connected_users, json.dumps(list(users)))
# === Draft Order === # === Draft Order ===
@property @property
def draft_order(self): def draft_order(self):
return json.loads(self.cache.get(self.keys.draft_order,"[]")) return json.loads(self.cache.get(self.cache_keys.draft_order,"[]"))
@draft_order.setter @draft_order.setter
def draft_order(self, draft_order: list[str]): def draft_order(self, draft_order: list[str]):
if not isinstance(draft_order, list): if not isinstance(draft_order, list):
return return
self.cache.set(self.keys.draft_order,json.dumps(draft_order)) self.cache.set(self.cache_keys.draft_order,json.dumps(draft_order))
def determine_draft_order(self, users: list[User]): def determine_draft_order(self) -> List[User]:
self.phase = DraftPhase.DETERMINE_ORDER
self.draft_index = 0
draft_order = random.sample( draft_order = random.sample(
users, len(users) self.participants, len(self.participants)
) )
self.draft_order = [user.username for user in draft_order] self.draft_order = [user.username for user in draft_order]
return self.draft_order return self.draft_order
@property @property
def draft_index(self): def draft_index(self):
return self.cache.get(self.keys.draft_index,0) return self.cache.get(self.cache_keys.draft_index,0)
@draft_index.setter @draft_index.setter
def draft_index(self, draft_index: int): def draft_index(self, draft_index: int):
self.cache.set(self.keys.draft_index, int(draft_index)) self.cache.set(self.cache_keys.draft_index, int(draft_index))
def draft_index_advance(self, n: int = 1): def draft_index_advance(self, n: int = 1):
self.draft_index += n self.draft_index += n
@@ -171,49 +178,67 @@ class DraftStateManager:
# === Current Nomination / Bid === # === Current Nomination / Bid ===
def start_nomination(self, movie_id: int): def start_nomination(self, movie_id: int):
self.cache.set(self.keys.current_movie, movie_id) self.cache.set(self.cache_keys.current_movie, movie_id)
self.cache.delete(self.keys.bids) self.cache.delete(self.cache_keys.bids)
def place_bid(self, user_id: int, amount: int): def place_bid(self, user_id: int, amount: int):
bids = self.get_bids() bids = self.get_bids()
bids[user_id] = amount bids[user_id] = amount
self.cache.set(self.keys.bids, json.dumps(bids)) self.cache.set(self.cache_keys.bids, json.dumps(bids))
def get_bids(self) -> dict: def get_bids(self) -> dict:
return json.loads(self.cache.get(self.keys.bids) or "{}") return json.loads(self.cache.get(self.cache_keys.bids) or "{}")
def current_movie(self) -> Movie | None: def current_movie(self) -> Movie | None:
movie_id = self.cache.get(self.keys.current_movie) movie_id = self.cache.get(self.cache_keys.current_movie)
return Movie.objects.filter(pk=movie_id).first() if movie_id else None return Movie.objects.filter(pk=movie_id).first() if movie_id else None
def start_timer(self): def start_bidding(self):
seconds = self.settings.bidding_duration seconds = self.settings.bidding_duration
start_time = time.time() start_time = time.time()
end_time = start_time + seconds end_time = start_time + seconds
self.cache.set(self.keys.bid_timer_end, end_time) self.cache.set(self.cache_keys.bid_timer_end, end_time)
self.cache.set(self.keys.bid_timer_start, start_time) self.cache.set(self.cache_keys.bid_timer_start, start_time)
def get_timer_end(self) -> str | None: def get_timer_end(self) -> str | None:
return self.cache.get(self.keys.bid_timer_end) return self.cache.get(self.cache_keys.bid_timer_end)
def get_timer_start(self) -> str | None: def get_timer_start(self) -> str | None:
return self.cache.get(self.keys.bid_timer_start) return self.cache.get(self.cache_keys.bid_timer_start)
# === Sync Snapshot === # === Sync Snapshot ===
def get_summary(self) -> dict: def to_dict(self) -> dict:
picks = self.next_picks(include_current=True) picks = self.next_picks(include_current=True)
return { return {
"phase": self.phase, "phase": self.phase,
"draft_order": self.draft_order, "draft_order": self.draft_order,
"draft_index": self.draft_index, "draft_index": self.draft_index,
"connected_participants": self.connected_participants, "connected_participants": self.connected_participants,
"current_movie": self.cache.get(self.keys.current_movie), "current_movie": self.cache.get(self.cache_keys.current_movie),
# "bids": self.get_bids(), # "bids": self.get_bids(),
"bidding_timer_end": self.get_timer_end(), "bidding_timer_end": self.get_timer_end(),
"bidding_timer_start": self.get_timer_start(), "bidding_timer_start": self.get_timer_start(),
"current_pick": picks[0] if picks else None, "current_pick": picks[0] if picks else None,
"next_picks": picks[1:] if picks else [] "next_picks": picks[1:] if picks else []
} }
# def __dict__(self):
# return self.get_summary()
def keys(self):
# return an iterable of keys
return self.to_dict().keys()
def __getitem__(self, key):
return self.to_dict()[key]
def __iter__(self):
# used for `dict(self.draft_state)` and iteration
return iter(self.to_dict())
def __len__(self):
return len(self.to_dict())
OrderType = Literal["snake", "linear"] OrderType = Literal["snake", "linear"]
def _round_and_pick(overall: int, n: int) -> Tuple[int, int]: def _round_and_pick(overall: int, n: int) -> Tuple[int, int]:

View File

@@ -1,10 +1,8 @@
{% extends "base.dj.html" %} {% extends "base.dj.html" %}
{% block content %} {% block body %}
<h1>Draft Room: {{ league.name }} {{ season.label }} {{ season.year }}</h1>
{% load static %} {% load static %}
<script> <script>
window.draftSessionId = "{{ draft_id_hashed }}" window.draftSessionId = "{{ draft_id_hashed }}"
</script> </script>
<div id="draft-admin-root" data-draft-hid="{{ draft_id_hashed }}"></div> <div id="draft-admin-root" data-draft-id="{{ draft_id_hashed }}"></div>
{% endblock body %}
{% endblock %}

View File

@@ -7,6 +7,7 @@ import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '.
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "../common/utils.js" import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "../common/utils.js"
import { DraftMoviePool } from "../common/DraftMoviePool.jsx" import { DraftMoviePool } from "../common/DraftMoviePool.jsx"
import { DraftCountdownClock } from "../common/DraftCountdownClock.jsx" import { DraftCountdownClock } from "../common/DraftCountdownClock.jsx"
import { DraftParticipant } from "../participant/DraftParticipant.jsx";
import { jsxs } from "react/jsx-runtime"; import { jsxs } from "react/jsx-runtime";
@@ -14,7 +15,6 @@ import { jsxs } from "react/jsx-runtime";
const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) => { const DraftPhaseDisplay = ({ draftPhase, nextPhaseHandler, prevPhaseHandler }) => {
return ( return (
<div className="draft-phase-container"> <div className="draft-phase-container">
<label>Phase</label>
<div className="d-flex"> <div className="d-flex">
<div className="change-phase"><button onClick={prevPhaseHandler}><i className="bi bi-chevron-left"></i></button></div> <div className="change-phase"><button onClick={prevPhaseHandler}><i className="bi bi-chevron-left"></i></button></div>
<ol> <ol>
@@ -46,19 +46,19 @@ export const DraftAdmin = ({ draftSessionId }) => {
}) })
}, []) }, [])
useEffect(()=>{ useEffect(() => {
if (!socket) return; if (!socket) return;
const openHandler = (event)=>{ const openHandler = (event) => {
console.log('Websocket Opened') console.log('Websocket Opened')
} }
const closeHandler = (event)=>{ const closeHandler = (event) => {
console.log('Websocket Closed') console.log('Websocket Closed')
} }
socket.addEventListener('open', openHandler ); socket.addEventListener('open', openHandler);
socket.addEventListener('close', closeHandler ); socket.addEventListener('close', closeHandler);
return ()=>{ return () => {
socket.removeEventListener('open', openHandler ); socket.removeEventListener('open', openHandler);
socket.removeEventListener('close', closeHandler ); socket.removeEventListener('close', closeHandler);
} }
}, [socket]) }, [socket])
@@ -67,7 +67,7 @@ export const DraftAdmin = ({ draftSessionId }) => {
const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState) const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState)
const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser) const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser)
const handleNominationRequest = (event)=> { const handleNominationRequest = (event) => {
const message = JSON.parse(event.data) const message = JSON.parse(event.data)
const { type, payload } = message; const { type, payload } = message;
if (type == DraftMessage.NOMINATION_SUBMIT_REQUEST) { if (type == DraftMessage.NOMINATION_SUBMIT_REQUEST) {
@@ -79,15 +79,15 @@ export const DraftAdmin = ({ draftSessionId }) => {
)) ))
} }
} }
socket.addEventListener('message', draftStatusMessageHandler ); socket.addEventListener('message', draftStatusMessageHandler);
socket.addEventListener('message', userIdentifyMessageHandler ); socket.addEventListener('message', userIdentifyMessageHandler);
socket.addEventListener('message', handleNominationRequest ); socket.addEventListener('message', handleNominationRequest);
return () => { return () => {
socket.removeEventListener('message', draftStatusMessageHandler) socket.removeEventListener('message', draftStatusMessageHandler)
socket.removeEventListener('message', userIdentifyMessageHandler ); socket.removeEventListener('message', userIdentifyMessageHandler);
socket.removeEventListener('message', handleNominationRequest ); socket.removeEventListener('message', handleNominationRequest);
}; };
}, [socket]); }, [socket]);
@@ -134,36 +134,31 @@ export const DraftAdmin = ({ draftSessionId }) => {
const handleStartBidding = () => { const handleStartBidding = () => {
socket.send( socket.send(
JSON.stringify( JSON.stringify(
{type: DraftMessage.BID_START_REQUEST} { type: DraftMessage.BID_START_REQUEST }
) )
) )
} }
return ( return (
<div className="container draft-panel admin"> <div className="">
<div className="d-flex justify-content-between border-bottom mb-2 p-1"> <div className="">
<h3>Draft Panel</h3> <DraftParticipant draftSessionId={draftSessionId}></DraftParticipant>
<div className="d-flex gap-1"> <div className="d-flex justify-content-between border-bottom mb-2 p-1">
<WebSocketStatus socket={socket} />
<button onClick={() => handleRequestDraftSummary()} className="btn btn-small btn-light">
<i className="bi bi-arrow-clockwise"></i>
</button>
</div> </div>
</div> </div>
<ParticipantList <section className="d-flex justify-content-center mt-3">
currentUser = {currentUser} <button onClick={() => handleRequestDraftSummary()} className="btn btn-small btn-light mx-1">
draftState={draftState} <i className="bi bi-arrow-clockwise"></i>
draftDetails={draftDetails} </button>
isAdmin={true} <button onClick={handleAdvanceDraft} className="btn btn-primary mx-1">Advance Index</button>
/> <button onClick={handleStartBidding} className="btn btn-primary mx-1">Start Bidding</button>
<div className="d-flex gap-1 m-1"> </section>
<button onClick={handleAdvanceDraft} className="btn btn-primary">Advance Draft</button>
<button onClick={handleStartBidding} className="btn btn-primary">Start Bidding</button> <div class="d-flex justify-content-center mt-3">
<DraftPhaseDisplay draftPhase={draftState.phase} nextPhaseHandler={() => { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}></DraftPhaseDisplay>
</div> </div>
<DraftMoviePool draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
<DraftPhaseDisplay draftPhase={draftState.phase} nextPhaseHandler={() => { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}></DraftPhaseDisplay>
<DraftCountdownClock endTime={draftState.bidding_timer_end}></DraftCountdownClock>
</div> </div>
); );
}; };

View File

@@ -27,7 +27,7 @@ export function DraftCountdownClock({ endTime, onFinish }) {
return ( return (
<div className="countdown-clock"> <div className="countdown-clock">
<span> <span>
{minutes}:{pad(secs)} {!isNaN(minutes) && !isNaN(secs) ? `${minutes}:${pad(secs)}` : "0:00"}
</span> </span>
</div> </div>
); );

View File

@@ -85,7 +85,7 @@ export const DraftParticipant = ({ draftSessionId }) => {
}, [socket]); }, [socket]);
return ( return (
<div className="draft-participant"> <div className="wrapper">
<section class="panel draft-live"> <section class="panel draft-live">
<header class="panel-header d-flex justify-content-between align-items-center"> <header class="panel-header d-flex justify-content-between align-items-center">
<h2 class="panel-title">Draft Live</h2> <h2 class="panel-title">Draft Live</h2>

View File

@@ -124,30 +124,34 @@
} }
} }
.draft-participant { #draft-participant-root,
display: flex; #draft-admin-root {
flex-wrap: wrap; /* allow panels to wrap */ @extend .flex-grow-1;
gap: 1rem; /* space between panels */ .wrapper:first-child {
justify-content: center; /* center the panels horizontally */ display: flex;
flex-wrap: wrap; /* allow panels to wrap */
gap: 1rem; /* space between panels */
justify-content: center; /* center the panels horizontally */
.panel { .panel {
flex: 1 1 350px; /* grow/shrink, base width */ flex: 1 1 350px; /* grow/shrink, base width */
max-width: 450px; /* never go beyond this */ max-width: 450px; /* never go beyond this */
min-width: 300px; /* keeps them from getting too small */ min-width: 300px; /* keeps them from getting too small */
} }
.panel.draft-live { .panel.draft-live {
.draft-live-state-container { .draft-live-state-container {
@extend .d-flex; @extend .d-flex;
.countdown-clock { .countdown-clock {
@extend .fs-1; @extend .fs-1;
@extend .fw-bold; @extend .fw-bold;
@extend .col; @extend .col;
@extend .align-content-center; @extend .align-content-center;
@extend .text-center; @extend .text-center;
} }
.pick-description{ .pick-description {
@extend .col; @extend .col;
}
} }
} }
} }
} }