Compare commits
3 Commits
e8bf313f53
...
5e08fdc9a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
5e08fdc9a2
|
|||
|
baddca8d50
|
|||
|
b38c779772
|
@@ -2,6 +2,7 @@ from rest_framework import serializers
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from boxofficefantasy.models import Movie, Season
|
from boxofficefantasy.models import Movie, Season
|
||||||
from draft.models import DraftSession, DraftSessionSettings, DraftPick
|
from draft.models import DraftSession, DraftSessionSettings, DraftPick
|
||||||
|
from boxofficefantasy.integrations.tmdb import get_tmdb_movie_by_imdb
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@@ -16,10 +17,27 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
return f"{obj.first_name} {obj.last_name}".strip()
|
return f"{obj.first_name} {obj.last_name}".strip()
|
||||||
|
|
||||||
class MovieSerializer(serializers.ModelSerializer):
|
class MovieSerializer(serializers.ModelSerializer):
|
||||||
|
tmdb_data = serializers.SerializerMethodField()
|
||||||
|
def get_tmdb_data(self, obj):
|
||||||
|
if hasattr(obj, 'imdb_id') and obj.imdb_id:
|
||||||
|
tmdb_movie = get_tmdb_movie_by_imdb(obj.imdb_id)
|
||||||
|
if tmdb_movie:
|
||||||
|
poster_url = None
|
||||||
|
if tmdb_movie.get('poster_path'):
|
||||||
|
poster_url = f"{tmdb_movie['poster_path']}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': tmdb_movie.get('id'),
|
||||||
|
'title': tmdb_movie.get('title'),
|
||||||
|
'overview': tmdb_movie.get('overview'),
|
||||||
|
'poster_url': tmdb_movie['poster_url'],
|
||||||
|
'release_date': tmdb_movie.get('release_date'),
|
||||||
|
}
|
||||||
|
return None
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Movie
|
model = Movie
|
||||||
# fields = ("id", "imdb_id", "title", "year", "poster_url")
|
# fields = ("id", "imdb_id", "title", "year", "poster_url")
|
||||||
fields = ("id", "title")
|
fields = ("id", "title", "tmdb_data")
|
||||||
|
|
||||||
class DraftSessionSettingsSerializer(serializers.ModelSerializer):
|
class DraftSessionSettingsSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -12,13 +12,15 @@ tmdb.language = "en"
|
|||||||
|
|
||||||
TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/w500"
|
TMDB_IMAGE_BASE = "https://image.tmdb.org/t/p/w500"
|
||||||
|
|
||||||
def get_tmdb_movie_by_imdb(imdb_id):
|
def get_tmdb_movie_by_imdb(imdb_id, cache_poster=True):
|
||||||
"""
|
"""
|
||||||
Fetch TMDb metadata by IMDb ID, using cache to avoid redundant API calls.
|
Fetch TMDb metadata by IMDb ID, using cache to avoid redundant API calls.
|
||||||
"""
|
"""
|
||||||
cache_key = f"tmdb:movie:{imdb_id}"
|
cache_key = f"tmdb:movie:{imdb_id}"
|
||||||
cached = cache.get(cache_key)
|
cached = cache.get(cache_key)
|
||||||
if cached:
|
if cached:
|
||||||
|
if cache_poster and not cached.get('poster_url'):
|
||||||
|
cached['poster_url'] = cache_tmdb_poster(cached['poster_path'])
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
results = Movie().external(external_id=imdb_id, external_source="imdb_id")
|
results = Movie().external(external_id=imdb_id, external_source="imdb_id")
|
||||||
@@ -27,6 +29,8 @@ def get_tmdb_movie_by_imdb(imdb_id):
|
|||||||
|
|
||||||
movie_data = results.movie_results[0]
|
movie_data = results.movie_results[0]
|
||||||
cache.set(cache_key, movie_data, timeout=60 * 60 * 24) # 1 day
|
cache.set(cache_key, movie_data, timeout=60 * 60 * 24) # 1 day
|
||||||
|
if cache_poster:
|
||||||
|
movie_data['poster_url'] = cache_tmdb_poster(movie_data['poster_path'])
|
||||||
return movie_data
|
return movie_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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.
@@ -11,6 +11,7 @@ class DraftMessage(StrEnum):
|
|||||||
USER_JOIN_INFORM = "user.join.inform" # server -> client
|
USER_JOIN_INFORM = "user.join.inform" # server -> client
|
||||||
USER_LEAVE_INFORM = "user.leave.inform"
|
USER_LEAVE_INFORM = "user.leave.inform"
|
||||||
USER_IDENTIFICATION_INFORM = "user.identification.inform" # server -> client (tells socket "you are X", e.g. after connect) # server -> client
|
USER_IDENTIFICATION_INFORM = "user.identification.inform" # server -> client (tells socket "you are X", e.g. after connect) # server -> client
|
||||||
|
USER_STATE_INFORM = "user.state.inform"
|
||||||
|
|
||||||
# Phase control
|
# Phase control
|
||||||
PHASE_CHANGE_INFORM = "phase.change.inform" # server -> client (target phase payload)
|
PHASE_CHANGE_INFORM = "phase.change.inform" # server -> client (target phase payload)
|
||||||
@@ -18,8 +19,8 @@ class DraftMessage(StrEnum):
|
|||||||
PHASE_CHANGE_CONFIRM = "phase.change.confirm" # server -> client (target phase payload)
|
PHASE_CHANGE_CONFIRM = "phase.change.confirm" # server -> client (target phase payload)
|
||||||
|
|
||||||
# Status / sync
|
# Status / sync
|
||||||
STATUS_SYNC_REQUEST = "status.sync.request" # client -> server
|
DRAFT_STATUS_REQUEST = "draft.status.request" # client -> server
|
||||||
STATUS_SYNC_INFORM = "status.sync.inform" # server -> client (full/partial state)
|
DRAFT_STATUS_INFORM = "draft.status.sync.inform" # server -> client (full/partial state)
|
||||||
|
|
||||||
DRAFT_INDEX_ADVANCE_REQUEST = "draft.index.advance.request"
|
DRAFT_INDEX_ADVANCE_REQUEST = "draft.index.advance.request"
|
||||||
DRAFT_INDEX_ADVANCE_CONFIRM = "draft.index.advance.confirm"
|
DRAFT_INDEX_ADVANCE_CONFIRM = "draft.index.advance.confirm"
|
||||||
@@ -31,8 +32,10 @@ class DraftMessage(StrEnum):
|
|||||||
# Bidding (examples, adjust to your flow)
|
# Bidding (examples, adjust to your flow)
|
||||||
BID_START_INFORM = "bid.start.inform" # server -> client (movie, ends_at)
|
BID_START_INFORM = "bid.start.inform" # server -> client (movie, ends_at)
|
||||||
BID_START_REQUEST = "bid.start.request" # server -> client (movie, ends_at)
|
BID_START_REQUEST = "bid.start.request" # server -> client (movie, ends_at)
|
||||||
|
BID_START_REJECT = "bid.start.reject" # server -> client (movie, ends_at)
|
||||||
BID_PLACE_REQUEST = "bid.place.request" # client -> server (amount)
|
BID_PLACE_REQUEST = "bid.place.request" # client -> server (amount)
|
||||||
BID_PLACE_CONFIRM = "bid.update.confirm" # server -> client (high bid)
|
BID_PLACE_REJECT = "bid.place.reject" # server -> client (high bid)
|
||||||
|
BID_PLACE_CONFIRM = "bid.place.confirm" # server -> client (high bid)
|
||||||
BID_UPDATE_INFORM = "bid.update.inform" # server -> client (high bid)
|
BID_UPDATE_INFORM = "bid.update.inform" # server -> client (high bid)
|
||||||
BID_END_INFORM = "bid.end.inform" # server -> client (winner)
|
BID_END_INFORM = "bid.end.inform" # server -> client (winner)
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,19 @@ 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
|
||||||
import asyncio
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from draft.constants import (
|
from draft.constants import (
|
||||||
DraftMessage,
|
DraftMessage,
|
||||||
DraftPhase,
|
DraftPhase,
|
||||||
DraftGroupChannelNames,
|
DraftGroupChannelNames,
|
||||||
)
|
)
|
||||||
from draft.state import DraftStateManager
|
from draft.state import DraftStateManager, DraftStateException
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__) # __name__ = module path
|
logger = logging.getLogger(__name__) # __name__ = module path
|
||||||
|
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
|
|
||||||
class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
||||||
group_names: DraftGroupChannelNames
|
group_names: DraftGroupChannelNames
|
||||||
draft_state: DraftStateManager
|
draft_state: DraftStateManager
|
||||||
@@ -61,6 +57,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
|||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
await self.accept()
|
await self.accept()
|
||||||
|
self.draft_state.connect_participant(self.user.username)
|
||||||
await self.channel_layer.group_add(
|
await self.channel_layer.group_add(
|
||||||
self.group_names.session, self.channel_name
|
self.group_names.session, self.channel_name
|
||||||
)
|
)
|
||||||
@@ -72,20 +69,6 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
|||||||
"payload": {"user": self.user.username},
|
"payload": {"user": self.user.username},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
await self.channel_layer.send(
|
|
||||||
self.channel_name,
|
|
||||||
{
|
|
||||||
"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
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await self.channel_layer.send(
|
await self.channel_layer.send(
|
||||||
self.channel_name,
|
self.channel_name,
|
||||||
{
|
{
|
||||||
@@ -94,6 +77,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
|||||||
"payload": {"user": self.user.username},
|
"payload": {"user": self.user.username},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await self.broadcast_state()
|
||||||
|
|
||||||
async def should_accept_user(self) -> bool:
|
async def should_accept_user(self) -> bool:
|
||||||
return self.user.is_authenticated
|
return self.user.is_authenticated
|
||||||
@@ -101,14 +85,53 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
|||||||
async def receive_json(self, content):
|
async def receive_json(self, content):
|
||||||
logger.info(f"receiving message {content}")
|
logger.info(f"receiving message {content}")
|
||||||
event_type = content.get("type")
|
event_type = content.get("type")
|
||||||
if event_type == DraftMessage.STATUS_SYNC_REQUEST:
|
if event_type == DraftMessage.DRAFT_STATUS_REQUEST:
|
||||||
await self.send_json(
|
await self.send_json(
|
||||||
{
|
{
|
||||||
"type": DraftMessage.STATUS_SYNC_INFORM,
|
"type": DraftMessage.DRAFT_STATUS_INFORM,
|
||||||
"payload": self.get_draft_status(),
|
"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.USER_STATE_INFORM,
|
||||||
|
"payload": self.draft_state.user_state(self.user),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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.USER_STATE_INFORM,
|
||||||
|
"payload": [self.draft_state.user_state(user) for user in self.draft_participants],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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
|
# Broadcast Handlers
|
||||||
async def direct_message(self, event):
|
async def direct_message(self, event):
|
||||||
await self._dispatch_broadcast(event)
|
await self._dispatch_broadcast(event)
|
||||||
@@ -132,9 +155,15 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
|||||||
def get_draft_session(self, draft_session_id_hashed) -> DraftSession:
|
def get_draft_session(self, draft_session_id_hashed) -> DraftSession:
|
||||||
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 = (
|
||||||
"season", "season__league", "settings",
|
DraftSession.objects.select_related(
|
||||||
).prefetch_related("participants").get(pk=draft_session_id)
|
"season",
|
||||||
|
"season__league",
|
||||||
|
"settings",
|
||||||
|
)
|
||||||
|
.prefetch_related("participants")
|
||||||
|
.get(pk=draft_session_id)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
@@ -155,89 +184,107 @@ class DraftAdminConsumer(DraftConsumerBase):
|
|||||||
|
|
||||||
await self.channel_layer.group_add(self.group_names.admin, self.channel_name)
|
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):
|
async def receive_json(self, content):
|
||||||
await super().receive_json(content)
|
await super().receive_json(content)
|
||||||
logger.info(f"Receive message {content}")
|
logger.info(f"Receive message {content}")
|
||||||
event_type = content.get("type")
|
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 (
|
match event_type:
|
||||||
event_type == DraftMessage.PHASE_CHANGE_REQUEST
|
case DraftMessage.PHASE_CHANGE_REQUEST:
|
||||||
and content.get("destination") == DraftPhase.NOMINATING
|
destination = content.get('destination')
|
||||||
):
|
match destination:
|
||||||
await self.start_nominate()
|
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:
|
case DraftPhase.NOMINATING:
|
||||||
self.draft_state.draft_index_advance()
|
await self.set_draft_phase(DraftPhase.NOMINATING)
|
||||||
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.DRAFT_INDEX_ADVANCE_CONFIRM,
|
"subtype": DraftMessage.PHASE_CHANGE_CONFIRM,
|
||||||
"payload": {**self.draft_state},
|
"payload": {"phase": self.draft_state.phase},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await self.broadcast_state()
|
||||||
|
|
||||||
|
case DraftPhase.BIDDING:
|
||||||
|
await self.set_draft_phase(DraftPhase.BIDDING)
|
||||||
|
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:
|
case DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST:
|
||||||
movie_id = content.get("payload", {}).get("movie_id")
|
self.draft_state.draft_index_advance()
|
||||||
user = content.get("payload", {}).get("user")
|
await self.channel_layer.group_send(
|
||||||
self.draft_state.start_nomination(movie_id)
|
self.group_names.session,
|
||||||
await self.channel_layer.group_send(
|
{
|
||||||
self.group_names.session,
|
"type": "broadcast.session",
|
||||||
{
|
"subtype": DraftMessage.DRAFT_INDEX_ADVANCE_CONFIRM,
|
||||||
"type": "broadcast.session",
|
"payload": {"draft_index": self.draft_state.draft_index},
|
||||||
"subtype": DraftMessage.NOMINATION_CONFIRM,
|
|
||||||
"payload": {
|
|
||||||
"current_movie": self.draft_state[
|
|
||||||
"current_movie"
|
|
||||||
],
|
|
||||||
"nominating_participant": user,
|
|
||||||
},
|
},
|
||||||
},
|
)
|
||||||
)
|
await self.broadcast_state()
|
||||||
if event_type == DraftMessage.BID_START_REQUEST:
|
|
||||||
|
|
||||||
self.draft_state.start_bidding()
|
case DraftMessage.NOMINATION_SUBMIT_REQUEST:
|
||||||
await self.channel_layer.group_send(
|
movie_id = content.get("payload", {}).get("movie_id")
|
||||||
self.group_names.session,
|
user = content.get("payload", {}).get("user")
|
||||||
{
|
self.draft_state.start_nomination(movie_id)
|
||||||
"type": "broadcast.session",
|
await self.channel_layer.group_send(
|
||||||
"subtype": DraftMessage.BID_START_INFORM,
|
self.group_names.session,
|
||||||
"payload": {**self.draft_state},
|
{
|
||||||
},
|
"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):
|
case DraftMessage.BID_START_REQUEST:
|
||||||
return super().should_accept_user() and self.user.is_staff
|
try:
|
||||||
|
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()
|
||||||
|
except DraftStateException as e:
|
||||||
|
await self.channel_layer.send(
|
||||||
|
self.channel_name, {
|
||||||
|
"type": "direct.message",
|
||||||
|
"subtype": DraftMessage.BID_START_REJECT,
|
||||||
|
"payload": {'message': str(e)}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# === 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):
|
# === Draft logic ===
|
||||||
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},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_draft_phase(self, destination: DraftPhase):
|
async def set_draft_phase(self, destination: DraftPhase):
|
||||||
self.draft_state.phase = destination
|
self.draft_state.phase = destination
|
||||||
@@ -269,7 +316,9 @@ class DraftParticipantConsumer(DraftConsumerBase):
|
|||||||
"subtype": DraftMessage.PARTICIPANT_JOIN_CONFIRM,
|
"subtype": DraftMessage.PARTICIPANT_JOIN_CONFIRM,
|
||||||
"payload": {
|
"payload": {
|
||||||
"user": self.user.username,
|
"user": self.user.username,
|
||||||
"connected_participants": self.draft_state.connected_participants,
|
"connected_participants": list(
|
||||||
|
self.draft_state.connected_participants
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -287,16 +336,20 @@ class DraftParticipantConsumer(DraftConsumerBase):
|
|||||||
"subtype": DraftMessage.PARTICIPANT_LEAVE_INFORM,
|
"subtype": DraftMessage.PARTICIPANT_LEAVE_INFORM,
|
||||||
"payload": {
|
"payload": {
|
||||||
"user": self.user.username,
|
"user": self.user.username,
|
||||||
"connected_participants": self.draft_state.connected_participants,
|
"connected_participants": list(
|
||||||
|
self.draft_state.connected_participants
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
await self.broadcast_state()
|
||||||
await super().disconnect(close_code)
|
await super().disconnect(close_code)
|
||||||
self.draft_state.disconnect_participant(self.user.username)
|
self.draft_state.disconnect_participant(self.user.username)
|
||||||
await self.channel_layer.group_discard(
|
await self.channel_layer.group_discard(
|
||||||
self.group_names.session, self.channel_name
|
self.group_names.session, self.channel_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def should_accept_user(self):
|
def should_accept_user(self):
|
||||||
return super().should_accept_user() and self.user in self.draft_participants
|
return super().should_accept_user() and self.user in self.draft_participants
|
||||||
|
|
||||||
@@ -310,23 +363,34 @@ 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("movie_id"),
|
||||||
"user": content.get("payload", {}).get("user"),
|
"user": content.get("payload", {}).get("user"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if event_type == DraftMessage.BID_PLACE_REQUEST:
|
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)
|
try:
|
||||||
await self.channel_layer.group_send(
|
self.draft_state.place_bid(self.user, bid_amount)
|
||||||
self.group_names.session,
|
await self.channel_layer.group_send(
|
||||||
{
|
self.group_names.session,
|
||||||
"type": "broadcast.session",
|
{
|
||||||
"subtype": DraftMessage.BID_PLACE_CONFIRM,
|
"type": "broadcast.session",
|
||||||
"payload": {**self.draft_state},
|
"subtype": DraftMessage.BID_PLACE_CONFIRM,
|
||||||
},
|
"payload": {'user': self.user.username, 'bid': bid_amount},
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
except DraftStateException as e:
|
||||||
|
await self.channel_layer.group_send(
|
||||||
|
self.group_names.session,
|
||||||
|
{
|
||||||
|
"type": "broadcast.session",
|
||||||
|
"subtype": DraftMessage.BID_PLACE_REJECT,
|
||||||
|
"payload": {'user': self.user.username, 'bid': bid_amount, 'error':str(e)},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await self.broadcast_state()
|
||||||
|
|
||||||
# === Broadcast handlers ===
|
# === Broadcast handlers ===
|
||||||
|
|
||||||
@@ -335,11 +399,8 @@ class DraftParticipantConsumer(DraftConsumerBase):
|
|||||||
|
|
||||||
# === Draft ===
|
# === Draft ===
|
||||||
|
|
||||||
async def nominate(self, movie_title): ...
|
|
||||||
|
|
||||||
async def place_bid(self, amount, user): ...
|
# === DB Access ===
|
||||||
|
|
||||||
# === Example DB Access ===
|
|
||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def add_draft_participant(self):
|
def add_draft_participant(self):
|
||||||
|
|||||||
206
draft/state.py
206
draft/state.py
@@ -1,10 +1,10 @@
|
|||||||
from django.core.cache import cache
|
from django.core.cache import cache, BaseCache
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from boxofficefantasy.models import Movie
|
from boxofficefantasy.models import Movie
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from draft.constants import DraftPhase
|
from draft.constants import DraftPhase
|
||||||
from draft.models import DraftSession
|
from draft.models import DraftSession, DraftSessionSettings
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple
|
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple
|
||||||
@@ -14,131 +14,124 @@ class DraftStateException(Exception):
|
|||||||
"""Raised when an action is not allowed due to the current draft state or phase."""
|
"""Raised when an action is not allowed due to the current draft state or phase."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class DraftCacheKeys:
|
class DraftCache:
|
||||||
def __init__(self, id):
|
phase: str
|
||||||
self.prefix = f"draft:{id}"
|
draft_order: str
|
||||||
|
draft_index: str
|
||||||
|
current_movie: str
|
||||||
|
bids: str
|
||||||
|
bid_timer_start: str
|
||||||
|
bid_timer_end: str
|
||||||
|
connected_participants: str
|
||||||
|
|
||||||
@property
|
_cached_properties = {
|
||||||
def admins(self):
|
"participants",
|
||||||
return f"{self.prefix}:admins"
|
"connected_participants",
|
||||||
|
"phase",
|
||||||
|
"draft_order",
|
||||||
|
"draft_index",
|
||||||
|
"current_movie",
|
||||||
|
"bids",
|
||||||
|
"bid_timer_start",
|
||||||
|
"bid_timer_end",
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
def __init__(self, draft_id: str, cache: BaseCache = cache):
|
||||||
def participants(self):
|
super().__setattr__("_cache", self._load_cache(cache))
|
||||||
return f"{self.prefix}:participants"
|
super().__setattr__("_prefix", f"draft:{draft_id}:")
|
||||||
|
|
||||||
@property
|
|
||||||
def users(self):
|
|
||||||
return f"{self.prefix}:users"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def connected_users(self):
|
|
||||||
return f"{self.prefix}:connected_users"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def phase(self):
|
|
||||||
return f"{self.prefix}:phase"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def draft_order(self):
|
|
||||||
return f"{self.prefix}:draft_order"
|
|
||||||
|
|
||||||
@property
|
def _load_cache(self, cache) -> BaseCache:
|
||||||
def draft_index(self):
|
return cache
|
||||||
return f"{self.prefix}:draft_index"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_movie(self):
|
|
||||||
return f"{self.prefix}:current_movie"
|
|
||||||
|
|
||||||
# @property
|
|
||||||
# def state(self):
|
|
||||||
# return f"{self.prefix}:state"
|
|
||||||
|
|
||||||
# @property
|
def _save_cache(self) -> None:
|
||||||
# def current_movie(self):
|
# Django cache saves itself
|
||||||
# return f"{self.prefix}:current_movie"
|
return
|
||||||
|
|
||||||
@property
|
def __getattr__(self, name: str) -> Any:
|
||||||
def bids(self):
|
if name == "_prefix": return super().__getattribute__('_prefix')
|
||||||
return f"{self.prefix}:bids"
|
if name in self._cached_properties:
|
||||||
|
return self._cache.get(self._prefix+name, None)
|
||||||
|
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
||||||
|
|
||||||
# @property
|
def __setattr__(self, name: str, value: Any):
|
||||||
# def participants(self):
|
if name in self._cached_properties:
|
||||||
# return f"{self.prefix}:participants"
|
self._cache.set(self._prefix+name, value)
|
||||||
|
self._save_cache()
|
||||||
|
else:
|
||||||
|
super().__setattr__(name, value)
|
||||||
|
|
||||||
@property
|
def __delattr__(self, name):
|
||||||
def bid_timer_end(self):
|
if name in self._cached_properties:
|
||||||
return f"{self.prefix}:bid_timer_end"
|
self._cache.delete(name)
|
||||||
@property
|
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
||||||
def bid_timer_start(self):
|
|
||||||
return f"{self.prefix}:bid_timer_start"
|
|
||||||
|
|
||||||
# def user_status(self, user_id):
|
|
||||||
# return f"{self.prefix}:user:{user_id}:status"
|
|
||||||
|
|
||||||
# def user_channel(self, user_id):
|
|
||||||
# return f"{self.prefix}:user:{user_id}:channel"
|
|
||||||
|
|
||||||
class DraftStateManager:
|
class DraftStateManager:
|
||||||
|
_initial_phase: DraftPhase = DraftPhase.WAITING.value
|
||||||
|
|
||||||
def __init__(self, session: DraftSession):
|
def __init__(self, session: DraftSession):
|
||||||
self.session_id = session.hashid
|
self.session_id: str = session.hashid
|
||||||
self.cache = cache
|
self.cache: DraftCache = DraftCache(self.session_id, cache)
|
||||||
self.cache_keys = DraftCacheKeys(self.session_id)
|
self.settings: DraftSessionSettings = session.settings
|
||||||
self._initial_phase = self.cache.get(self.cache_keys.phase, DraftPhase.WAITING.value)
|
self._participants = list(session.participants.all())
|
||||||
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.cache_keys.phase, self._initial_phase))
|
return self.cache.phase or self._initial_phase
|
||||||
|
|
||||||
@phase.setter
|
@phase.setter
|
||||||
def phase(self, new_phase: DraftPhase):
|
def phase(self, new_phase: DraftPhase) -> None:
|
||||||
self.cache.set(self.cache_keys.phase, new_phase.value)
|
self.cache.phase = new_phase
|
||||||
|
|
||||||
# === Connected Users ===
|
# === Connected Users ===
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connected_participants(self) -> list[str]:
|
def connected_participants(self):
|
||||||
return json.loads(self.cache.get(self.cache_keys.connected_users) or "[]")
|
return set(json.loads(self.cache.connected_participants or "[]"))
|
||||||
|
|
||||||
def connect_participant(self, username: str):
|
def connect_participant(self, username: str):
|
||||||
users = set(self.connected_participants)
|
connected_participants = self.connected_participants
|
||||||
users.add(username)
|
connected_participants.add(username)
|
||||||
self.cache.set(self.cache_keys.connected_users, json.dumps(list(users)))
|
self.cache.connected_participants = json.dumps(list(connected_participants))
|
||||||
|
return connected_participants
|
||||||
|
|
||||||
def disconnect_participant(self, username: str):
|
def disconnect_participant(self, username: str):
|
||||||
users = set(self.connected_participants)
|
connected_participants = self.connected_participants
|
||||||
users.discard(username)
|
connected_participants.discard(username)
|
||||||
self.cache.set(self.cache_keys.connected_users, json.dumps(list(users)))
|
self.cache.connected_participants = json.dumps(list(connected_participants))
|
||||||
|
return connected_participants
|
||||||
|
|
||||||
# === Draft Order ===
|
# === Draft Order ===
|
||||||
@property
|
@property
|
||||||
def draft_order(self):
|
def draft_order(self):
|
||||||
return json.loads(self.cache.get(self.cache_keys.draft_order,"[]"))
|
return json.loads(self.cache.draft_order or "[]")
|
||||||
|
|
||||||
@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.cache_keys.draft_order,json.dumps(draft_order))
|
self.cache.draft_order = json.dumps(draft_order)
|
||||||
|
|
||||||
def determine_draft_order(self) -> List[User]:
|
def determine_draft_order(self) -> List[User]:
|
||||||
self.phase = DraftPhase.DETERMINE_ORDER
|
self.phase = DraftPhase.DETERMINE_ORDER
|
||||||
self.draft_index = 0
|
self.draft_index = 0
|
||||||
draft_order = random.sample(
|
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]
|
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.cache_keys.draft_index,0)
|
draft_index = self.cache.draft_index
|
||||||
|
if not draft_index:
|
||||||
|
draft_index = 0
|
||||||
|
self.cache.draft_index = draft_index
|
||||||
|
return self.cache.draft_index
|
||||||
|
|
||||||
@draft_index.setter
|
@draft_index.setter
|
||||||
def draft_index(self, draft_index: int):
|
def draft_index(self, draft_index: int):
|
||||||
self.cache.set(self.cache_keys.draft_index, int(draft_index))
|
self.cache.draft_index = 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
|
||||||
@@ -178,36 +171,47 @@ 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.cache_keys.current_movie, movie_id)
|
self.cache.current_movie = movie_id
|
||||||
self.cache.delete(self.cache_keys.bids)
|
self.cache.bids = []
|
||||||
|
|
||||||
def place_bid(self, user: User, amount: int|str):
|
def place_bid(self, user: User, amount: int|str):
|
||||||
if isinstance(amount, str):
|
if isinstance(amount, str):
|
||||||
amount = int(amount)
|
amount = int(amount)
|
||||||
bids = self.get_bids()
|
bids = self.get_bids()
|
||||||
bids.append({"user":user.username, "amount":amount})
|
user_state = self.user_state(user)
|
||||||
self.cache.set(self.cache_keys.bids, json.dumps(bids))
|
timestamp = int(time.time() * 1000)
|
||||||
|
if not user_state['can_bid']:
|
||||||
|
raise DraftStateException('Cannot bid')
|
||||||
|
if not user_state['remaining_budget'] > amount:
|
||||||
|
raise DraftStateException('No Budget Remaining')
|
||||||
|
if not self.get_timer_end() or not timestamp < self.get_timer_end() * 1000:
|
||||||
|
raise DraftStateException("Timer Error")
|
||||||
|
bids.append({"user":user.username, "amount":amount, 'timestamp': timestamp})
|
||||||
|
self.cache.bids = json.dumps(bids)
|
||||||
|
|
||||||
def get_bids(self) -> dict:
|
def get_bids(self) -> dict:
|
||||||
return json.loads(self.cache.get(self.cache_keys.bids) or "[]")
|
return json.loads(self.cache.bids or "[]")
|
||||||
|
|
||||||
def current_movie(self) -> Movie | None:
|
def current_movie(self) -> Movie | None:
|
||||||
movie_id = self.cache.get(self.cache_keys.current_movie)
|
movie_id = self.cache.current_movie
|
||||||
return Movie.objects.filter(pk=movie_id).first() if movie_id else None
|
return movie_id if movie_id else None
|
||||||
|
|
||||||
def start_bidding(self):
|
def start_bidding(self):
|
||||||
|
if not self.phase == DraftPhase.BIDDING:
|
||||||
|
raise DraftStateException('Not the right phase for that')
|
||||||
|
if not self.current_movie():
|
||||||
|
raise DraftStateException('No movie nominated')
|
||||||
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.cache_keys.bid_timer_end, end_time)
|
self.cache.bid_timer_end = end_time
|
||||||
self.cache.set(self.cache_keys.bid_timer_start, start_time)
|
self.cache.bid_timer_start = start_time
|
||||||
|
|
||||||
def get_timer_end(self) -> str | None:
|
def get_timer_end(self) -> str | None:
|
||||||
return self.cache.get(self.cache_keys.bid_timer_end)
|
return self.cache.bid_timer_end
|
||||||
|
|
||||||
def get_timer_start(self) -> str | None:
|
def get_timer_start(self) -> str | None:
|
||||||
return self.cache.get(self.cache_keys.bid_timer_start)
|
return self.cache.bid_timer_start
|
||||||
|
|
||||||
# === Sync Snapshot ===
|
# === Sync Snapshot ===
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
@@ -216,8 +220,9 @@ class DraftStateManager:
|
|||||||
"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": list(self.connected_participants),
|
||||||
"current_movie": self.cache.get(self.cache_keys.current_movie),
|
"current_movie": self.cache.current_movie,
|
||||||
|
"awards": [],
|
||||||
"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(),
|
||||||
@@ -225,6 +230,17 @@ class DraftStateManager:
|
|||||||
"next_picks": picks[1:] if picks else []
|
"next_picks": picks[1:] if picks else []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def user_state(self, user: User) -> dict:
|
||||||
|
picks = self.next_picks(include_current=True)
|
||||||
|
return {
|
||||||
|
"is_admin": user.is_staff,
|
||||||
|
"user": user.username,
|
||||||
|
"can_bid": self.phase == DraftPhase.BIDDING,
|
||||||
|
"can_nominate": self.phase == DraftPhase.NOMINATING and picks[0].get('participant') == user.username,
|
||||||
|
"movies":[],
|
||||||
|
"remaining_budget":100,
|
||||||
|
}
|
||||||
|
|
||||||
# def __dict__(self):
|
# def __dict__(self):
|
||||||
# return self.get_summary()
|
# return self.get_summary()
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
<script>
|
<script>
|
||||||
window.draftSessionId = "{{ draft_id_hashed }}"
|
window.draftSessionId = "{{ draft_id_hashed }}"
|
||||||
|
window.isAdmin = "{{user.is_staff}}"
|
||||||
|
console.log("{{user}}")
|
||||||
</script>
|
</script>
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<div id="draft-admin-bar-root" data-draft-id="{{ draft_id_hashed }}">You are admin!</div>
|
||||||
|
{% endif %}
|
||||||
<div id="draft-participant-root" data-draft-id="{{ draft_id_hashed }}"></div>
|
<div id="draft-participant-root" data-draft-id="{{ draft_id_hashed }}"></div>
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
@@ -6,6 +6,7 @@ app_name = "draft"
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path("", views.draft_room, name="room"),
|
# 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>/", 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"),
|
# 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
|
from boxofficefantasy_project.utils import decode_id
|
||||||
|
|
||||||
@login_required(login_url='/login/')
|
@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:
|
if draft_session_id_hashed:
|
||||||
draft_session_id = decode_id(draft_session_id_hashed)
|
draft_session_id = decode_id(draft_session_id_hashed)
|
||||||
draft_session = get_object_or_404(DraftSession, id=draft_session_id)
|
draft_session = get_object_or_404(DraftSession, id=draft_session_id)
|
||||||
league = draft_session.season.league
|
league = draft_session.season.league
|
||||||
season = draft_session.season
|
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 = {
|
context = {
|
||||||
"draft_id_hashed": draft_session.hashid,
|
"draft_id_hashed": draft_session.hashid,
|
||||||
"league": league,
|
"league": league,
|
||||||
"season": season,
|
"season": season,
|
||||||
}
|
}
|
||||||
|
return render(request, "draft/room.dj.html", context)
|
||||||
|
|
||||||
if subpage == "admin":
|
def draft_room_debug(request, draft_session_id_hashed=None):
|
||||||
return render(request, "draft/room_admin.dj.html", context)
|
if draft_session_id_hashed:
|
||||||
elif subpage == "debug":
|
draft_session_id = decode_id(draft_session_id_hashed)
|
||||||
return render(request, "draft/room_debug.dj.html", context)
|
draft_session = get_object_or_404(DraftSession, id=draft_session_id)
|
||||||
else:
|
return render(request, "draft/room_debug.dj.html", {"draft_id_hashed": draft_session.hashid,})
|
||||||
return render(request, "draft/room.dj.html", context)
|
|
||||||
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 React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useWebSocket } from "../common/WebSocketContext.jsx";
|
import { useWebSocket } from "./components/WebSocketContext.jsx";
|
||||||
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
|
||||||
import { ParticipantList } from "../common/ParticipantList.jsx";
|
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from './constants.js';
|
||||||
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '../constants.js';
|
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./utils.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 { jsxs } from "react/jsx-runtime";
|
import { jsxs } from "react/jsx-runtime";
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +38,6 @@ export const DraftAdmin = ({ draftSessionId }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDraftDetails(draftSessionId)
|
fetchDraftDetails(draftSessionId)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log("Fetched draft data", data)
|
|
||||||
setDraftDetails(data)
|
setDraftDetails(data)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
@@ -102,7 +98,6 @@ export const DraftAdmin = ({ draftSessionId }) => {
|
|||||||
else if (target == "previous" && originPhaseIndex > 0) {
|
else if (target == "previous" && originPhaseIndex > 0) {
|
||||||
destination = DraftPhasesOrdered[originPhaseIndex - 1]
|
destination = DraftPhasesOrdered[originPhaseIndex - 1]
|
||||||
}
|
}
|
||||||
console.log(destination)
|
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{ type: DraftMessage.PHASE_CHANGE_REQUEST, origin, destination }
|
{ type: DraftMessage.PHASE_CHANGE_REQUEST, origin, destination }
|
||||||
@@ -140,22 +135,15 @@ export const DraftAdmin = ({ draftSessionId }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div id="draft-admin-bar">
|
||||||
<div className="">
|
<div>
|
||||||
<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">
|
|
||||||
<button onClick={() => handleRequestDraftSummary()} className="btn btn-small btn-light mx-1">
|
<button onClick={() => handleRequestDraftSummary()} className="btn btn-small btn-light mx-1">
|
||||||
<i className="bi bi-arrow-clockwise"></i>
|
<i className="bi bi-arrow-clockwise"></i>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleAdvanceDraft} className="btn btn-primary mx-1">Advance Index</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>
|
<button onClick={handleStartBidding} className="btn btn-primary mx-1">Start Bidding</button>
|
||||||
</section>
|
</div>
|
||||||
|
<div>
|
||||||
<div class="d-flex justify-content-center mt-3">
|
|
||||||
<DraftPhaseDisplay draftPhase={draftState.phase} nextPhaseHandler={() => { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}></DraftPhaseDisplay>
|
<DraftPhaseDisplay draftPhase={draftState.phase} nextPhaseHandler={() => { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}></DraftPhaseDisplay>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
169
frontend/src/apps/draft/DraftDashboard.jsx
Normal file
169
frontend/src/apps/draft/DraftDashboard.jsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// DraftAdmin.jsx
|
||||||
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
|
|
||||||
|
import { useWebSocket } from "./components/WebSocketContext.jsx";
|
||||||
|
import { WebSocketStatus } from "./components/WebSocketStatus.jsx";
|
||||||
|
import { DraftMessage, DraftPhaseLabel, DraftPhase } from './constants.js';
|
||||||
|
import { fetchDraftDetails, isEmptyObject } from "./utils.js";
|
||||||
|
import { DraftMoviePool } from "./components/DraftMoviePool.jsx";
|
||||||
|
import { ParticipantList } from "./components/ParticipantList.jsx";
|
||||||
|
import { DraftCountdownClock } from "./components/DraftCountdownClock.jsx"
|
||||||
|
import { handleDraftStatusMessages, handleUserStatusMessages, handleUserIdentifyMessages } from './utils.js'
|
||||||
|
// import { Collapse } from 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||||
|
import { Collapse } from "react-bootstrap";
|
||||||
|
|
||||||
|
export const DraftParticipant = ({ draftSessionId }) => {
|
||||||
|
|
||||||
|
const socket = useWebSocket();
|
||||||
|
const [draftState, setDraftState] = useState({});
|
||||||
|
const [userStatus, setUserState] = useState([]);
|
||||||
|
const [draftDetails, setDraftDetails] = useState({});
|
||||||
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
|
|
||||||
|
const [movies, setMovies] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDraftDetails(draftSessionId)
|
||||||
|
.then((data) => {
|
||||||
|
console.log("Fetched draft data", data)
|
||||||
|
setMovies(data.movies)
|
||||||
|
setDraftDetails(data)
|
||||||
|
})
|
||||||
|
}, [draftSessionId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket) return;
|
||||||
|
|
||||||
|
const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState)
|
||||||
|
const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser)
|
||||||
|
const userStatusMessageHandler = (event) => handleUserStatusMessages(event, setUserState)
|
||||||
|
socket.addEventListener('message', draftStatusMessageHandler);
|
||||||
|
socket.addEventListener('message', userIdentifyMessageHandler);
|
||||||
|
socket.addEventListener('message', userStatusMessageHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.removeEventListener('message', draftStatusMessageHandler);
|
||||||
|
socket.removeEventListener('message', userIdentifyMessageHandler);
|
||||||
|
socket.removeEventListener('message', userStatusMessageHandler);
|
||||||
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
function submitBidRequest(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
const form = event.target
|
||||||
|
const formData = new FormData(form)
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: DraftMessage.BID_PLACE_REQUEST,
|
||||||
|
payload: {
|
||||||
|
bid_amount: formData.get('bidAmount'),
|
||||||
|
user: currentUser
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const currentUserStatus = userStatus.find(u => u.user == currentUser)
|
||||||
|
const currentMovie = movies.find(i => draftState.current_movie == i.id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`wrapper`}>
|
||||||
|
<section id="draft-live">
|
||||||
|
<div className="panel">
|
||||||
|
<header className="panel-header">
|
||||||
|
<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>
|
||||||
|
<div id="draft-clock">
|
||||||
|
<DraftCountdownClock draftState={draftState}></DraftCountdownClock>
|
||||||
|
<div className="pick-description">
|
||||||
|
<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-controls btn-group d-flex flex-column">
|
||||||
|
<Collapse in={draftState.phase == DraftPhase.BIDDING}>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div className="row g-0 border rounded-2 m-2">
|
||||||
|
<div className="col-3">
|
||||||
|
<img className="img-fluid flex-fill" src={currentMovie?.tmdb_data?.poster_url}/>
|
||||||
|
</div>
|
||||||
|
<div className="col d-flex justify-content-center align-items-center">
|
||||||
|
<span className="fw-bold">{currentMovie?.title}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<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="p-1">
|
||||||
|
<form id="bid" onSubmit={submitBidRequest}>
|
||||||
|
<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>
|
||||||
|
</Collapse>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="panel" id="draft-slate">
|
||||||
|
<header className="panel-header">
|
||||||
|
<div className="panel-title"><span>Films</span></div>
|
||||||
|
</header>
|
||||||
|
<div className="panel-body">
|
||||||
|
<div className="movie-filters"></div>
|
||||||
|
<DraftMoviePool currentUserStatus={currentUserStatus} currentUser={currentUser} socket={socket} isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section className="panel my-team">
|
||||||
|
<header className="panel-header">
|
||||||
|
<div className="panel-title"><span>My Team</span></div>
|
||||||
|
</header>
|
||||||
|
<div className="panel-body">
|
||||||
|
<ul className="team-movie-list list-group">
|
||||||
|
<li className="team-movie-item list-group-item"></li>
|
||||||
|
</ul>
|
||||||
|
<div className="budget-status"></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section className="panel teams">
|
||||||
|
<header className="panel-header">
|
||||||
|
<div className="panel-title"><span>Teams</span></div>
|
||||||
|
</header>
|
||||||
|
<div className="panel-body">
|
||||||
|
<ParticipantList currentUser={currentUser} className="team-list" draftDetails={draftDetails} draftState={draftState} isAdmin={isAdmin}></ParticipantList>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";;
|
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 { 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 }) => {
|
export const DraftDebug = ({ draftSessionId }) => {
|
||||||
const [draftState, setDraftState] = useState({})
|
const [draftState, setDraftState] = useState({})
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { isEmptyObject } from "./utils";
|
|
||||||
|
|
||||||
export const DraftMoviePool = ({ isParticipant, draftDetails, draftState }) => {
|
|
||||||
if(isEmptyObject(draftDetails)) {return}
|
|
||||||
const {movies} = draftDetails
|
|
||||||
const {current_movie} = draftState
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="movie-pool-container">
|
|
||||||
<label>Movies</label>
|
|
||||||
<ul>
|
|
||||||
{movies.map(m => (
|
|
||||||
<li key={m.id} className={`${current_movie == m.id ? "current-movie fw-bold" : null }`}>
|
|
||||||
<a href={`/api/movie/${m.id}/detail`}>
|
|
||||||
{m.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { fetchDraftDetails, isEmptyObject } from "../common/utils.js"
|
|
||||||
|
|
||||||
export const ParticipantList = ({ isAdmin, draftState, draftDetails, currentUser }) => {
|
|
||||||
if (isEmptyObject(draftState) || isEmptyObject(draftDetails)) { console.warn('empty draft state', draftState); return }
|
|
||||||
const { draft_order, draft_index, connected_participants } = draftState
|
|
||||||
const { participants } = draftDetails
|
|
||||||
|
|
||||||
const ListTag = draft_order?.length > 0 ? "ol" : "ul"
|
|
||||||
const listItems = draft_order?.length > 0 ? draft_order.map(d => participants.find(p => p.username == d)) : participants
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="participant-list-container">
|
|
||||||
<label>Particpants</label>
|
|
||||||
<ListTag className="participant-list">
|
|
||||||
{listItems.map((p, i) => (
|
|
||||||
<li key={i} className={`${i == draft_index ? "fw-bold" : ""}`}>
|
|
||||||
<span className={`${p.username == currentUser ? "current-user" : ""}`}>{p?.full_name}</span>
|
|
||||||
{isAdmin ? (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
`ms-2 stop-light ${connected_participants.includes(p?.username) ? "success" : "danger"}`
|
|
||||||
}
|
|
||||||
></div>
|
|
||||||
) : null}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ListTag>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,26 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
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 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(() => {
|
useEffect(() => {
|
||||||
if (timeLeft <= 0) {
|
setTimeLeft(getTimeLeft(bidding_timer_end)); // reset timer when bidding_timer_end changes
|
||||||
|
|
||||||
|
if (getTimeLeft(bidding_timer_end) <= 0) {
|
||||||
if (onFinish) onFinish();
|
if (onFinish) onFinish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
const t = getTimeLeft(endTime);
|
const t = getTimeLeft(bidding_timer_end);
|
||||||
setTimeLeft(t);
|
setTimeLeft(t);
|
||||||
if (t <= 0 && onFinish) onFinish();
|
if (t <= 0 && onFinish) onFinish();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
// eslint-disable-next-line
|
}, [bidding_timer_end, onFinish]);
|
||||||
}, [endTime, onFinish, timeLeft]);
|
|
||||||
|
|
||||||
const minutes = Math.floor(timeLeft / 60);
|
const minutes = Math.floor(timeLeft / 60);
|
||||||
const secs = timeLeft % 60;
|
const secs = timeLeft % 60;
|
||||||
87
frontend/src/apps/draft/components/DraftMoviePool.jsx
Normal file
87
frontend/src/apps/draft/components/DraftMoviePool.jsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { isEmptyObject } from "../utils";
|
||||||
|
import { DraftMessage } from "../constants";
|
||||||
|
|
||||||
|
const NominateForm = ({ socket, currentUser, movie, className}) => {
|
||||||
|
|
||||||
|
const requestNomination = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const formData = new FormData(event.target)
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: DraftMessage.NOMINATION_SUBMIT_REQUEST,
|
||||||
|
payload: {
|
||||||
|
movie_id: formData.get('movie_id'),
|
||||||
|
user: currentUser
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={requestNomination} className={className}>
|
||||||
|
<input type="hidden" name="movie_id" value={movie.id} />
|
||||||
|
<button type="submit" className="btn btn-primary nominate">
|
||||||
|
Nominate
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DraftMoviePool = ({ socket, currentUser, currentUserStatus, draftDetails, draftState, isNominating = false }) => {
|
||||||
|
if (isEmptyObject(draftDetails)) { return }
|
||||||
|
const { movies } = draftDetails
|
||||||
|
const { current_movie } = draftState
|
||||||
|
const can_nominate = currentUserStatus?.can_nominate
|
||||||
|
const is_admin = currentUserStatus?.is_admin
|
||||||
|
|
||||||
|
const nominateHandler = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const formData = new FormData(event.target)
|
||||||
|
const movieId = formData.get('movie_id');
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: DraftMessage.NOMINATION_SUBMIT_REQUEST,
|
||||||
|
payload: {
|
||||||
|
movie_id: movieId,
|
||||||
|
user: currentUser
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="movie-pool-container">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Poster</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Release Date</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{movies.map(m => (
|
||||||
|
<tr key={m.id} className={`${current_movie == m.id ? "current-movie fw-bold" : null}`}>
|
||||||
|
<td><img src={m.tmdb_data.poster_url}></img></td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
<a href={`/api/movie/${m.id}/detail`} className="fs-5">
|
||||||
|
{m.title}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href={`https://www.themoviedb.org/movie/${m.tmdb_data.id}`}>
|
||||||
|
TMDB
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{can_nominate || is_admin ? (
|
||||||
|
<NominateForm socket={socket} currentUser={currentUser} movie={m} className={!can_nominate && is_admin ? 'admin-override' : ''}></NominateForm>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{m.tmdb_data.release_date}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
38
frontend/src/apps/draft/components/ParticipantList.jsx
Normal file
38
frontend/src/apps/draft/components/ParticipantList.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { fetchDraftDetails, isEmptyObject } from "../utils.js"
|
||||||
|
import Badge from 'react-bootstrap/Badge';
|
||||||
|
|
||||||
|
export const ParticipantList = ({ isAdmin, draftState, draftDetails, currentUser }) => {
|
||||||
|
if (isEmptyObject(draftState) || isEmptyObject(draftDetails)) { return }
|
||||||
|
const { draft_order, draft_index, connected_participants } = draftState
|
||||||
|
const { participants } = draftDetails
|
||||||
|
|
||||||
|
const ListTag = draft_order?.length > 0 ? "ol" : "ul"
|
||||||
|
const listItems = draft_order?.length > 0 ? draft_order.map(d => participants.find(p => p.username == d)) : participants
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListTag className="participant-list">
|
||||||
|
{listItems.map((p, idx) => (
|
||||||
|
<li className="team-item" key={idx}>
|
||||||
|
|
||||||
|
<div className={`team-name ${p.username == currentUser ? "current-user" : ""}`}>
|
||||||
|
<div>
|
||||||
|
{p.full_name}
|
||||||
|
{p.username == draftState.current_pick?.participant ? (<Badge bg="warning" className="ms-1">Current Pick</Badge>) : null}
|
||||||
|
</div>
|
||||||
|
<ul className="team-movie-list list-group list-group-flush">
|
||||||
|
<li className="team-movie-item list-group-item"></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{isAdmin === "True" ? (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
`ms-2 stop-light ${connected_participants.includes(p?.username) ? "success" : "danger"}`
|
||||||
|
}
|
||||||
|
></div>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ListTag>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,18 +10,22 @@ export const DraftMessage = {
|
|||||||
USER_JOIN_INFORM: "user.join.inform",
|
USER_JOIN_INFORM: "user.join.inform",
|
||||||
USER_LEAVE_INFORM: "user.leave.inform",
|
USER_LEAVE_INFORM: "user.leave.inform",
|
||||||
USER_IDENTIFICATION_INFORM: "user.identification.inform",
|
USER_IDENTIFICATION_INFORM: "user.identification.inform",
|
||||||
|
USER_STATE_INFORM: "user.state.inform",
|
||||||
PHASE_CHANGE_INFORM: "phase.change.inform",
|
PHASE_CHANGE_INFORM: "phase.change.inform",
|
||||||
PHASE_CHANGE_REQUEST: "phase.change.request",
|
PHASE_CHANGE_REQUEST: "phase.change.request",
|
||||||
PHASE_CHANGE_CONFIRM: "phase.change.confirm",
|
PHASE_CHANGE_CONFIRM: "phase.change.confirm",
|
||||||
STATUS_SYNC_REQUEST: "status.sync.request",
|
DRAFT_STATUS_REQUEST: "draft.status.request",
|
||||||
STATUS_SYNC_INFORM: "status.sync.inform",
|
DRAFT_STATUS_INFORM: "draft.status.sync.inform",
|
||||||
DRAFT_INDEX_ADVANCE_REQUEST: "draft.index.advance.request",
|
DRAFT_INDEX_ADVANCE_REQUEST: "draft.index.advance.request",
|
||||||
DRAFT_INDEX_ADVANCE_CONFIRM: "draft.index.advance.confirm",
|
DRAFT_INDEX_ADVANCE_CONFIRM: "draft.index.advance.confirm",
|
||||||
ORDER_DETERMINE_REQUEST: "order.determine.request",
|
ORDER_DETERMINE_REQUEST: "order.determine.request",
|
||||||
ORDER_DETERMINE_CONFIRM: "order.determine.confirm",
|
ORDER_DETERMINE_CONFIRM: "order.determine.confirm",
|
||||||
BID_START_INFORM: "bid.start.inform",
|
BID_START_INFORM: "bid.start.inform",
|
||||||
BID_START_REQUEST: "bid.start.request",
|
BID_START_REQUEST: "bid.start.request",
|
||||||
|
BID_START_REJECT: "bid.start.reject",
|
||||||
BID_PLACE_REQUEST: "bid.place.request",
|
BID_PLACE_REQUEST: "bid.place.request",
|
||||||
|
BID_PLACE_REJECT: "bid.place.reject",
|
||||||
|
BID_PLACE_CONFIRM: "bid.place.confirm",
|
||||||
BID_UPDATE_INFORM: "bid.update.inform",
|
BID_UPDATE_INFORM: "bid.update.inform",
|
||||||
BID_END_INFORM: "bid.end.inform",
|
BID_END_INFORM: "bid.end.inform",
|
||||||
NOMINATION_SUBMIT_REQUEST: "nomination.submit.request",
|
NOMINATION_SUBMIT_REQUEST: "nomination.submit.request",
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
// 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 { Collapse } from 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
|
||||||
import { Collapse, ListGroup } from "react-bootstrap";
|
|
||||||
|
|
||||||
|
|
||||||
const NominateMenu = ({ socket, draftState, draftDetails, currentUser, }) => {
|
|
||||||
if (!socket || isEmptyObject(draftDetails) || isEmptyObject(draftState)) return;
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const { movies } = draftDetails
|
|
||||||
|
|
||||||
const requestNomination = (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
const formData = new FormData(event.target)
|
|
||||||
socket.send(JSON.stringify({
|
|
||||||
type: DraftMessage.NOMINATION_SUBMIT_REQUEST,
|
|
||||||
payload: {
|
|
||||||
id: formData.get('movie'),
|
|
||||||
user: currentUser
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEmptyObject(draftState) || isEmptyObject(draftState.current_pick)) return;
|
|
||||||
|
|
||||||
if (currentUser == draftState.current_pick.participant) {
|
|
||||||
setOpen(true)
|
|
||||||
} else {
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// collapse.toggle()
|
|
||||||
}, [draftState])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapse in={open} className="nominate-menu">
|
|
||||||
<div> {/* Everything must be wrapped in one parent */}
|
|
||||||
<label>Nominate</label>
|
|
||||||
<div className="d-flex">
|
|
||||||
<form onSubmit={requestNomination}>
|
|
||||||
<select className="form-control" name="movie">
|
|
||||||
{movies.map(m => (
|
|
||||||
<option key={m.id} value={m.id}>{m.title}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button className="btn btn-primary">Nominate</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Collapse>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DraftParticipant = ({ draftSessionId }) => {
|
|
||||||
const socket = useWebSocket();
|
|
||||||
const [draftState, setDraftState] = useState({});
|
|
||||||
const [draftDetails, setDraftDetails] = useState({});
|
|
||||||
const [currentUser, setCurrentUser] = useState(null);
|
|
||||||
|
|
||||||
const [movies, setMovies] = useState([]);
|
|
||||||
console.log(socket)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDraftDetails(draftSessionId)
|
|
||||||
.then((data) => {
|
|
||||||
console.log("Fetched draft data", data)
|
|
||||||
setMovies(data.movies)
|
|
||||||
setDraftDetails(data)
|
|
||||||
})
|
|
||||||
}, [draftSessionId])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!socket) return;
|
|
||||||
socket.onclose = (event) => {
|
|
||||||
console.log('Websocket Closed')
|
|
||||||
}
|
|
||||||
}, [socket])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState)
|
|
||||||
const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser)
|
|
||||||
socket.addEventListener('message', draftStatusMessageHandler);
|
|
||||||
socket.addEventListener('message', userIdentifyMessageHandler);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.removeEventListener('message', draftStatusMessageHandler);
|
|
||||||
socket.removeEventListener('message', userIdentifyMessageHandler);
|
|
||||||
};
|
|
||||||
}, [socket]);
|
|
||||||
|
|
||||||
function submitBidRequest(event) {
|
|
||||||
event.preventDefault()
|
|
||||||
const form = event.target
|
|
||||||
const formData = new FormData(form)
|
|
||||||
console.log('submitting bid...')
|
|
||||||
socket.send(JSON.stringify({
|
|
||||||
type: DraftMessage.BID_PLACE_REQUEST,
|
|
||||||
payload: {
|
|
||||||
bid_amount: formData.get('bidAmount'),
|
|
||||||
user: currentUser
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="wrapper">
|
|
||||||
<section className="panel draft-live">
|
|
||||||
<header className="panel-header">
|
|
||||||
<h2 className="panel-title">Draft Live</h2>
|
|
||||||
<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 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>
|
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div className="bid-controls btn-group d-flex flex-column">
|
|
||||||
<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>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ul className="pick-list">
|
|
||||||
<li>
|
|
||||||
<div>Current Pick: {draftState.current_pick?.participant}</div>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
>
|
|
||||||
<div>Next Pick: {draftState.next_picks ? draftState.next_picks[0]?.participant : ""}</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="panel draft-catalog">
|
|
||||||
<header className="panel-header">
|
|
||||||
<h2 className="panel-title">Draft Catalog</h2>
|
|
||||||
</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>
|
|
||||||
<div className="movie-filters"></div>
|
|
||||||
|
|
||||||
<DraftMoviePool isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
<section className="panel my-team">
|
|
||||||
<header className="panel-header">
|
|
||||||
<h2 className="panel-title">My Team</h2>
|
|
||||||
</header>
|
|
||||||
<div className="panel-body">
|
|
||||||
<ul className="team-movie-list list-group">
|
|
||||||
<li className="team-movie-item list-group-item"></li>
|
|
||||||
</ul>
|
|
||||||
<div className="budget-status"></div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
|
||||||
<section className="panel teams">
|
|
||||||
<header className="panel-header">
|
|
||||||
<h2 className="panel-title">Teams</h2>
|
|
||||||
</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>
|
|
||||||
<ul className="team-movie-list list-group list-group-flush">
|
|
||||||
<li className="team-movie-item list-group-item"></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DraftMessage } from "../constants";
|
import { DraftMessage } from "./constants";
|
||||||
|
|
||||||
export async function fetchDraftDetails(draftSessionId) {
|
export async function fetchDraftDetails(draftSessionId) {
|
||||||
return fetch(`/api/draft/${draftSessionId}/`)
|
return fetch(`/api/draft/${draftSessionId}/`)
|
||||||
@@ -37,38 +37,12 @@ export function isEmptyObject(obj) {
|
|||||||
export const handleDraftStatusMessages = (event, setDraftState) => {
|
export const handleDraftStatusMessages = (event, setDraftState) => {
|
||||||
const message = JSON.parse(event.data);
|
const message = JSON.parse(event.data);
|
||||||
const { type, payload } = message;
|
const { type, payload } = message;
|
||||||
console.log("Message: ", type, event?.data);
|
|
||||||
|
|
||||||
if (!payload) return;
|
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(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) => {
|
export const handleUserIdentifyMessages = (event, setUser) => {
|
||||||
@@ -76,8 +50,16 @@ export const handleUserIdentifyMessages = (event, setUser) => {
|
|||||||
const { type, payload } = message;
|
const { type, payload } = message;
|
||||||
|
|
||||||
if (type == DraftMessage.USER_IDENTIFICATION_INFORM) {
|
if (type == DraftMessage.USER_IDENTIFICATION_INFORM) {
|
||||||
console.log("Message: ", type, event.data);
|
|
||||||
const { user } = payload;
|
const { user } = payload;
|
||||||
setUser(user);
|
setUser(user);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleUserStatusMessages = (event, setUserStatus) => {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
const { type, payload } = message;
|
||||||
|
|
||||||
|
if (type == DraftMessage.USER_STATE_INFORM) {
|
||||||
|
setUserStatus(payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,28 +2,28 @@ import './scss/styles.scss'
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { WebSocketProvider } from "./apps/draft/common/WebSocketContext.jsx";
|
import { WebSocketProvider } from "./apps/draft/components/WebSocketContext.jsx";
|
||||||
import { DraftAdmin } from "./apps/draft/admin/DraftAdmin.jsx";
|
import { DraftAdmin } from "./apps/draft/DraftAdminBar.jsx";
|
||||||
import { DraftParticipant} from './apps/draft/participant/DraftParticipant.jsx'
|
import { DraftParticipant} from './apps/draft/DraftDashboard.jsx'
|
||||||
import { DraftDebug} from './apps/draft/DraftDebug.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 draftPartipantRoot = document.getElementById("draft-participant-root")
|
||||||
const draftDebugRoot = document.getElementById("draft-debug-root")
|
const draftDebugRoot = document.getElementById("draft-debug-root")
|
||||||
const {draftSessionId} = window; // from backend template
|
const {draftSessionId, isAdmin} = window; // from backend template
|
||||||
|
|
||||||
if (draftPartipantRoot) {
|
if (draftPartipantRoot) {
|
||||||
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/participant`;
|
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/participant`;
|
||||||
createRoot(draftPartipantRoot).render(
|
createRoot(draftPartipantRoot).render(
|
||||||
<WebSocketProvider url={wsUrl}>
|
<WebSocketProvider url={wsUrl}>
|
||||||
<DraftParticipant draftSessionId={draftSessionId} />
|
<DraftParticipant draftSessionId={draftSessionId} className={`${isAdmin ? 'admin':''}`}/>
|
||||||
</WebSocketProvider>
|
</WebSocketProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (draftAdminRoot) {
|
if (draftAdminBarRoot) {
|
||||||
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/admin`;
|
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/admin`;
|
||||||
createRoot(draftAdminRoot).render(
|
createRoot(draftAdminBarRoot).render(
|
||||||
<WebSocketProvider url={wsUrl}>
|
<WebSocketProvider url={wsUrl}>
|
||||||
<DraftAdmin draftSessionId={draftSessionId}/>
|
<DraftAdmin draftSessionId={draftSessionId}/>
|
||||||
</WebSocketProvider>
|
</WebSocketProvider>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@use "../../node_modules/bootstrap/scss/bootstrap.scss";
|
@use "../../node_modules/bootstrap/scss/bootstrap.scss";
|
||||||
@use "./fonts/graphique.css";
|
@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 only functions & variables
|
||||||
@import "~bootstrap/scss/functions";
|
@import "~bootstrap/scss/functions";
|
||||||
@import "~bootstrap/scss/variables";
|
@import "~bootstrap/scss/variables";
|
||||||
@@ -95,40 +95,83 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.participant-list-container,
|
ol.participant-list {
|
||||||
.movie-pool-container {
|
@extend .list-group-numbered;
|
||||||
max-width: 575.98px;
|
}
|
||||||
label {
|
|
||||||
@extend .fs-3;
|
ol.participant-list,
|
||||||
}
|
ul.participant-list {
|
||||||
@extend .list-group;
|
@extend .list-group;
|
||||||
ol,
|
|
||||||
ul {
|
|
||||||
@extend .p-0;
|
|
||||||
}
|
|
||||||
ol {
|
|
||||||
@extend .list-group-numbered;
|
|
||||||
}
|
|
||||||
li {
|
li {
|
||||||
@extend .list-group-item;
|
@extend .list-group-item;
|
||||||
@extend .d-flex;
|
@extend .d-flex;
|
||||||
@extend .justify-content-between;
|
@extend .justify-content-between;
|
||||||
@extend .align-items-center;
|
@extend .align-items-center;
|
||||||
span {
|
.team-name {
|
||||||
@extend .me-auto;
|
@extend .flex-grow-1;
|
||||||
@extend .ps-1;
|
@extend .ps-2;
|
||||||
|
}
|
||||||
|
.team-movie-list {
|
||||||
|
li {
|
||||||
|
@extend .p-0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.current-user {
|
.current-user {
|
||||||
|
@extend .fw-bold;
|
||||||
&::after {
|
&::after {
|
||||||
content: " *";
|
// content: " *";
|
||||||
font-size: 1em; // adjust as needed
|
font-size: 1em; // adjust as needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#draft-participant-root,
|
.movie-pool-container {
|
||||||
#draft-admin-root {
|
img {
|
||||||
|
height: 128px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
@extend .text-decoration-none;
|
||||||
|
@extend .text-reset;
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
display: block;
|
||||||
|
// height: 200px; /* or any desired height */
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
width: 150px; /* Set consistent widths to align columns */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-override {
|
||||||
|
button {
|
||||||
|
@extend .btn-warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#draft-participant-root {
|
||||||
@extend .flex-grow-1;
|
@extend .flex-grow-1;
|
||||||
.wrapper:first-child {
|
.wrapper:first-child {
|
||||||
@extend .p-2;
|
@extend .p-2;
|
||||||
@@ -137,47 +180,65 @@
|
|||||||
gap: 1rem; /* space between panels */
|
gap: 1rem; /* space between panels */
|
||||||
justify-content: center; /* center the panels horizontally */
|
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 {
|
.panel {
|
||||||
@extend .border;
|
@extend .border;
|
||||||
@extend .shadow-sm;
|
@extend .shadow-sm;
|
||||||
@extend .rounded-2;
|
@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 {
|
header.panel-header {
|
||||||
@extend .p-1;
|
@extend .p-1;
|
||||||
@extend .text-uppercase;
|
@extend .text-uppercase;
|
||||||
@extend .align-items-center;
|
@extend .align-items-center;
|
||||||
@extend .border-bottom;
|
@extend .border-bottom;
|
||||||
@extend .border-secondary;
|
@extend .border-2;
|
||||||
background-color: $blue-100;
|
@extend .border-secondary-subtle;
|
||||||
|
// background-color: $blue-100;
|
||||||
|
@extend .bg-dark;
|
||||||
|
@extend .bg-gradient;
|
||||||
|
@extend .text-light;
|
||||||
@extend .rounded-top-2;
|
@extend .rounded-top-2;
|
||||||
.panel-title {
|
.panel-title {
|
||||||
|
@extend .ms-2;
|
||||||
@extend .fw-bold;
|
@extend .fw-bold;
|
||||||
@extend .fs-5;
|
@extend .fs-5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.panel.draft-live {
|
.bids-container {
|
||||||
|
overflow: scroll;
|
||||||
|
height: 85px;
|
||||||
|
}
|
||||||
|
#draft-live {
|
||||||
header.panel-header {
|
header.panel-header {
|
||||||
@extend .d-flex;
|
@extend .d-flex;
|
||||||
@extend .justify-content-between;
|
@extend .justify-content-between;
|
||||||
}
|
}
|
||||||
.draft-live-state-container {
|
#draft-clock {
|
||||||
@extend .d-flex;
|
@extend .row;
|
||||||
background-color: $green-100;
|
@extend .g-0;
|
||||||
|
// background-color: $green-100;
|
||||||
|
@extend .text-light;
|
||||||
|
@extend .text-bg-dark;
|
||||||
|
@extend .lh-1;
|
||||||
.countdown-clock {
|
.countdown-clock {
|
||||||
@extend .fs-1;
|
font-family: "League Gothic";
|
||||||
@extend .fw-bold;
|
font-size: $font-size-base * 5;
|
||||||
|
@extend .fw-bolder;
|
||||||
@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;
|
||||||
|
@extend .align-content-center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div:has(.pick-list), div:has(.bid-list){
|
div:has(.pick-list),
|
||||||
|
div:has(.bid-list) {
|
||||||
ul {
|
ul {
|
||||||
@extend .list-group;
|
@extend .list-group;
|
||||||
}
|
}
|
||||||
|
|||||||
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