From baddca8d50cadc2bec45e82727a831b6aefa13f1 Mon Sep 17 00:00:00 2001 From: Anthony Correa Date: Sun, 24 Aug 2025 12:06:41 -0500 Subject: [PATCH] Refactor draft app with improved state management and components * Rename WebSocket message types for better organization * Improve state handling with dedicated methods like broadcast_state * Restructure frontend components and remove unused code --- data/cache_concept.py | 75 +++++++ data/draft_cache.json | Bin 0 -> 162 bytes draft/constants.py | 4 +- draft/consumers.py | 205 ++++++++++-------- draft/state.py | 7 +- draft/templates/draft/room.dj.html | 4 + draft/urls.py | 3 +- draft/views.py | 20 +- draft_cache.json | 21 ++ .../DraftAdmin.jsx => DraftAdminBar.jsx} | 29 +-- ...raftParticipant.jsx => DraftDashboard.jsx} | 171 ++++++++------- frontend/src/apps/draft/DraftDebug.jsx | 4 +- .../DraftCountdownClock.jsx | 10 +- .../{common => components}/DraftMoviePool.jsx | 2 +- .../ParticipantList.jsx | 2 +- .../WebSocketContext.jsx | 0 .../WebSocketStatus.jsx | 0 frontend/src/apps/draft/constants.js | 5 +- frontend/src/apps/draft/{common => }/utils.js | 30 +-- frontend/src/index.js | 12 +- frontend/src/scss/styles.scss | 58 +++-- scripts/generate_js_constants.py | 0 22 files changed, 387 insertions(+), 275 deletions(-) create mode 100644 data/cache_concept.py create mode 100644 data/draft_cache.json create mode 100644 draft_cache.json rename frontend/src/apps/draft/{admin/DraftAdmin.jsx => DraftAdminBar.jsx} (84%) rename frontend/src/apps/draft/{participant/DraftParticipant.jsx => DraftDashboard.jsx} (50%) rename frontend/src/apps/draft/{common => components}/DraftCountdownClock.jsx (73%) rename frontend/src/apps/draft/{common => components}/DraftMoviePool.jsx (93%) rename frontend/src/apps/draft/{common => components}/ParticipantList.jsx (94%) rename frontend/src/apps/draft/{common => components}/WebSocketContext.jsx (100%) rename frontend/src/apps/draft/{common => components}/WebSocketStatus.jsx (100%) rename frontend/src/apps/draft/{common => }/utils.js (60%) mode change 100644 => 100755 scripts/generate_js_constants.py diff --git a/data/cache_concept.py b/data/cache_concept.py new file mode 100644 index 0000000..b85584d --- /dev/null +++ b/data/cache_concept.py @@ -0,0 +1,75 @@ +import pickle +import os +from typing import Any +from pathlib import Path +import json + +DEFAULT_PATH = Path("/Users/asc/Developer/boxofficefantasy/main/data/draft_cache.json") + +class CachedDraftState: + participants: list + phase: str # Replace with Enum if needed + draft_order: list = [] + draft_index: int + current_movie: str + bids: list + + def __init__(self, cache_file: str = "draft_cache.json"): + super().__setattr__("_cache_file", cache_file) + super().__setattr__("_cache", self._load_cache()) + + def _load_cache(self) -> dict: + if os.path.exists(self._cache_file): + try: + with open(self._cache_file, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + print(f"Failed to load cache: {e}") + return {} + return {} + + def _save_cache(self): + try: + with open(self._cache_file, "w", encoding="utf-8") as f: + json.dump(self._cache, f, indent=2) + except Exception as e: + print(f"Failed to save cache: {e}") + + def __getattr__(self, name: str) -> Any: + if name in self.__class__.__annotations__: + print(f"[GET] {name} -> {self._cache.get(name)}") + return self._cache.get(name, None) + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") + + def __setattr__(self, name: str, value: Any): + if name in self.__class__.__annotations__: + print(f"[SET] {name} = {value}") + self._cache[name] = value + self._save_cache() + else: + super().__setattr__(name, value) + +if __name__ == "__main__": + # Clean start for testing + if os.path.exists("draft_cache.pkl"): + os.remove("draft_cache.pkl") + + print("\n--- First Run: Setting Attributes ---") + state = CachedDraftState() + state.participants = ["Alice", "Bob"] + state.phase = "nominating" + # state.draft_order = ["Bob", "Alice"] + state.draft_index = 0 + state.current_movie = "The Matrix" + state.bids = [{"Alice": 10}, {"Bob": 12}] + + print("\n--- Second Run: Reading from Cache ---") + state2 = CachedDraftState() + print("participants:", state2.participants) + print("phase:", state2.phase) + print("draft_order:", state2.draft_order) + print("draft_index:", state2.draft_index) + print("current_movie:", state2.current_movie) + print("bids:", state2.bids) + + pass \ No newline at end of file diff --git a/data/draft_cache.json b/data/draft_cache.json new file mode 100644 index 0000000000000000000000000000000000000000..23eef64022420fe3cb84d56af5f7d112ab89f99a GIT binary patch literal 162 zcmXwyK?=e!6hsS6H?`iur7Q2C+u*KSDQWUGA8ZnSQt77P0kZt!b!`=AGt7gTd5YJ& z_-W>SZi1qO8iR5v@?{~_@s1iemnYvdFmjq}Ekx~9&P0a{B|*b@E} qoq>sEk+*sz0++hMKX9FBE$q%J5l);%kLG(gvn^#E7Zb9HFdjaT>N|b_ literal 0 HcmV?d00001 diff --git a/draft/constants.py b/draft/constants.py index a308a31..db339bc 100644 --- a/draft/constants.py +++ b/draft/constants.py @@ -18,8 +18,8 @@ class DraftMessage(StrEnum): PHASE_CHANGE_CONFIRM = "phase.change.confirm" # server -> client (target phase payload) # Status / sync - STATUS_SYNC_REQUEST = "status.sync.request" # client -> server - STATUS_SYNC_INFORM = "status.sync.inform" # server -> client (full/partial state) + DRAFT_STATUS_REQUEST = "draft.status.request" # client -> server + DRAFT_STATUS_INFORM = "draft.status.sync.inform" # server -> client (full/partial state) DRAFT_INDEX_ADVANCE_REQUEST = "draft.index.advance.request" DRAFT_INDEX_ADVANCE_CONFIRM = "draft.index.advance.confirm" diff --git a/draft/consumers.py b/draft/consumers.py index aa626bf..f1a1875 100644 --- a/draft/consumers.py +++ b/draft/consumers.py @@ -72,18 +72,12 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): "payload": {"user": self.user.username}, }, ) - await self.channel_layer.send( - self.channel_name, + await self.channel_layer.group_send( + self.group_names.session, { "type": "direct.message", - "subtype": DraftMessage.STATUS_SYNC_INFORM, - "payload": { - **self.draft_state, - "user": self.user.username, - "participants": [ - user.username for user in self.draft_participants - ], - }, + "subtype": DraftMessage.DRAFT_STATUS_INFORM, + "payload": self.draft_state.to_dict(), }, ) await self.channel_layer.send( @@ -101,14 +95,37 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): async def receive_json(self, content): logger.info(f"receiving message {content}") event_type = content.get("type") - if event_type == DraftMessage.STATUS_SYNC_REQUEST: + if event_type == DraftMessage.DRAFT_STATUS_REQUEST: await self.send_json( { - "type": DraftMessage.STATUS_SYNC_INFORM, + "type": DraftMessage.DRAFT_STATUS_INFORM, "payload": self.get_draft_status(), } ) + # --- Convenience helpers --- + async def send_draft_state(self): + """Send the current draft state only to this client.""" + await self.channel_layer.send( + self.channel_name, + { + "type": "direct.message", + "subtype": DraftMessage.DRAFT_STATUS_INFORM, + "payload": self.draft_state.to_dict(), + }, + ) + + async def broadcast_state(self): + """Broadcast current draft state to all in session group.""" + await self.channel_layer.group_send( + self.group_names.session, + { + "type": "broadcast.session", + "subtype": DraftMessage.DRAFT_STATUS_INFORM, + "payload": self.draft_state.to_dict(), + }, + ) + # Broadcast Handlers async def direct_message(self, event): await self._dispatch_broadcast(event) @@ -132,9 +149,15 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): def get_draft_session(self, draft_session_id_hashed) -> DraftSession: draft_session_id = DraftSession.decode_id(draft_session_id_hashed) if draft_session_id: - draft_session = DraftSession.objects.select_related( - "season", "season__league", "settings", - ).prefetch_related("participants").get(pk=draft_session_id) + draft_session = ( + DraftSession.objects.select_related( + "season", + "season__league", + "settings", + ) + .prefetch_related("participants") + .get(pk=draft_session_id) + ) else: raise Exception() @@ -155,89 +178,85 @@ class DraftAdminConsumer(DraftConsumerBase): await self.channel_layer.group_add(self.group_names.admin, self.channel_name) + def should_accept_user(self): + return super().should_accept_user() and self.user.is_staff + async def receive_json(self, content): await super().receive_json(content) logger.info(f"Receive message {content}") event_type = content.get("type") - if ( - event_type == DraftMessage.PHASE_CHANGE_REQUEST - and content.get("destination") == DraftPhase.DETERMINE_ORDER - ): - await self.determine_draft_order() - if ( - event_type == DraftMessage.PHASE_CHANGE_REQUEST - and content.get("destination") == DraftPhase.NOMINATING - ): - await self.start_nominate() + match event_type: + case DraftMessage.PHASE_CHANGE_REQUEST: + destination = content.get('destination') + match destination: + case DraftPhase.DETERMINE_ORDER: + await self.set_draft_phase(DraftPhase.DETERMINE_ORDER) + self.draft_state.determine_draft_order() + await self.channel_layer.group_send( + self.group_names.session, + { + "type": "broadcast.session", + "subtype": DraftMessage.ORDER_DETERMINE_CONFIRM, + "payload": {"draft_order": self.draft_state.draft_order}, + }, + ) + await self.broadcast_state() - if event_type == DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST: - self.draft_state.draft_index_advance() - await self.channel_layer.group_send( - self.group_names.session, - { - "type": "broadcast.session", - "subtype": DraftMessage.DRAFT_INDEX_ADVANCE_CONFIRM, - "payload": {**self.draft_state}, - }, - ) + case DraftPhase.NOMINATING: + await self.set_draft_phase(DraftPhase.NOMINATING) + await self.channel_layer.group_send( + self.group_names.session, + { + "type": "broadcast.session", + "subtype": DraftMessage.PHASE_CHANGE_CONFIRM, + "payload": {"phase": self.draft_state.phase}, + }, + ) + await self.broadcast_state() - if event_type == DraftMessage.NOMINATION_SUBMIT_REQUEST: - movie_id = content.get("payload", {}).get("movie_id") - user = content.get("payload", {}).get("user") - self.draft_state.start_nomination(movie_id) - await self.channel_layer.group_send( - self.group_names.session, - { - "type": "broadcast.session", - "subtype": DraftMessage.NOMINATION_CONFIRM, - "payload": { - "current_movie": self.draft_state[ - "current_movie" - ], - "nominating_participant": user, + case DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST: + self.draft_state.draft_index_advance() + await self.channel_layer.group_send( + self.group_names.session, + { + "type": "broadcast.session", + "subtype": DraftMessage.DRAFT_INDEX_ADVANCE_CONFIRM, + "payload": {"draft_index": self.draft_state.draft_index}, }, - }, - ) - if event_type == DraftMessage.BID_START_REQUEST: + ) + await self.broadcast_state() - self.draft_state.start_bidding() - await self.channel_layer.group_send( - self.group_names.session, - { - "type": "broadcast.session", - "subtype": DraftMessage.BID_START_INFORM, - "payload": {**self.draft_state}, - }, - ) + case DraftMessage.NOMINATION_SUBMIT_REQUEST: + movie_id = content.get("payload", {}).get("movie_id") + user = content.get("payload", {}).get("user") + self.draft_state.start_nomination(movie_id) + await self.channel_layer.group_send( + self.group_names.session, + { + "type": "broadcast.session", + "subtype": DraftMessage.NOMINATION_CONFIRM, + "payload": { + "current_movie": self.draft_state["current_movie"], + "nominating_participant": user, + }, + }, + ) + await self.broadcast_state() - def should_accept_user(self): - return super().should_accept_user() and self.user.is_staff + case DraftMessage.BID_START_REQUEST: + self.draft_state.start_bidding() + await self.channel_layer.group_send( + self.group_names.session, + { + "type": "broadcast.session", + "subtype": DraftMessage.BID_START_INFORM, + "payload": {**self.draft_state}, + }, + ) + await self.broadcast_state() - # === Draft logic === - async def start_nominate(self): - await self.set_draft_phase(DraftPhase.NOMINATING) - await self.channel_layer.group_send( - self.group_names.session, - { - "type": "broadcast.session", - "subtype": DraftMessage.PHASE_CHANGE_CONFIRM, - "payload": {"phase": self.draft_state.phase}, - }, - ) - - async def determine_draft_order(self): - self.draft_state.determine_draft_order() - next_picks = self.draft_state.next_picks(include_current=True) - - await self.channel_layer.group_send( - self.group_names.session, - { - "type": "broadcast.session", - "subtype": DraftMessage.ORDER_DETERMINE_CONFIRM, - "payload": {**self.draft_state}, - }, - ) + # === Draft logic === async def set_draft_phase(self, destination: DraftPhase): self.draft_state.phase = destination @@ -269,7 +288,9 @@ class DraftParticipantConsumer(DraftConsumerBase): "subtype": DraftMessage.PARTICIPANT_JOIN_CONFIRM, "payload": { "user": self.user.username, - "connected_participants": list(self.draft_state.connected_participants), + "connected_participants": list( + self.draft_state.connected_participants + ), }, }, ) @@ -287,7 +308,9 @@ class DraftParticipantConsumer(DraftConsumerBase): "subtype": DraftMessage.PARTICIPANT_LEAVE_INFORM, "payload": { "user": self.user.username, - "connected_participants": list(self.draft_state.connected_participants), + "connected_participants": list( + self.draft_state.connected_participants + ), }, }, ) @@ -315,9 +338,9 @@ class DraftParticipantConsumer(DraftConsumerBase): }, }, ) - + if event_type == DraftMessage.BID_PLACE_REQUEST: - bid_amount = content.get('payload',{}).get('bid_amount') + bid_amount = content.get("payload", {}).get("bid_amount") self.draft_state.place_bid(self.user, bid_amount) await self.channel_layer.group_send( self.group_names.session, diff --git a/draft/state.py b/draft/state.py index 7011e27..86df768 100644 --- a/draft/state.py +++ b/draft/state.py @@ -65,10 +65,11 @@ class DraftCache: raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") class DraftStateManager: + _initial_phase: DraftPhase = DraftPhase.WAITING.value + def __init__(self, session: DraftSession): self.session_id: str = session.hashid self.cache: DraftCache = DraftCache(self.session_id, cache) - self._initial_phase: DraftPhase = self.cache.phase or DraftPhase.WAITING.value self.settings: DraftSessionSettings = session.settings self.participants: set[User] = set(session.participants.all()) self.connected_participants: set[User] = set() @@ -76,7 +77,7 @@ class DraftStateManager: # === Phase Management === @property def phase(self) -> str: - return self.cache.phase + return self.cache.phase or self._initial_phase @phase.setter def phase(self, new_phase: DraftPhase) -> None: @@ -106,7 +107,7 @@ class DraftStateManager: self.phase = DraftPhase.DETERMINE_ORDER self.draft_index = 0 draft_order = random.sample( - self.participants, len(self.participants) + list(self.participants), len(self.participants) ) self.draft_order = [user.username for user in draft_order] return self.draft_order diff --git a/draft/templates/draft/room.dj.html b/draft/templates/draft/room.dj.html index 7dbb4c0..9aa5f24 100644 --- a/draft/templates/draft/room.dj.html +++ b/draft/templates/draft/room.dj.html @@ -3,6 +3,10 @@ {% load static %}
+{% if user.is_staff %} +
You are admin!
+{% endif %} {% endblock body %} \ No newline at end of file diff --git a/draft/urls.py b/draft/urls.py index 71c420f..aaa63ee 100644 --- a/draft/urls.py +++ b/draft/urls.py @@ -6,6 +6,7 @@ app_name = "draft" urlpatterns = [ # path("", views.draft_room, name="room"), path("session//", views.draft_room, name="session"), - path("session//", views.draft_room, name="admin_session"), + path("session//debug", views.draft_room_debug, name="session"), + # path("session//", views.draft_room, name="admin_session"), # path("//", views.draft_room_list, name="room"), ] \ No newline at end of file diff --git a/draft/views.py b/draft/views.py index 6e77179..fe62e26 100644 --- a/draft/views.py +++ b/draft/views.py @@ -6,28 +6,22 @@ from django.contrib.auth.decorators import login_required from boxofficefantasy_project.utils import decode_id @login_required(login_url='/login/') -def draft_room(request, league_slug=None, season_slug=None, draft_session_id_hashed=None, subpage=""): +def draft_room(request, draft_session_id_hashed=None): if draft_session_id_hashed: draft_session_id = decode_id(draft_session_id_hashed) draft_session = get_object_or_404(DraftSession, id=draft_session_id) league = draft_session.season.league season = draft_session.season - elif league_slug and season_slug: - raise NotImplementedError - league = get_object_or_404(League, slug=league_slug) - label, year = parse_season_slug(season_slug) - season = get_object_or_404(Season, league=league, label__iexact=label, year=year) - draft_session = get_object_or_404(DraftSession, season=season) context = { "draft_id_hashed": draft_session.hashid, "league": league, "season": season, } + return render(request, "draft/room.dj.html", context) - if subpage == "admin": - return render(request, "draft/room_admin.dj.html", context) - elif subpage == "debug": - return render(request, "draft/room_debug.dj.html", context) - else: - return render(request, "draft/room.dj.html", context) +def draft_room_debug(request, draft_session_id_hashed=None): + if draft_session_id_hashed: + draft_session_id = decode_id(draft_session_id_hashed) + draft_session = get_object_or_404(DraftSession, id=draft_session_id) + return render(request, "draft/room_debug.dj.html", {"draft_id_hashed": draft_session.hashid,}) \ No newline at end of file diff --git a/draft_cache.json b/draft_cache.json new file mode 100644 index 0000000..b70d1cf --- /dev/null +++ b/draft_cache.json @@ -0,0 +1,21 @@ +{ + "participants": [ + "Alice", + "Bob" + ], + "phase": "nominating", + "draft_order": [ + "Bob", + "Alice" + ], + "draft_index": 0, + "current_movie": "The Matrix", + "bids": [ + { + "Alice": 10 + }, + { + "Bob": 12 + } + ] +} \ No newline at end of file diff --git a/frontend/src/apps/draft/admin/DraftAdmin.jsx b/frontend/src/apps/draft/DraftAdminBar.jsx similarity index 84% rename from frontend/src/apps/draft/admin/DraftAdmin.jsx rename to frontend/src/apps/draft/DraftAdminBar.jsx index af4e283..ef708e8 100644 --- a/frontend/src/apps/draft/admin/DraftAdmin.jsx +++ b/frontend/src/apps/draft/DraftAdminBar.jsx @@ -1,13 +1,10 @@ import React, { useEffect, useState } from "react"; -import { useWebSocket } from "../common/WebSocketContext.jsx"; -import { WebSocketStatus } from "../common/WebSocketStatus.jsx"; -import { ParticipantList } from "../common/ParticipantList.jsx"; -import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '../constants.js'; -import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "../common/utils.js" -import { DraftMoviePool } from "../common/DraftMoviePool.jsx" -import { DraftCountdownClock } from "../common/DraftCountdownClock.jsx" -import { DraftParticipant } from "../participant/DraftParticipant.jsx"; +import { useWebSocket } from "./components/WebSocketContext.jsx"; + +import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from './constants.js'; +import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./utils.js" + import { jsxs } from "react/jsx-runtime"; @@ -102,7 +99,6 @@ export const DraftAdmin = ({ draftSessionId }) => { else if (target == "previous" && originPhaseIndex > 0) { destination = DraftPhasesOrdered[originPhaseIndex - 1] } - console.log(destination) socket.send( JSON.stringify( { type: DraftMessage.PHASE_CHANGE_REQUEST, origin, destination } @@ -140,22 +136,15 @@ export const DraftAdmin = ({ draftSessionId }) => { } return ( -
-
- -
-
-
- -
+
+
-
- -
+
+
{ handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}>
diff --git a/frontend/src/apps/draft/participant/DraftParticipant.jsx b/frontend/src/apps/draft/DraftDashboard.jsx similarity index 50% rename from frontend/src/apps/draft/participant/DraftParticipant.jsx rename to frontend/src/apps/draft/DraftDashboard.jsx index 77d8ce4..4ebca86 100644 --- a/frontend/src/apps/draft/participant/DraftParticipant.jsx +++ b/frontend/src/apps/draft/DraftDashboard.jsx @@ -1,14 +1,14 @@ // DraftAdmin.jsx import React, { useEffect, useState, useRef } from "react"; -import { useWebSocket } from "../common/WebSocketContext.jsx"; -import { WebSocketStatus } from "../common/WebSocketStatus.jsx"; -import { DraftMessage, DraftPhaseLabel, DraftPhases } from '../constants.js'; -import { fetchDraftDetails, handleUserIdentifyMessages, isEmptyObject } from "../common/utils.js"; -import { DraftMoviePool } from "../common/DraftMoviePool.jsx"; -import { ParticipantList } from "../common/ParticipantList.jsx"; -import { DraftCountdownClock } from "../common/DraftCountdownClock.jsx" -import { handleDraftStatusMessages } from '../common/utils.js' +import { useWebSocket } from "./components/WebSocketContext.jsx"; +import { WebSocketStatus } from "./components/WebSocketStatus.jsx"; +import { DraftMessage, DraftPhaseLabel, DraftPhases } from './constants.js'; +import { fetchDraftDetails, handleUserIdentifyMessages, isEmptyObject } from "./utils.js"; +import { DraftMoviePool } from "./components/DraftMoviePool.jsx"; +import { ParticipantList } from "./components/ParticipantList.jsx"; +import { DraftCountdownClock } from "./components/DraftCountdownClock.jsx" +import { handleDraftStatusMessages } from './utils.js' // import { Collapse } from 'bootstrap/dist/js/bootstrap.bundle.min.js'; import { Collapse, ListGroup } from "react-bootstrap"; @@ -62,6 +62,7 @@ const NominateMenu = ({ socket, draftState, draftDetails, currentUser, }) => { } export const DraftParticipant = ({ draftSessionId }) => { + const socket = useWebSocket(); const [draftState, setDraftState] = useState({}); const [draftDetails, setDraftDetails] = useState({}); @@ -79,13 +80,6 @@ export const DraftParticipant = ({ draftSessionId }) => { }) }, [draftSessionId]) - useEffect(() => { - if (!socket) return; - socket.onclose = (event) => { - console.log('Websocket Closed') - } - }, [socket]) - useEffect(() => { if (!socket) return; @@ -116,80 +110,88 @@ export const DraftParticipant = ({ draftSessionId }) => { return (
-
-
-

Draft Live

-
-
{DraftPhaseLabel[draftState.phase]}
- -
-
-
-
- -
- {console.log("draft_state", draftState)} -
Round {draftState.current_pick?.round}
-
Pick {draftState.current_pick?.pick_in_round}
-
{draftState.current_pick?.overall + 1} Overall
+
+
+
+
Draft Live
+
+
{DraftPhaseLabel[draftState.phase]}
+
-
-
-
-
- {draftState.bids?.length > 0 ? Math.max(draftState.bids?.map(i=>i.bid_amount)) : ""} + +
+
+ +
+
Round {draftState.current_pick?.round}
+
Pick {draftState.current_pick?.pick_in_round}
+
{draftState.current_pick?.overall + 1} Overall
-
- highest bid +
+
+ + Show Content +
+
+
+
+
+ Movie title +
+
+
+
+
Bids
+
+
    + {draftState.bids?.reverse().map((b,idx) => ( +
  1. +
    +
    {b.user}
    +
    {b.amount}
    +
    +
  2. + ))} + +
+
+
+
+
+
+ Bid + + +
+
+
+
-
    - {draftState.bids?.map((bid, idx) => ( -
  1. {bid.user}: {bid.amount}
  2. - ))} -
+
    +
  • +
    Current Pick: {draftState.current_pick?.participant}
    +
  • +
  • +
    Next Pick: {draftState.next_picks ? draftState.next_picks[0]?.participant : ""}
    +
  • +
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-
-
    -
  • -
    Current Pick: {draftState.current_pick?.participant}
    -
  • -
  • -
    Next Pick: {draftState.next_picks ? draftState.next_picks[0]?.participant : ""}
    -
  • -
-
-
-
+
-

Draft Catalog

+
Draft Catalog
Current Nomination: {movies.find(i => draftState.current_movie == i.id)?.title}
- + {/* */}
@@ -199,7 +201,7 @@ export const DraftParticipant = ({ draftSessionId }) => {
-

My Team

+
My Team
    @@ -212,21 +214,18 @@ export const DraftParticipant = ({ draftSessionId }) => {
    -

    Teams

    +
    Teams
    -
      -
    • -
      -
        -
      • -
      -
    • + {draftState.participants?.map(p => ( +
    • +
      {p}
      +
        +
      • +
      +
    • + ))}
    diff --git a/frontend/src/apps/draft/DraftDebug.jsx b/frontend/src/apps/draft/DraftDebug.jsx index 544c088..da0afcf 100644 --- a/frontend/src/apps/draft/DraftDebug.jsx +++ b/frontend/src/apps/draft/DraftDebug.jsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react";; -import { useWebSocket } from "./common/WebSocketContext.jsx"; +import { useWebSocket } from "./components/WebSocketContext.jsx"; import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from './constants.js'; -import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./common/utils.js" +import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./utils.js" export const DraftDebug = ({ draftSessionId }) => { const [draftState, setDraftState] = useState({}) diff --git a/frontend/src/apps/draft/common/DraftCountdownClock.jsx b/frontend/src/apps/draft/components/DraftCountdownClock.jsx similarity index 73% rename from frontend/src/apps/draft/common/DraftCountdownClock.jsx rename to frontend/src/apps/draft/components/DraftCountdownClock.jsx index 2d25a8f..eeb4fed 100644 --- a/frontend/src/apps/draft/common/DraftCountdownClock.jsx +++ b/frontend/src/apps/draft/components/DraftCountdownClock.jsx @@ -1,10 +1,10 @@ import React, { useEffect, useState } from "react"; -export function DraftCountdownClock({ endTime, onFinish }) { +export function DraftCountdownClock({ draftState }) { // endTime is in seconds (Unix time) - + const {bidding_timer_end, onFinish} = draftState const getTimeLeft = (et) => Math.max(0, Math.floor(et - Date.now() / 1000)); - const [timeLeft, setTimeLeft] = useState(getTimeLeft(endTime)); + const [timeLeft, setTimeLeft] = useState(getTimeLeft(bidding_timer_end)); useEffect(() => { if (timeLeft <= 0) { @@ -12,13 +12,13 @@ export function DraftCountdownClock({ endTime, onFinish }) { return; } const timer = setInterval(() => { - const t = getTimeLeft(endTime); + const t = getTimeLeft(bidding_timer_end); setTimeLeft(t); if (t <= 0 && onFinish) onFinish(); }, 100); return () => clearInterval(timer); // eslint-disable-next-line - }, [endTime, onFinish, timeLeft]); + }, [bidding_timer_end, onFinish, timeLeft]); const minutes = Math.floor(timeLeft / 60); const secs = timeLeft % 60; diff --git a/frontend/src/apps/draft/common/DraftMoviePool.jsx b/frontend/src/apps/draft/components/DraftMoviePool.jsx similarity index 93% rename from frontend/src/apps/draft/common/DraftMoviePool.jsx rename to frontend/src/apps/draft/components/DraftMoviePool.jsx index 00a445d..cc733d4 100644 --- a/frontend/src/apps/draft/common/DraftMoviePool.jsx +++ b/frontend/src/apps/draft/components/DraftMoviePool.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { isEmptyObject } from "./utils"; +import { isEmptyObject } from "../utils"; export const DraftMoviePool = ({ isParticipant, draftDetails, draftState }) => { if(isEmptyObject(draftDetails)) {return} diff --git a/frontend/src/apps/draft/common/ParticipantList.jsx b/frontend/src/apps/draft/components/ParticipantList.jsx similarity index 94% rename from frontend/src/apps/draft/common/ParticipantList.jsx rename to frontend/src/apps/draft/components/ParticipantList.jsx index ee1363b..0bd97c4 100644 --- a/frontend/src/apps/draft/common/ParticipantList.jsx +++ b/frontend/src/apps/draft/components/ParticipantList.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { fetchDraftDetails, isEmptyObject } from "../common/utils.js" +import { fetchDraftDetails, isEmptyObject } from "../utils.js" export const ParticipantList = ({ isAdmin, draftState, draftDetails, currentUser }) => { if (isEmptyObject(draftState) || isEmptyObject(draftDetails)) { console.warn('empty draft state', draftState); return } diff --git a/frontend/src/apps/draft/common/WebSocketContext.jsx b/frontend/src/apps/draft/components/WebSocketContext.jsx similarity index 100% rename from frontend/src/apps/draft/common/WebSocketContext.jsx rename to frontend/src/apps/draft/components/WebSocketContext.jsx diff --git a/frontend/src/apps/draft/common/WebSocketStatus.jsx b/frontend/src/apps/draft/components/WebSocketStatus.jsx similarity index 100% rename from frontend/src/apps/draft/common/WebSocketStatus.jsx rename to frontend/src/apps/draft/components/WebSocketStatus.jsx diff --git a/frontend/src/apps/draft/constants.js b/frontend/src/apps/draft/constants.js index a7c8def..fddd87e 100644 --- a/frontend/src/apps/draft/constants.js +++ b/frontend/src/apps/draft/constants.js @@ -13,8 +13,8 @@ export const DraftMessage = { PHASE_CHANGE_INFORM: "phase.change.inform", PHASE_CHANGE_REQUEST: "phase.change.request", PHASE_CHANGE_CONFIRM: "phase.change.confirm", - STATUS_SYNC_REQUEST: "status.sync.request", - STATUS_SYNC_INFORM: "status.sync.inform", + DRAFT_STATUS_REQUEST: "draft.status.request", + DRAFT_STATUS_INFORM: "draft.status.sync.inform", DRAFT_INDEX_ADVANCE_REQUEST: "draft.index.advance.request", DRAFT_INDEX_ADVANCE_CONFIRM: "draft.index.advance.confirm", ORDER_DETERMINE_REQUEST: "order.determine.request", @@ -22,6 +22,7 @@ export const DraftMessage = { BID_START_INFORM: "bid.start.inform", BID_START_REQUEST: "bid.start.request", BID_PLACE_REQUEST: "bid.place.request", + BID_PLACE_CONFIRM: "bid.update.confirm", BID_UPDATE_INFORM: "bid.update.inform", BID_END_INFORM: "bid.end.inform", NOMINATION_SUBMIT_REQUEST: "nomination.submit.request", diff --git a/frontend/src/apps/draft/common/utils.js b/frontend/src/apps/draft/utils.js similarity index 60% rename from frontend/src/apps/draft/common/utils.js rename to frontend/src/apps/draft/utils.js index 02ad344..13caa00 100644 --- a/frontend/src/apps/draft/common/utils.js +++ b/frontend/src/apps/draft/utils.js @@ -1,4 +1,4 @@ -import { DraftMessage } from "../constants"; +import { DraftMessage } from "./constants"; export async function fetchDraftDetails(draftSessionId) { return fetch(`/api/draft/${draftSessionId}/`) @@ -37,38 +37,12 @@ export function isEmptyObject(obj) { export const handleDraftStatusMessages = (event, setDraftState) => { const message = JSON.parse(event.data); const { type, payload } = message; - console.log("Message: ", type, event?.data); if (!payload) return; - const { - connected_participants, - phase, - draft_order, - draft_index, - current_movie, - bidding_timer_end, - bidding_timer_start, - current_pick, - next_picks, - bids - } = payload; - if (type == DraftMessage.STATUS_SYNC_INFORM) { + if (type == DraftMessage.DRAFT_STATUS_INFORM) { setDraftState(payload); } - - setDraftState((prev) => ({ - ...prev, - ...(connected_participants ? { connected_participants } : {}), - ...(draft_order ? { draft_order } : {}), - ...(draft_index ? { draft_index } : {}), - ...(phase ? { phase: Number(phase) } : {}), - ...(current_movie ? { current_movie } : {}), - ...(bidding_timer_end ? { bidding_timer_end: Number(bidding_timer_end) } : {}), - ...(current_pick ? { current_pick } : {}), - ...(next_picks ? { next_picks } : {}), - ...(bids ? {bids} : {}) - })); }; export const handleUserIdentifyMessages = (event, setUser) => { diff --git a/frontend/src/index.js b/frontend/src/index.js index bef5fbf..4b06521 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -2,13 +2,13 @@ import './scss/styles.scss' import React from "react"; import { createRoot } from "react-dom/client"; -import { WebSocketProvider } from "./apps/draft/common/WebSocketContext.jsx"; -import { DraftAdmin } from "./apps/draft/admin/DraftAdmin.jsx"; -import { DraftParticipant} from './apps/draft/participant/DraftParticipant.jsx' +import { WebSocketProvider } from "./apps/draft/components/WebSocketContext.jsx"; +import { DraftAdmin } from "./apps/draft/DraftAdminBar.jsx"; +import { DraftParticipant} from './apps/draft/DraftDashboard.jsx' import { DraftDebug} from './apps/draft/DraftDebug.jsx' -const draftAdminRoot = document.getElementById("draft-admin-root"); +const draftAdminBarRoot = document.getElementById("draft-admin-bar-root"); const draftPartipantRoot = document.getElementById("draft-participant-root") const draftDebugRoot = document.getElementById("draft-debug-root") const {draftSessionId} = window; // from backend template @@ -21,9 +21,9 @@ if (draftPartipantRoot) { ); } -if (draftAdminRoot) { +if (draftAdminBarRoot) { const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/admin`; - createRoot(draftAdminRoot).render( + createRoot(draftAdminBarRoot).render( diff --git a/frontend/src/scss/styles.scss b/frontend/src/scss/styles.scss index 8799767..505cf95 100644 --- a/frontend/src/scss/styles.scss +++ b/frontend/src/scss/styles.scss @@ -1,6 +1,6 @@ @use "../../node_modules/bootstrap/scss/bootstrap.scss"; @use "./fonts/graphique.css"; -@import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Oswald:wght@200..700&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=League+Gothic&family=Oswald:wght@200..700&display=swap'); // Import only functions & variables @import "~bootstrap/scss/functions"; @import "~bootstrap/scss/variables"; @@ -127,8 +127,21 @@ } } -#draft-participant-root, -#draft-admin-root { +#draft-admin-bar { + @extend .d-flex; + @extend .flex-column; + @extend .border-top; + @extend .border-bottom; + @extend .gap-2; + @extend .p-2; + @extend .shadow-sm; + div { + @extend .d-flex; + @extend .justify-content-center + } +} + +#draft-participant-root { @extend .flex-grow-1; .wrapper:first-child { @extend .p-2; @@ -137,44 +150,61 @@ gap: 1rem; /* space between panels */ justify-content: center; /* center the panels horizontally */ + section { + max-width: 450px; /* never go beyond this */ + min-width: 300px; /* keeps them from getting too small */ + flex: 1 1 350px; /* grow/shrink, base width */ + } .panel { @extend .border; @extend .shadow-sm; @extend .rounded-2; - flex: 1 1 350px; /* grow/shrink, base width */ - max-width: 450px; /* never go beyond this */ - min-width: 300px; /* keeps them from getting too small */ header.panel-header { @extend .p-1; @extend .text-uppercase; @extend .align-items-center; @extend .border-bottom; - @extend .border-secondary; - background-color: $blue-100; + @extend .border-2; + @extend .border-secondary-subtle; + // background-color: $blue-100; + @extend .bg-dark; + @extend .bg-gradient; + @extend .text-light; @extend .rounded-top-2; .panel-title { + @extend .ms-2; @extend .fw-bold; @extend .fs-5; } } } - .panel.draft-live { + .bids-container { + overflow: scroll; + height: 85px; + } + #draft-live { header.panel-header { @extend .d-flex; @extend .justify-content-between; } - .draft-live-state-container { - @extend .d-flex; - background-color: $green-100; + #draft-clock { + @extend .row; + @extend .g-0; + // background-color: $green-100; + @extend .text-light; + @extend .text-bg-dark; + @extend .lh-1; .countdown-clock { - @extend .fs-1; - @extend .fw-bold; + font-family: 'League Gothic'; + font-size: $font-size-base * 5; + @extend .fw-bolder; @extend .col; @extend .align-content-center; @extend .text-center; } .pick-description { @extend .col; + @extend .align-content-center; } } div:has(.pick-list), div:has(.bid-list){ diff --git a/scripts/generate_js_constants.py b/scripts/generate_js_constants.py old mode 100644 new mode 100755