diff --git a/api/serializers.py b/api/serializers.py index 5362de8..bfd3ddf 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -2,6 +2,7 @@ from rest_framework import serializers from django.contrib.auth import get_user_model from boxofficefantasy.models import Movie, Season from draft.models import DraftSession, DraftSessionSettings, DraftPick +from boxofficefantasy.integrations.tmdb import get_tmdb_movie_by_imdb User = get_user_model() @@ -16,10 +17,27 @@ class UserSerializer(serializers.ModelSerializer): return f"{obj.first_name} {obj.last_name}".strip() 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: model = Movie # fields = ("id", "imdb_id", "title", "year", "poster_url") - fields = ("id", "title") + fields = ("id", "title", "tmdb_data") class DraftSessionSettingsSerializer(serializers.ModelSerializer): class Meta: diff --git a/boxofficefantasy/integrations/tmdb.py b/boxofficefantasy/integrations/tmdb.py index 8e8cec6..e68b548 100644 --- a/boxofficefantasy/integrations/tmdb.py +++ b/boxofficefantasy/integrations/tmdb.py @@ -12,13 +12,15 @@ tmdb.language = "en" 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. """ cache_key = f"tmdb:movie:{imdb_id}" cached = cache.get(cache_key) if cached: + if cache_poster and not cached.get('poster_url'): + cached['poster_url'] = cache_tmdb_poster(cached['poster_path']) return cached 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] 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 diff --git a/draft/constants.py b/draft/constants.py index db339bc..b08c189 100644 --- a/draft/constants.py +++ b/draft/constants.py @@ -11,6 +11,7 @@ class DraftMessage(StrEnum): USER_JOIN_INFORM = "user.join.inform" # server -> client 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_STATE_INFORM = "user.state.inform" # Phase control PHASE_CHANGE_INFORM = "phase.change.inform" # server -> client (target phase payload) @@ -31,8 +32,10 @@ class DraftMessage(StrEnum): # Bidding (examples, adjust to your flow) BID_START_INFORM = "bid.start.inform" # 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_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_END_INFORM = "bid.end.inform" # server -> client (winner) diff --git a/draft/consumers.py b/draft/consumers.py index f1a1875..02cea23 100644 --- a/draft/consumers.py +++ b/draft/consumers.py @@ -4,23 +4,19 @@ from django.core.exceptions import PermissionDenied from boxofficefantasy.models import League, Season from boxofficefantasy.views import parse_season_slug from draft.models import DraftSession, DraftSessionParticipant -import asyncio from django.contrib.auth.models import User from draft.constants import ( DraftMessage, DraftPhase, DraftGroupChannelNames, ) -from draft.state import DraftStateManager +from draft.state import DraftStateManager, DraftStateException from typing import Any import logging logger = logging.getLogger(__name__) # __name__ = module path -import random - - class DraftConsumerBase(AsyncJsonWebsocketConsumer): group_names: DraftGroupChannelNames draft_state: DraftStateManager @@ -61,6 +57,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): return else: await self.accept() + self.draft_state.connect_participant(self.user.username) await self.channel_layer.group_add( self.group_names.session, self.channel_name ) @@ -72,14 +69,6 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): "payload": {"user": self.user.username}, }, ) - await self.channel_layer.group_send( - self.group_names.session, - { - "type": "direct.message", - "subtype": DraftMessage.DRAFT_STATUS_INFORM, - "payload": self.draft_state.to_dict(), - }, - ) await self.channel_layer.send( self.channel_name, { @@ -88,6 +77,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): "payload": {"user": self.user.username}, }, ) + await self.broadcast_state() async def should_accept_user(self) -> bool: return self.user.is_authenticated @@ -106,6 +96,14 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): # --- 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, { @@ -117,6 +115,14 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): 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, { @@ -214,6 +220,18 @@ class DraftAdminConsumer(DraftConsumerBase): }, ) 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() case DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST: self.draft_state.draft_index_advance() @@ -245,16 +263,26 @@ class DraftAdminConsumer(DraftConsumerBase): 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() + 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 === @@ -314,12 +342,14 @@ class DraftParticipantConsumer(DraftConsumerBase): }, }, ) + await self.broadcast_state() await super().disconnect(close_code) self.draft_state.disconnect_participant(self.user.username) await self.channel_layer.group_discard( self.group_names.session, self.channel_name ) + def should_accept_user(self): return super().should_accept_user() and self.user in self.draft_participants @@ -333,7 +363,7 @@ class DraftParticipantConsumer(DraftConsumerBase): "type": "broadcast.admin", "subtype": event_type, "payload": { - "movie_id": content.get("payload", {}).get("id"), + "movie_id": content.get("payload", {}).get("movie_id"), "user": content.get("payload", {}).get("user"), }, }, @@ -341,15 +371,26 @@ class DraftParticipantConsumer(DraftConsumerBase): if event_type == DraftMessage.BID_PLACE_REQUEST: 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, - { - "type": "broadcast.session", - "subtype": DraftMessage.BID_PLACE_CONFIRM, - "payload": {**self.draft_state}, - }, - ) + try: + self.draft_state.place_bid(self.user, bid_amount) + await self.channel_layer.group_send( + self.group_names.session, + { + "type": "broadcast.session", + "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 === @@ -358,11 +399,8 @@ class DraftParticipantConsumer(DraftConsumerBase): # === Draft === - async def nominate(self, movie_title): ... - async def place_bid(self, amount, user): ... - - # === Example DB Access === + # === DB Access === @database_sync_to_async def add_draft_participant(self): diff --git a/draft/state.py b/draft/state.py index 86df768..87ff7e0 100644 --- a/draft/state.py +++ b/draft/state.py @@ -22,9 +22,11 @@ class DraftCache: bids: str bid_timer_start: str bid_timer_end: str + connected_participants: str _cached_properties = { "participants", + "connected_participants", "phase", "draft_order", "draft_index", @@ -32,7 +34,6 @@ class DraftCache: "bids", "bid_timer_start", "bid_timer_end", - } def __init__(self, draft_id: str, cache: BaseCache = cache): @@ -71,8 +72,7 @@ class DraftStateManager: self.session_id: str = session.hashid self.cache: DraftCache = DraftCache(self.session_id, cache) self.settings: DraftSessionSettings = session.settings - self.participants: set[User] = set(session.participants.all()) - self.connected_participants: set[User] = set() + self._participants = list(session.participants.all()) # === Phase Management === @property @@ -85,12 +85,21 @@ class DraftStateManager: # === Connected Users === + @property + def connected_participants(self): + return set(json.loads(self.cache.connected_participants or "[]")) + def connect_participant(self, username: str): - self.connected_participants.add(username) - return self.connected_participants + connected_participants = self.connected_participants + connected_participants.add(username) + self.cache.connected_participants = json.dumps(list(connected_participants)) + return connected_participants def disconnect_participant(self, username: str): - self.connected_participants.discard(username) + connected_participants = self.connected_participants + connected_participants.discard(username) + self.cache.connected_participants = json.dumps(list(connected_participants)) + return connected_participants # === Draft Order === @property @@ -107,7 +116,7 @@ class DraftStateManager: self.phase = DraftPhase.DETERMINE_ORDER self.draft_index = 0 draft_order = random.sample( - list(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 @@ -169,18 +178,29 @@ class DraftStateManager: if isinstance(amount, str): amount = int(amount) bids = self.get_bids() - bids.append({"user":user.username, "amount":amount}) + user_state = self.user_state(user) + 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: return json.loads(self.cache.bids or "[]") def current_movie(self) -> Movie | None: - movie_id = self.current_movie - return Movie.objects.filter(pk=movie_id).first() if movie_id else None + movie_id = self.cache.current_movie + return movie_id if movie_id else None 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 start_time = time.time() end_time = start_time + seconds @@ -202,6 +222,7 @@ class DraftStateManager: "draft_index": self.draft_index, "connected_participants": list(self.connected_participants), "current_movie": self.cache.current_movie, + "awards": [], "bids": self.get_bids(), "bidding_timer_end": self.get_timer_end(), "bidding_timer_start": self.get_timer_start(), @@ -209,6 +230,17 @@ class DraftStateManager: "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): # return self.get_summary() diff --git a/draft/templates/draft/room.dj.html b/draft/templates/draft/room.dj.html index 9aa5f24..de8c544 100644 --- a/draft/templates/draft/room.dj.html +++ b/draft/templates/draft/room.dj.html @@ -3,10 +3,11 @@ {% load static %} -
{% if user.is_staff %}
You are admin!
{% endif %} +
{% endblock body %} \ No newline at end of file diff --git a/frontend/src/apps/draft/DraftAdminBar.jsx b/frontend/src/apps/draft/DraftAdminBar.jsx index ef708e8..61ec15b 100644 --- a/frontend/src/apps/draft/DraftAdminBar.jsx +++ b/frontend/src/apps/draft/DraftAdminBar.jsx @@ -38,7 +38,6 @@ export const DraftAdmin = ({ draftSessionId }) => { useEffect(() => { fetchDraftDetails(draftSessionId) .then((data) => { - console.log("Fetched draft data", data) setDraftDetails(data) }) }, []) diff --git a/frontend/src/apps/draft/DraftDashboard.jsx b/frontend/src/apps/draft/DraftDashboard.jsx index 4ebca86..2acc3f5 100644 --- a/frontend/src/apps/draft/DraftDashboard.jsx +++ b/frontend/src/apps/draft/DraftDashboard.jsx @@ -3,73 +3,24 @@ import React, { useEffect, useState, useRef } from "react"; 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 { 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 } from './utils.js' +import { handleDraftStatusMessages, handleUserStatusMessages, handleUserIdentifyMessages } from './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 ( - -
{/* Everything must be wrapped in one parent */} - -
-
- - -
-
-
-
- ); -} +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([]); - console.log(socket) useEffect(() => { fetchDraftDetails(draftSessionId) @@ -85,12 +36,15 @@ export const DraftParticipant = ({ draftSessionId }) => { 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]); @@ -98,7 +52,6 @@ export const DraftParticipant = ({ draftSessionId }) => { 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: { @@ -108,8 +61,12 @@ export const DraftParticipant = ({ draftSessionId }) => { })) } + + const currentUserStatus = userStatus.find(u => u.user == currentUser) + const currentMovie = movies.find(i => draftState.current_movie == i.id) + return ( -
+
@@ -120,64 +77,55 @@ export const DraftParticipant = ({ draftSessionId }) => {
-
- -
-
Round {draftState.current_pick?.round}
-
Pick {draftState.current_pick?.pick_in_round}
-
{draftState.current_pick?.overall + 1} Overall
-
-
-
- - Show Content -
-
-
-
-
- Movie title -
-
-
-
-
Bids
-
-
    - {draftState.bids?.reverse().map((b,idx) => ( -
  1. -
    -
    {b.user}
    -
    {b.amount}
    -
    -
  2. - ))} - -
-
-
-
-
-
- Bid - - -
-
-
-
- -
-
    -
  • -
    Current Pick: {draftState.current_pick?.participant}
    -
  • -
  • -
    Next Pick: {draftState.next_picks ? draftState.next_picks[0]?.participant : ""}
    -
  • -
+
+ +
+
Round {draftState.current_pick?.round}
+
Pick {draftState.current_pick?.pick_in_round}
+
{draftState.current_pick?.overall + 1} Overall
+
+
+
+ +
+
+
+
+ +
+
+ {currentMovie?.title} +
+
+
+
+
Bids
+
+
    + {draftState.bids?.reverse().map((b, idx) => ( +
  1. +
    +
    {b.user}
    +
    {b.amount}
    +
    +
  2. + ))} +
+
+
+
+
+
+ Bid + + +
+
+
+
+
+
@@ -185,16 +133,11 @@ export const DraftParticipant = ({ draftSessionId }) => {
-
Draft Catalog
+
Films
-
- Current Nomination: {movies.find(i => draftState.current_movie == i.id)?.title} -
- {/* */}
- - +
@@ -217,16 +160,7 @@ export const DraftParticipant = ({ draftSessionId }) => {
Teams
- +
diff --git a/frontend/src/apps/draft/components/DraftCountdownClock.jsx b/frontend/src/apps/draft/components/DraftCountdownClock.jsx index eeb4fed..3b9894a 100644 --- a/frontend/src/apps/draft/components/DraftCountdownClock.jsx +++ b/frontend/src/apps/draft/components/DraftCountdownClock.jsx @@ -1,24 +1,26 @@ import React, { useEffect, useState } from "react"; export function DraftCountdownClock({ draftState }) { - // endTime is in seconds (Unix time) - const {bidding_timer_end, onFinish} = draftState + const { bidding_timer_end, onFinish } = draftState; const getTimeLeft = (et) => Math.max(0, Math.floor(et - Date.now() / 1000)); const [timeLeft, setTimeLeft] = useState(getTimeLeft(bidding_timer_end)); 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(); return; } + const timer = setInterval(() => { const t = getTimeLeft(bidding_timer_end); setTimeLeft(t); if (t <= 0 && onFinish) onFinish(); }, 100); + return () => clearInterval(timer); - // eslint-disable-next-line - }, [bidding_timer_end, onFinish, timeLeft]); + }, [bidding_timer_end, onFinish]); const minutes = Math.floor(timeLeft / 60); const secs = timeLeft % 60; diff --git a/frontend/src/apps/draft/components/DraftMoviePool.jsx b/frontend/src/apps/draft/components/DraftMoviePool.jsx index cc733d4..9b17c74 100644 --- a/frontend/src/apps/draft/components/DraftMoviePool.jsx +++ b/frontend/src/apps/draft/components/DraftMoviePool.jsx @@ -1,23 +1,87 @@ import React from "react"; import { isEmptyObject } from "../utils"; +import { DraftMessage } from "../constants"; -export const DraftMoviePool = ({ isParticipant, draftDetails, draftState }) => { - if(isEmptyObject(draftDetails)) {return} - const {movies} = draftDetails - const {current_movie} = draftState +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 ( +
+ + +
+ ); +} + +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 (
- - + + + + + + + + + + {movies.map(m => ( + + + + + + ))} + +
PosterTitleRelease Date
+ + +
+ {can_nominate || is_admin ? ( + + ) : null} +
+
{m.tmdb_data.release_date}
) } \ No newline at end of file diff --git a/frontend/src/apps/draft/components/ParticipantList.jsx b/frontend/src/apps/draft/components/ParticipantList.jsx index 0bd97c4..1a537c5 100644 --- a/frontend/src/apps/draft/components/ParticipantList.jsx +++ b/frontend/src/apps/draft/components/ParticipantList.jsx @@ -1,32 +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)) { console.warn('empty draft state', draftState); return } + 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 ( -
- - - {listItems.map((p, i) => ( -
  • - {p?.full_name} - {isAdmin ? ( -
    + {listItems.map((p, idx) => ( +
  • + +
    +
    + {p.full_name} + {p.username == draftState.current_pick?.participant ? (Current Pick) : null} +
    +
      +
    • +
    +
    + {isAdmin === "True" ? ( +
    - ) : null} -
  • - ))} -
    -
    + ) : null} + + ))} + ) } \ No newline at end of file diff --git a/frontend/src/apps/draft/constants.js b/frontend/src/apps/draft/constants.js index fddd87e..0341515 100644 --- a/frontend/src/apps/draft/constants.js +++ b/frontend/src/apps/draft/constants.js @@ -10,6 +10,7 @@ export const DraftMessage = { USER_JOIN_INFORM: "user.join.inform", USER_LEAVE_INFORM: "user.leave.inform", USER_IDENTIFICATION_INFORM: "user.identification.inform", + USER_STATE_INFORM: "user.state.inform", PHASE_CHANGE_INFORM: "phase.change.inform", PHASE_CHANGE_REQUEST: "phase.change.request", PHASE_CHANGE_CONFIRM: "phase.change.confirm", @@ -21,8 +22,10 @@ export const DraftMessage = { ORDER_DETERMINE_CONFIRM: "order.determine.confirm", BID_START_INFORM: "bid.start.inform", BID_START_REQUEST: "bid.start.request", + BID_START_REJECT: "bid.start.reject", BID_PLACE_REQUEST: "bid.place.request", - BID_PLACE_CONFIRM: "bid.update.confirm", + BID_PLACE_REJECT: "bid.place.reject", + BID_PLACE_CONFIRM: "bid.place.confirm", BID_UPDATE_INFORM: "bid.update.inform", BID_END_INFORM: "bid.end.inform", NOMINATION_SUBMIT_REQUEST: "nomination.submit.request", diff --git a/frontend/src/apps/draft/utils.js b/frontend/src/apps/draft/utils.js index 13caa00..c836f6f 100644 --- a/frontend/src/apps/draft/utils.js +++ b/frontend/src/apps/draft/utils.js @@ -50,8 +50,16 @@ export const handleUserIdentifyMessages = (event, setUser) => { const { type, payload } = message; if (type == DraftMessage.USER_IDENTIFICATION_INFORM) { - console.log("Message: ", type, event.data); const { user } = payload; 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); + } +}; \ No newline at end of file diff --git a/frontend/src/index.js b/frontend/src/index.js index 4b06521..7487b21 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -11,13 +11,13 @@ import { DraftDebug} from './apps/draft/DraftDebug.jsx' 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 +const {draftSessionId, isAdmin} = window; // from backend template if (draftPartipantRoot) { const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/participant`; createRoot(draftPartipantRoot).render( - + ); } diff --git a/frontend/src/scss/styles.scss b/frontend/src/scss/styles.scss index 505cf95..6ee9621 100644 --- a/frontend/src/scss/styles.scss +++ b/frontend/src/scss/styles.scss @@ -1,6 +1,6 @@ @use "../../node_modules/bootstrap/scss/bootstrap.scss"; @use "./fonts/graphique.css"; -@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=League+Gothic&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"; @@ -95,38 +95,62 @@ } } -.participant-list-container, -.movie-pool-container { - max-width: 575.98px; - label { - @extend .fs-3; - } +ol.participant-list { + @extend .list-group-numbered; +} + +ol.participant-list, +ul.participant-list { @extend .list-group; - ol, - ul { - @extend .p-0; - } - ol { - @extend .list-group-numbered; - } li { @extend .list-group-item; @extend .d-flex; @extend .justify-content-between; @extend .align-items-center; - span { - @extend .me-auto; - @extend .ps-1; + .team-name { + @extend .flex-grow-1; + @extend .ps-2; + } + .team-movie-list { + li { + @extend .p-0; + } } } .current-user { + @extend .fw-bold; &::after { - content: " *"; + // content: " *"; font-size: 1em; // adjust as needed } } } +.movie-pool-container { + 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; @@ -137,7 +161,13 @@ @extend .shadow-sm; div { @extend .d-flex; - @extend .justify-content-center + @extend .justify-content-center; + } +} + +.admin-override { + button { + @extend .btn-warning; } } @@ -195,7 +225,7 @@ @extend .text-bg-dark; @extend .lh-1; .countdown-clock { - font-family: 'League Gothic'; + font-family: "League Gothic"; font-size: $font-size-base * 5; @extend .fw-bolder; @extend .col; @@ -207,7 +237,8 @@ @extend .align-content-center; } } - div:has(.pick-list), div:has(.bid-list){ + div:has(.pick-list), + div:has(.bid-list) { ul { @extend .list-group; }