diff --git a/boxofficefantasy/templates/base.dj.html b/boxofficefantasy/templates/base.dj.html index e624ee1..5e1ed86 100644 --- a/boxofficefantasy/templates/base.dj.html +++ b/boxofficefantasy/templates/base.dj.html @@ -9,9 +9,9 @@ /> {% if DEBUG %} - + {% else %} - + {% endif %} diff --git a/draft/consumers.py b/draft/consumers.py index 851ed4b..846eb79 100644 --- a/draft/consumers.py +++ b/draft/consumers.py @@ -40,7 +40,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): self.group_names = DraftGroupChannelNames(draft_hashid) self.cache_keys = DraftCacheKeys(draft_hashid) - self.draft_state = DraftStateManager(draft_hashid) + self.draft_state = DraftStateManager(draft_hashid, self.draft_session.settings) self.user = self.scope["user"] if not self.should_accept_user(): @@ -133,7 +133,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer): draft_session_id = DraftSession.decode_id(draft_session_id_hashed) if draft_session_id: draft_session = DraftSession.objects.select_related( - "season", "season__league" + "season", "season__league", "settings" ).get(pk=draft_session_id) else: raise Exception() @@ -198,14 +198,17 @@ class DraftAdminConsumer(DraftConsumerBase): } ) if event_type == DraftMessage.BID_START_REQUEST: - self.draft_state + self.draft_state.start_timer() await self.channel_layer.group_send( self.group_names.session, { "type": "broadcast.session", "subtype": DraftMessage.BID_START_INFORM, "payload": { - "current_movie": self.draft_state.get_summary()['current_movie'] + "current_movie": self.draft_state.get_summary()['current_movie'], + "bidding_duration": self.draft_state.settings.bidding_duration, + "bidding_timer_end": self.draft_state.get_timer_end(), + "bidding_timer_start": self.draft_state.get_timer_start() } } ) diff --git a/draft/migrations/0007_draftsessionsettings_bidding_duration_and_more.py b/draft/migrations/0007_draftsessionsettings_bidding_duration_and_more.py new file mode 100644 index 0000000..22c5784 --- /dev/null +++ b/draft/migrations/0007_draftsessionsettings_bidding_duration_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.4 on 2025-08-10 22:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('boxofficefantasy', '0009_alter_moviemetric_value_alter_pick_bid_amount_and_more'), + ('draft', '0006_remove_draftparticipant_draft_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='draftsessionsettings', + name='bidding_duration', + field=models.IntegerField(default=90), + ), + migrations.AlterField( + model_name='draftpick', + name='draft', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='draft_picks', to='draft.draftsession'), + ), + migrations.AlterField( + model_name='draftsession', + name='movies', + field=models.ManyToManyField(blank=True, related_name='draft_sessions', to='boxofficefantasy.movie'), + ), + migrations.AlterField( + model_name='draftsessionparticipant', + name='draft_session', + field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='draft.draftsession'), + ), + ] diff --git a/draft/models.py b/draft/models.py index b116979..a3d482f 100644 --- a/draft/models.py +++ b/draft/models.py @@ -62,6 +62,7 @@ class DraftSessionSettings(Model): draft_session = OneToOneField( DraftSession, on_delete=CASCADE, related_name="settings" ) + bidding_duration = IntegerField(default=90) def __str__(self): return f"Settings for {self.draft_session}" diff --git a/draft/state.py b/draft/state.py index 301f436..586c8f2 100644 --- a/draft/state.py +++ b/draft/state.py @@ -4,6 +4,8 @@ from datetime import datetime, timedelta from boxofficefantasy.models import Movie from django.contrib.auth.models import User from draft.constants import DraftPhase +from draft.models import DraftSessionSettings +import time class DraftCacheKeys: def __init__(self, id): @@ -57,9 +59,12 @@ class DraftCacheKeys: # def participants(self): # return f"{self.prefix}:participants" - # @property - # def bid_timer_end(self): - # return f"{self.prefix}:bid_timer_end" + @property + def bid_timer_end(self): + return f"{self.prefix}:bid_timer_end" + @property + 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" @@ -68,12 +73,12 @@ class DraftCacheKeys: # return f"{self.prefix}:user:{user_id}:channel" class DraftStateManager: - def __init__(self, session_id: int): + def __init__(self, session_id: int, settings: DraftSessionSettings): self.session_id = session_id self.cache = cache self.keys = DraftCacheKeys(session_id) self._initial_phase = self.cache.get(self.keys.phase, DraftPhase.WAITING.value) - + self.settings = settings # === Phase Management === @property @@ -135,13 +140,18 @@ class DraftStateManager: movie_id = self.cache.get(self.keys.current_movie) return Movie.objects.filter(pk=movie_id).first() if movie_id else None - def start_timer(self, seconds: int): - end_time = (datetime.now() + timedelta(seconds=seconds)).isoformat() - self.cache.set(self.keys.timer_end, end_time) - self.cache.set(self.keys.timer_end, end_time) + def start_timer(self): + seconds = self.settings.bidding_duration + start_time = time.time() + end_time = start_time + seconds + self.cache.set(self.keys.bid_timer_end, end_time) + self.cache.set(self.keys.bid_timer_start, start_time) def get_timer_end(self) -> str | None: - return self.cache.get(self.keys.timer_end).decode("utf-8") if self.cache.get(self.keys.timer_end) else None + return self.cache.get(self.keys.bid_timer_end) + + def get_timer_start(self) -> str | None: + return self.cache.get(self.keys.bid_timer_start) # === Sync Snapshot === def get_summary(self) -> dict: @@ -152,5 +162,6 @@ class DraftStateManager: "connected_participants": self.connected_participants, "current_movie": self.cache.get(self.keys.current_movie), # "bids": self.get_bids(), - # "timer_end": self.get_timer_end(), + "bidding_timer_end": self.get_timer_end(), + "bidding_timer_start": self.get_timer_start(), } \ No newline at end of file diff --git a/draft/templates/draft/room_admin.dj.html b/draft/templates/draft/room_admin.dj.html index 72b2883..8f0bc47 100644 --- a/draft/templates/draft/room_admin.dj.html +++ b/draft/templates/draft/room_admin.dj.html @@ -6,9 +6,5 @@ window.draftSessionId = "{{ draft_id_hashed }}"
-{% if DEBUG %} - -{% else %} - -{% endif %} + {% endblock %} \ No newline at end of file diff --git a/draft/templates/draft/room_debug.dj.html b/draft/templates/draft/room_debug.dj.html new file mode 100644 index 0000000..9524d50 --- /dev/null +++ b/draft/templates/draft/room_debug.dj.html @@ -0,0 +1,14 @@ +{% load static %} + +{% if DEBUG %} + +{% else %} + +{% endif %} + + + +
+ \ No newline at end of file diff --git a/draft/urls.py b/draft/urls.py index 33dcd59..71c420f 100644 --- a/draft/urls.py +++ b/draft/urls.py @@ -6,6 +6,6 @@ app_name = "draft" urlpatterns = [ # path("", views.draft_room, name="room"), path("session//", views.draft_room, name="session"), - path("session//", views.draft_room, name="admin_session"), + path("session//", views.draft_room, name="admin_session"), # path("//", views.draft_room_list, name="room"), ] \ No newline at end of file diff --git a/draft/views.py b/draft/views.py index 0bb8b9e..6e77179 100644 --- a/draft/views.py +++ b/draft/views.py @@ -6,7 +6,7 @@ from django.contrib.auth.decorators import login_required from boxofficefantasy_project.utils import decode_id @login_required(login_url='/login/') -def draft_room(request, league_slug=None, season_slug=None, draft_session_id_hashed=None, is_admin=""): +def draft_room(request, league_slug=None, season_slug=None, draft_session_id_hashed=None, subpage=""): if draft_session_id_hashed: draft_session_id = decode_id(draft_session_id_hashed) draft_session = get_object_or_404(DraftSession, id=draft_session_id) @@ -25,8 +25,9 @@ def draft_room(request, league_slug=None, season_slug=None, draft_session_id_has "season": season, } - if is_admin == "admin": + if subpage == "admin": return render(request, "draft/room_admin.dj.html", context) + elif subpage == "debug": + return render(request, "draft/room_debug.dj.html", context) else: return render(request, "draft/room.dj.html", context) - diff --git a/frontend/src/apps/draft/DraftDebug.jsx b/frontend/src/apps/draft/DraftDebug.jsx new file mode 100644 index 0000000..32d9565 --- /dev/null +++ b/frontend/src/apps/draft/DraftDebug.jsx @@ -0,0 +1,45 @@ +import React, { useEffect, useState } from "react";; +import { useWebSocket } from "./WebSocketContext.jsx"; +import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from './constants.js'; +import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./common/utils.js" + +export const DraftDebug = ({ draftSessionId }) => { + const [draftState, setDraftState] = useState({}) + const socket = useWebSocket(); + if (!socket) return; + + useEffect(() => { + if (!socket) return; + const openHandler = (event) => { + console.log('Websocket Opened') + } + const closeHandler = (event) => { + console.log('Websocket Closed') + } + socket.addEventListener('open', openHandler); + socket.addEventListener('close', closeHandler); + return () => { + socket.removeEventListener('open', openHandler); + socket.removeEventListener('close', closeHandler); + } + }, [socket]) + + useEffect(() => { + if (!socket) return; + + const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState) + + socket.addEventListener('message', draftStatusMessageHandler); + + + return () => { + socket.removeEventListener('message', draftStatusMessageHandler) + }; + }, [socket]); + const data = { 'message': 'test' } + return (
+    {JSON.stringify(draftState, null, 2)}
+  
+ ) + +} diff --git a/frontend/src/apps/draft/admin/DraftAdmin.jsx b/frontend/src/apps/draft/admin/DraftAdmin.jsx index 2153da8..5053757 100644 --- a/frontend/src/apps/draft/admin/DraftAdmin.jsx +++ b/frontend/src/apps/draft/admin/DraftAdmin.jsx @@ -6,6 +6,7 @@ import { ParticipantList } from "../common/ParticipantList.jsx"; import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '../constants.js'; import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "../common/utils.js" import { DraftMoviePool } from "../common/DraftMoviePool.jsx" +import { DraftCountdownClock } from "../common/DraftCountdownClock.jsx" import { jsxs } from "react/jsx-runtime"; @@ -69,7 +70,6 @@ export const DraftAdmin = ({ draftSessionId }) => { const handleNominationRequest = (event)=> { const message = JSON.parse(event.data) const { type, payload } = message; - console.log('passing through nomination request', message) if (type == DraftMessage.NOMINATION_SUBMIT_REQUEST) { socket.send(JSON.stringify( { @@ -87,7 +87,7 @@ export const DraftAdmin = ({ draftSessionId }) => { return () => { socket.removeEventListener('message', draftStatusMessageHandler) socket.removeEventListener('message', userIdentifyMessageHandler ); - socket.remove('message', handleNominationRequest ); + socket.removeEventListener('message', handleNominationRequest ); }; }, [socket]); @@ -162,8 +162,8 @@ export const DraftAdmin = ({ draftSessionId }) => { - { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}> + ); }; \ No newline at end of file diff --git a/frontend/src/apps/draft/common/DraftCountdownClock.jsx b/frontend/src/apps/draft/common/DraftCountdownClock.jsx new file mode 100644 index 0000000..edf9963 --- /dev/null +++ b/frontend/src/apps/draft/common/DraftCountdownClock.jsx @@ -0,0 +1,32 @@ +import React, { useEffect, useState } from "react"; + +export function DraftCountdownClock({ endTime, onFinish }) { + // endTime is in seconds (Unix time) + + const getTimeLeft = (et) => Math.max(0, Math.floor(et - Date.now() / 1000)); + const [timeLeft, setTimeLeft] = useState(getTimeLeft(endTime)); + + useEffect(() => { + if (timeLeft <= 0) { + if (onFinish) onFinish(); + return; + } + const timer = setInterval(() => { + const t = getTimeLeft(endTime); + setTimeLeft(t); + if (t <= 0 && onFinish) onFinish(); + }, 100); + return () => clearInterval(timer); + // eslint-disable-next-line + }, [endTime, onFinish, timeLeft]); + + const minutes = Math.floor(timeLeft / 60); + const secs = timeLeft % 60; + const pad = n => String(n).padStart(2, "0"); + + return ( + + {minutes}:{pad(secs)} + + ); +} \ No newline at end of file diff --git a/frontend/src/apps/draft/common/DraftMoviePool.jsx b/frontend/src/apps/draft/common/DraftMoviePool.jsx index 924908f..00a445d 100644 --- a/frontend/src/apps/draft/common/DraftMoviePool.jsx +++ b/frontend/src/apps/draft/common/DraftMoviePool.jsx @@ -8,6 +8,7 @@ export const DraftMoviePool = ({ isParticipant, draftDetails, draftState }) => { return (
+
    {movies.map(m => (
  • diff --git a/frontend/src/apps/draft/common/utils.js b/frontend/src/apps/draft/common/utils.js index f35a9a6..57161e0 100644 --- a/frontend/src/apps/draft/common/utils.js +++ b/frontend/src/apps/draft/common/utils.js @@ -1,69 +1,78 @@ -import { DraftMessage } from "../constants" +import { DraftMessage } from "../constants"; export async function fetchDraftDetails(draftSessionId) { - return fetch(`/api/draft/${draftSessionId}/`) - .then((response) => { - if (response.ok) { - return response.json() - } - else { - throw new Error() - } - }) - .catch((err) => { - console.error("Error fetching draft details", err) - }) - } + return fetch(`/api/draft/${draftSessionId}/`) + .then((response) => { + if (response.ok) { + return response.json(); + } else { + throw new Error(); + } + }) + .catch((err) => { + console.error("Error fetching draft details", err); + }); +} export async function fetchMovieDetails(draftSessionId) { - return fetch(`/api/draft/${draftSessionId}/movie/`) - .then((response) => { - if (response.ok) { - return response.json() - } - else { - throw new Error() - } - }) - .catch((err) => { - console.error("Error fetching draft details", err) - }) - } + return fetch(`/api/draft/${draftSessionId}/movie/`) + .then((response) => { + if (response.ok) { + return response.json(); + } else { + throw new Error(); + } + }) + .catch((err) => { + console.error("Error fetching draft details", err); + }); +} export function isEmptyObject(obj) { - return obj == null || (Object.keys(obj).length === 0 && obj.constructor === Object); + return ( + obj == null || (Object.keys(obj).length === 0 && obj.constructor === Object) + ); } export const handleDraftStatusMessages = (event, setDraftState) => { - const message = JSON.parse(event.data) - const { type, payload } = message; - console.log("Message: ", type, event?.data) + const message = JSON.parse(event.data); + const { type, payload } = message; + console.log("Message: ", type, event?.data); - if (!payload) return - const {connected_participants, phase, draft_order, draft_index, current_movie} = payload + if (!payload) return; + const { + connected_participants, + phase, + draft_order, + draft_index, + current_movie, + bidding_timer_end, + bidding_timer_start + } = payload; - if (type == DraftMessage.STATUS_SYNC_INFORM) { - setDraftState(payload) - } + if (type == DraftMessage.STATUS_SYNC_INFORM) { + setDraftState(payload); + } - setDraftState(prev=>({ - ...prev, - ...(connected_participants ? { connected_participants } : {}), - ...(draft_order ? { draft_order } : {}), - ...(draft_index ? { draft_index } : {}), - ...(phase ? { phase: Number(phase) } : {}), - ...(current_movie ? {current_movie} : {}), - })) - - } + 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) } : {}), + ...(bidding_timer_start ? { bidding_timer_start: Number(bidding_timer_start) } : {}), + })); +}; export const handleUserIdentifyMessages = (event, setUser) => { - const message = JSON.parse(event.data) - const { type, payload } = message; - - if (type==DraftMessage.USER_IDENTIFICATION_INFORM){ - console.log("Message: ", type, event.data) - const {user} = payload - setUser(user) - } -} \ No newline at end of file + const message = JSON.parse(event.data); + const { type, payload } = message; + + if (type == DraftMessage.USER_IDENTIFICATION_INFORM) { + console.log("Message: ", type, event.data); + const { user } = payload; + setUser(user); + } +}; diff --git a/frontend/src/apps/draft/participant/DraftParticipant.jsx b/frontend/src/apps/draft/participant/DraftParticipant.jsx index c999773..c35125c 100644 --- a/frontend/src/apps/draft/participant/DraftParticipant.jsx +++ b/frontend/src/apps/draft/participant/DraftParticipant.jsx @@ -7,6 +7,7 @@ import { DraftMessage, 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' const NominateMenu = ({socket, draftState, draftDetails, currentUser}) => { @@ -30,7 +31,6 @@ const NominateMenu = ({socket, draftState, draftDetails, currentUser}) => { return (
    - {draftState.draft_order[draftState.draft_index]}