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
This commit is contained in:
75
data/cache_concept.py
Normal file
75
data/cache_concept.py
Normal file
@@ -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
|
||||
BIN
data/draft_cache.json
Normal file
BIN
data/draft_cache.json
Normal file
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -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,67 +178,32 @@ 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()
|
||||
|
||||
if event_type == DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST:
|
||||
self.draft_state.draft_index_advance()
|
||||
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.DRAFT_INDEX_ADVANCE_CONFIRM,
|
||||
"payload": {**self.draft_state},
|
||||
"subtype": DraftMessage.ORDER_DETERMINE_CONFIRM,
|
||||
"payload": {"draft_order": self.draft_state.draft_order},
|
||||
},
|
||||
)
|
||||
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,
|
||||
},
|
||||
},
|
||||
)
|
||||
if event_type == 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},
|
||||
},
|
||||
)
|
||||
|
||||
def should_accept_user(self):
|
||||
return super().should_accept_user() and self.user.is_staff
|
||||
|
||||
# === Draft logic ===
|
||||
async def start_nominate(self):
|
||||
case DraftPhase.NOMINATING:
|
||||
await self.set_draft_phase(DraftPhase.NOMINATING)
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
@@ -225,19 +213,50 @@ class DraftAdminConsumer(DraftConsumerBase):
|
||||
"payload": {"phase": self.draft_state.phase},
|
||||
},
|
||||
)
|
||||
await self.broadcast_state()
|
||||
|
||||
async def determine_draft_order(self):
|
||||
self.draft_state.determine_draft_order()
|
||||
next_picks = self.draft_state.next_picks(include_current=True)
|
||||
|
||||
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.ORDER_DETERMINE_CONFIRM,
|
||||
"subtype": DraftMessage.DRAFT_INDEX_ADVANCE_CONFIRM,
|
||||
"payload": {"draft_index": self.draft_state.draft_index},
|
||||
},
|
||||
)
|
||||
await self.broadcast_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()
|
||||
|
||||
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 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
|
||||
),
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -317,7 +340,7 @@ 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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
{% load static %}
|
||||
<script>
|
||||
window.draftSessionId = "{{ draft_id_hashed }}"
|
||||
console.log("{{user}}")
|
||||
</script>
|
||||
<div id="draft-participant-root" data-draft-id="{{ draft_id_hashed }}"></div>
|
||||
{% if user.is_staff %}
|
||||
<div id="draft-admin-bar-root" data-draft-id="{{ draft_id_hashed }}">You are admin!</div>
|
||||
{% endif %}
|
||||
{% endblock body %}
|
||||
@@ -6,6 +6,7 @@ app_name = "draft"
|
||||
urlpatterns = [
|
||||
# path("", views.draft_room, name="room"),
|
||||
path("session/<str:draft_session_id_hashed>/", views.draft_room, name="session"),
|
||||
path("session/<str:draft_session_id_hashed>/<str:subpage>", views.draft_room, name="admin_session"),
|
||||
path("session/<str:draft_session_id_hashed>/debug", views.draft_room_debug, name="session"),
|
||||
# path("session/<str:draft_session_id_hashed>/<str:subpage>", views.draft_room, name="admin_session"),
|
||||
# path("<slug:league_slug>/<slug:season_slug>/", views.draft_room_list, name="room"),
|
||||
]
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
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,})
|
||||
21
draft_cache.json
Normal file
21
draft_cache.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="">
|
||||
<div className="">
|
||||
<DraftParticipant draftSessionId={draftSessionId}></DraftParticipant>
|
||||
<div className="d-flex justify-content-between border-bottom mb-2 p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="d-flex justify-content-center mt-3">
|
||||
<div id="draft-admin-bar">
|
||||
<div>
|
||||
<button onClick={() => handleRequestDraftSummary()} className="btn btn-small btn-light mx-1">
|
||||
<i className="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
<button onClick={handleAdvanceDraft} className="btn btn-primary mx-1">Advance Index</button>
|
||||
<button onClick={handleStartBidding} className="btn btn-primary mx-1">Start Bidding</button>
|
||||
</section>
|
||||
|
||||
<div class="d-flex justify-content-center mt-3">
|
||||
</div>
|
||||
<div>
|
||||
<DraftPhaseDisplay draftPhase={draftState.phase} nextPhaseHandler={() => { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}></DraftPhaseDisplay>
|
||||
</div>
|
||||
|
||||
@@ -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,56 +110,64 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
|
||||
return (
|
||||
<div className="wrapper">
|
||||
<section className="panel draft-live">
|
||||
<section id="draft-live">
|
||||
<div className="panel">
|
||||
<header className="panel-header">
|
||||
<h2 className="panel-title">Draft Live</h2>
|
||||
<div className="panel-title"><span>Draft Live</span></div>
|
||||
<div className="d-flex gap-1">
|
||||
<div className="phase-indicator badge bg-primary">{DraftPhaseLabel[draftState.phase]}</div>
|
||||
<WebSocketStatus socket={socket} />
|
||||
</div>
|
||||
</header>
|
||||
<div className="panel-body">
|
||||
<div className="draft-live-state-container">
|
||||
<DraftCountdownClock endTime={draftState.bidding_timer_end}></DraftCountdownClock>
|
||||
<div id="draft-clock">
|
||||
<DraftCountdownClock draftState={draftState}></DraftCountdownClock>
|
||||
<div className="pick-description">
|
||||
{console.log("draft_state", draftState)}
|
||||
<div>Round {draftState.current_pick?.round}</div>
|
||||
<div>Pick {draftState.current_pick?.pick_in_round}</div>
|
||||
<div>{draftState.current_pick?.overall + 1} Overall</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bid-status">
|
||||
<div className="d-flex">
|
||||
<div className="flex-grow-1 text-center">
|
||||
{draftState.bids?.length > 0 ? Math.max(draftState.bids?.map(i=>i.bid_amount)) : ""}
|
||||
<div className="bid-controls btn-group d-flex flex-column">
|
||||
|
||||
<a className="btn btn-primary d-none" data-bs-toggle="collapse" aria-expanded="true" aria-controls="collapse-1" href="#collapse-1" role="button">Show Content</a>
|
||||
<div id="collapse-1" className="collapse show">
|
||||
<div>
|
||||
<div className="row g-0 border rounded-2 m-2">
|
||||
<div className="col-3"><img className="img-fluid flex-fill" /></div>
|
||||
<div className="col d-flex justify-content-center align-items-center">
|
||||
<span className="fw-bold">Movie title</span>
|
||||
</div>
|
||||
<div className="flex-grow-1 text-center">
|
||||
highest bid
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ol className="bid-list">
|
||||
{draftState.bids?.map((bid, idx) => (
|
||||
<li key={idx}>{bid.user}: {bid.amount}</li>
|
||||
<div className="lh-sm text-center border p-0 border-bottom"><span>Bids</span></div>
|
||||
<div className="bids-container">
|
||||
<ol className="list-group list-group-flush">
|
||||
{draftState.bids?.reverse().map((b,idx) => (
|
||||
<li key={idx} className="list-group-item p-0">
|
||||
<div className="row g-0">
|
||||
<div className="col-8 col-xl-9 col-xxl-10"><span>{b.user}</span></div>
|
||||
<div className="col"><span>{b.amount}</span></div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="bid-controls btn-group d-flex flex-column">
|
||||
<div className="p-1">
|
||||
<form id="bid" onSubmit={submitBidRequest}>
|
||||
<div className="d-flex">
|
||||
<div className="flex-grow-1 text-center">
|
||||
<input type="number" id="bidAmount" name="bidAmount"></input>
|
||||
</div>
|
||||
<div className="flex-grow-1 text-center">
|
||||
<button className="flex-grow-1">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex">
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-text">Bid</span>
|
||||
<input className="form-control" type="number" id="bidAmount" name="bidAmount"/>
|
||||
<button className="btn btn-primary" type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<ul className="pick-list">
|
||||
<li>
|
||||
@@ -177,19 +179,19 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel draft-catalog">
|
||||
<section className="panel" id="draft-slate">
|
||||
<header className="panel-header">
|
||||
<h2 className="panel-title">Draft Catalog</h2>
|
||||
<div className="panel-title"><span>Draft Catalog</span></div>
|
||||
</header>
|
||||
<div className="panel-body">
|
||||
<div className="current-movie card">
|
||||
<span>Current Nomination: {movies.find(i => draftState.current_movie == i.id)?.title}</span>
|
||||
</div>
|
||||
<NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu>
|
||||
{/* <NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu> */}
|
||||
<div className="movie-filters"></div>
|
||||
|
||||
<DraftMoviePool isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
||||
@@ -199,7 +201,7 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
|
||||
<section className="panel my-team">
|
||||
<header className="panel-header">
|
||||
<h2 className="panel-title">My Team</h2>
|
||||
<div className="panel-title"><span>My Team</span></div>
|
||||
</header>
|
||||
<div className="panel-body">
|
||||
<ul className="team-movie-list list-group">
|
||||
@@ -212,21 +214,18 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
|
||||
<section className="panel teams">
|
||||
<header className="panel-header">
|
||||
<h2 className="panel-title">Teams</h2>
|
||||
<div className="panel-title"><span>Teams</span></div>
|
||||
</header>
|
||||
<div className="panel-body">
|
||||
<ParticipantList
|
||||
currentUser={currentUser}
|
||||
draftState={draftState}
|
||||
draftDetails={draftDetails}
|
||||
/>
|
||||
<ul className="team-list list-group">
|
||||
<li className="team-item list-group-item">
|
||||
<div className="team-name fw-bold"></div>
|
||||
{draftState.participants?.map(p => (
|
||||
<li className="team-item list-group-item" key={p}>
|
||||
<div className="team-name fw-bold">{p}</div>
|
||||
<ul className="team-movie-list list-group list-group-flush">
|
||||
<li className="team-movie-item list-group-item"></li>
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
@@ -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({})
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
@@ -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 }
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
@@ -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) {
|
||||
</WebSocketProvider>
|
||||
);
|
||||
}
|
||||
if (draftAdminRoot) {
|
||||
if (draftAdminBarRoot) {
|
||||
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/admin`;
|
||||
createRoot(draftAdminRoot).render(
|
||||
createRoot(draftAdminBarRoot).render(
|
||||
<WebSocketProvider url={wsUrl}>
|
||||
<DraftAdmin draftSessionId={draftSessionId}/>
|
||||
</WebSocketProvider>
|
||||
|
||||
@@ -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){
|
||||
|
||||
0
scripts/generate_js_constants.py
Normal file → Executable file
0
scripts/generate_js_constants.py
Normal file → Executable file
Reference in New Issue
Block a user