Refactor draft app with improved state management and components

* Rename WebSocket message types for better organization
* Improve state handling with dedicated methods like broadcast_state
* Restructure frontend components and remove unused code
This commit is contained in:
2025-08-24 12:06:41 -05:00
parent b38c779772
commit baddca8d50
22 changed files with 387 additions and 275 deletions

View File

@@ -1,13 +1,10 @@
import React, { useEffect, useState } from "react";
import { useWebSocket } from "../common/WebSocketContext.jsx";
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
import { ParticipantList } from "../common/ParticipantList.jsx";
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '../constants.js';
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "../common/utils.js"
import { DraftMoviePool } from "../common/DraftMoviePool.jsx"
import { DraftCountdownClock } from "../common/DraftCountdownClock.jsx"
import { DraftParticipant } from "../participant/DraftParticipant.jsx";
import { useWebSocket } from "./components/WebSocketContext.jsx";
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from './constants.js';
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./utils.js"
import { jsxs } from "react/jsx-runtime";
@@ -102,7 +99,6 @@ export const DraftAdmin = ({ draftSessionId }) => {
else if (target == "previous" && originPhaseIndex > 0) {
destination = DraftPhasesOrdered[originPhaseIndex - 1]
}
console.log(destination)
socket.send(
JSON.stringify(
{ type: DraftMessage.PHASE_CHANGE_REQUEST, origin, destination }
@@ -140,22 +136,15 @@ export const DraftAdmin = ({ draftSessionId }) => {
}
return (
<div className="">
<div className="">
<DraftParticipant draftSessionId={draftSessionId}></DraftParticipant>
<div className="d-flex justify-content-between border-bottom mb-2 p-1">
</div>
</div>
<section className="d-flex justify-content-center mt-3">
<div id="draft-admin-bar">
<div>
<button onClick={() => handleRequestDraftSummary()} className="btn btn-small btn-light mx-1">
<i className="bi bi-arrow-clockwise"></i>
</button>
<button onClick={handleAdvanceDraft} className="btn btn-primary mx-1">Advance Index</button>
<button onClick={handleStartBidding} className="btn btn-primary mx-1">Start Bidding</button>
</section>
<div class="d-flex justify-content-center mt-3">
</div>
<div>
<DraftPhaseDisplay draftPhase={draftState.phase} nextPhaseHandler={() => { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}></DraftPhaseDisplay>
</div>

View File

@@ -1,14 +1,14 @@
// DraftAdmin.jsx
import React, { useEffect, useState, useRef } from "react";
import { useWebSocket } from "../common/WebSocketContext.jsx";
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
import { DraftMessage, DraftPhaseLabel, DraftPhases } from '../constants.js';
import { fetchDraftDetails, handleUserIdentifyMessages, isEmptyObject } from "../common/utils.js";
import { DraftMoviePool } from "../common/DraftMoviePool.jsx";
import { ParticipantList } from "../common/ParticipantList.jsx";
import { DraftCountdownClock } from "../common/DraftCountdownClock.jsx"
import { handleDraftStatusMessages } from '../common/utils.js'
import { useWebSocket } from "./components/WebSocketContext.jsx";
import { WebSocketStatus } from "./components/WebSocketStatus.jsx";
import { DraftMessage, DraftPhaseLabel, DraftPhases } from './constants.js';
import { fetchDraftDetails, handleUserIdentifyMessages, isEmptyObject } from "./utils.js";
import { DraftMoviePool } from "./components/DraftMoviePool.jsx";
import { ParticipantList } from "./components/ParticipantList.jsx";
import { DraftCountdownClock } from "./components/DraftCountdownClock.jsx"
import { handleDraftStatusMessages } from './utils.js'
// import { Collapse } from 'bootstrap/dist/js/bootstrap.bundle.min.js';
import { Collapse, ListGroup } from "react-bootstrap";
@@ -62,6 +62,7 @@ const NominateMenu = ({ socket, draftState, draftDetails, currentUser, }) => {
}
export const DraftParticipant = ({ draftSessionId }) => {
const socket = useWebSocket();
const [draftState, setDraftState] = useState({});
const [draftDetails, setDraftDetails] = useState({});
@@ -79,13 +80,6 @@ export const DraftParticipant = ({ draftSessionId }) => {
})
}, [draftSessionId])
useEffect(() => {
if (!socket) return;
socket.onclose = (event) => {
console.log('Websocket Closed')
}
}, [socket])
useEffect(() => {
if (!socket) return;
@@ -116,80 +110,88 @@ export const DraftParticipant = ({ draftSessionId }) => {
return (
<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>
<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>
</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)) : ""}
</header>
<div className="panel-body">
<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 className="flex-grow-1 text-center">
highest bid
</div>
<div className="bid-controls btn-group d-flex flex-column">
<a className="btn btn-primary d-none" data-bs-toggle="collapse" aria-expanded="true" aria-controls="collapse-1" href="#collapse-1" role="button">Show Content</a>
<div id="collapse-1" className="collapse show">
<div>
<div className="row g-0 border rounded-2 m-2">
<div className="col-3"><img className="img-fluid flex-fill" /></div>
<div className="col d-flex justify-content-center align-items-center">
<span className="fw-bold">Movie title</span>
</div>
</div>
</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>
</div>
<div>
<ol className="bid-list">
{draftState.bids?.map((bid, idx) => (
<li key={idx}>{bid.user}: {bid.amount}</li>
))}
</ol>
<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>
<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">
<section className="panel" id="draft-slate">
<header className="panel-header">
<h2 className="panel-title">Draft Catalog</h2>
<div className="panel-title"><span>Draft Catalog</span></div>
</header>
<div className="panel-body">
<div className="current-movie card">
<span>Current Nomination: {movies.find(i => draftState.current_movie == i.id)?.title}</span>
</div>
<NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu>
{/* <NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu> */}
<div className="movie-filters"></div>
<DraftMoviePool isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
@@ -199,7 +201,7 @@ export const DraftParticipant = ({ draftSessionId }) => {
<section className="panel my-team">
<header className="panel-header">
<h2 className="panel-title">My Team</h2>
<div className="panel-title"><span>My Team</span></div>
</header>
<div className="panel-body">
<ul className="team-movie-list list-group">
@@ -212,21 +214,18 @@ export const DraftParticipant = ({ draftSessionId }) => {
<section className="panel teams">
<header className="panel-header">
<h2 className="panel-title">Teams</h2>
<div className="panel-title"><span>Teams</span></div>
</header>
<div className="panel-body">
<ParticipantList
currentUser={currentUser}
draftState={draftState}
draftDetails={draftDetails}
/>
<ul className="team-list list-group">
<li className="team-item list-group-item">
<div className="team-name fw-bold"></div>
<ul className="team-movie-list list-group list-group-flush">
<li className="team-movie-item list-group-item"></li>
</ul>
</li>
{draftState.participants?.map(p => (
<li className="team-item list-group-item" key={p}>
<div className="team-name fw-bold">{p}</div>
<ul className="team-movie-list list-group list-group-flush">
<li className="team-movie-item list-group-item"></li>
</ul>
</li>
))}
</ul>
</div>
</section>

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";;
import { useWebSocket } from "./common/WebSocketContext.jsx";
import { useWebSocket } from "./components/WebSocketContext.jsx";
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from './constants.js';
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./common/utils.js"
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./utils.js"
export const DraftDebug = ({ draftSessionId }) => {
const [draftState, setDraftState] = useState({})

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useState } from "react";
export function DraftCountdownClock({ endTime, onFinish }) {
export function DraftCountdownClock({ draftState }) {
// endTime is in seconds (Unix time)
const {bidding_timer_end, onFinish} = draftState
const getTimeLeft = (et) => Math.max(0, Math.floor(et - Date.now() / 1000));
const [timeLeft, setTimeLeft] = useState(getTimeLeft(endTime));
const [timeLeft, setTimeLeft] = useState(getTimeLeft(bidding_timer_end));
useEffect(() => {
if (timeLeft <= 0) {
@@ -12,13 +12,13 @@ export function DraftCountdownClock({ endTime, onFinish }) {
return;
}
const timer = setInterval(() => {
const t = getTimeLeft(endTime);
const t = getTimeLeft(bidding_timer_end);
setTimeLeft(t);
if (t <= 0 && onFinish) onFinish();
}, 100);
return () => clearInterval(timer);
// eslint-disable-next-line
}, [endTime, onFinish, timeLeft]);
}, [bidding_timer_end, onFinish, timeLeft]);
const minutes = Math.floor(timeLeft / 60);
const secs = timeLeft % 60;

View File

@@ -1,5 +1,5 @@
import React from "react";
import { isEmptyObject } from "./utils";
import { isEmptyObject } from "../utils";
export const DraftMoviePool = ({ isParticipant, draftDetails, draftState }) => {
if(isEmptyObject(draftDetails)) {return}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { fetchDraftDetails, isEmptyObject } from "../common/utils.js"
import { fetchDraftDetails, isEmptyObject } from "../utils.js"
export const ParticipantList = ({ isAdmin, draftState, draftDetails, currentUser }) => {
if (isEmptyObject(draftState) || isEmptyObject(draftDetails)) { console.warn('empty draft state', draftState); return }

View File

@@ -13,8 +13,8 @@ export const DraftMessage = {
PHASE_CHANGE_INFORM: "phase.change.inform",
PHASE_CHANGE_REQUEST: "phase.change.request",
PHASE_CHANGE_CONFIRM: "phase.change.confirm",
STATUS_SYNC_REQUEST: "status.sync.request",
STATUS_SYNC_INFORM: "status.sync.inform",
DRAFT_STATUS_REQUEST: "draft.status.request",
DRAFT_STATUS_INFORM: "draft.status.sync.inform",
DRAFT_INDEX_ADVANCE_REQUEST: "draft.index.advance.request",
DRAFT_INDEX_ADVANCE_CONFIRM: "draft.index.advance.confirm",
ORDER_DETERMINE_REQUEST: "order.determine.request",
@@ -22,6 +22,7 @@ export const DraftMessage = {
BID_START_INFORM: "bid.start.inform",
BID_START_REQUEST: "bid.start.request",
BID_PLACE_REQUEST: "bid.place.request",
BID_PLACE_CONFIRM: "bid.update.confirm",
BID_UPDATE_INFORM: "bid.update.inform",
BID_END_INFORM: "bid.end.inform",
NOMINATION_SUBMIT_REQUEST: "nomination.submit.request",

View File

@@ -1,4 +1,4 @@
import { DraftMessage } from "../constants";
import { DraftMessage } from "./constants";
export async function fetchDraftDetails(draftSessionId) {
return fetch(`/api/draft/${draftSessionId}/`)
@@ -37,38 +37,12 @@ export function isEmptyObject(obj) {
export const handleDraftStatusMessages = (event, setDraftState) => {
const message = JSON.parse(event.data);
const { type, payload } = message;
console.log("Message: ", type, event?.data);
if (!payload) return;
const {
connected_participants,
phase,
draft_order,
draft_index,
current_movie,
bidding_timer_end,
bidding_timer_start,
current_pick,
next_picks,
bids
} = payload;
if (type == DraftMessage.STATUS_SYNC_INFORM) {
if (type == DraftMessage.DRAFT_STATUS_INFORM) {
setDraftState(payload);
}
setDraftState((prev) => ({
...prev,
...(connected_participants ? { connected_participants } : {}),
...(draft_order ? { draft_order } : {}),
...(draft_index ? { draft_index } : {}),
...(phase ? { phase: Number(phase) } : {}),
...(current_movie ? { current_movie } : {}),
...(bidding_timer_end ? { bidding_timer_end: Number(bidding_timer_end) } : {}),
...(current_pick ? { current_pick } : {}),
...(next_picks ? { next_picks } : {}),
...(bids ? {bids} : {})
}));
};
export const handleUserIdentifyMessages = (event, setUser) => {

View File

@@ -2,13 +2,13 @@ import './scss/styles.scss'
import React from "react";
import { createRoot } from "react-dom/client";
import { WebSocketProvider } from "./apps/draft/common/WebSocketContext.jsx";
import { DraftAdmin } from "./apps/draft/admin/DraftAdmin.jsx";
import { DraftParticipant} from './apps/draft/participant/DraftParticipant.jsx'
import { WebSocketProvider } from "./apps/draft/components/WebSocketContext.jsx";
import { DraftAdmin } from "./apps/draft/DraftAdminBar.jsx";
import { DraftParticipant} from './apps/draft/DraftDashboard.jsx'
import { DraftDebug} from './apps/draft/DraftDebug.jsx'
const draftAdminRoot = document.getElementById("draft-admin-root");
const draftAdminBarRoot = document.getElementById("draft-admin-bar-root");
const draftPartipantRoot = document.getElementById("draft-participant-root")
const draftDebugRoot = document.getElementById("draft-debug-root")
const {draftSessionId} = window; // from backend template
@@ -21,9 +21,9 @@ if (draftPartipantRoot) {
</WebSocketProvider>
);
}
if (draftAdminRoot) {
if (draftAdminBarRoot) {
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/admin`;
createRoot(draftAdminRoot).render(
createRoot(draftAdminBarRoot).render(
<WebSocketProvider url={wsUrl}>
<DraftAdmin draftSessionId={draftSessionId}/>
</WebSocketProvider>

View File

@@ -1,6 +1,6 @@
@use "../../node_modules/bootstrap/scss/bootstrap.scss";
@use "./fonts/graphique.css";
@import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Oswald:wght@200..700&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=League+Gothic&family=Oswald:wght@200..700&display=swap');
// Import only functions & variables
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@@ -127,8 +127,21 @@
}
}
#draft-participant-root,
#draft-admin-root {
#draft-admin-bar {
@extend .d-flex;
@extend .flex-column;
@extend .border-top;
@extend .border-bottom;
@extend .gap-2;
@extend .p-2;
@extend .shadow-sm;
div {
@extend .d-flex;
@extend .justify-content-center
}
}
#draft-participant-root {
@extend .flex-grow-1;
.wrapper:first-child {
@extend .p-2;
@@ -137,44 +150,61 @@
gap: 1rem; /* space between panels */
justify-content: center; /* center the panels horizontally */
section {
max-width: 450px; /* never go beyond this */
min-width: 300px; /* keeps them from getting too small */
flex: 1 1 350px; /* grow/shrink, base width */
}
.panel {
@extend .border;
@extend .shadow-sm;
@extend .rounded-2;
flex: 1 1 350px; /* grow/shrink, base width */
max-width: 450px; /* never go beyond this */
min-width: 300px; /* keeps them from getting too small */
header.panel-header {
@extend .p-1;
@extend .text-uppercase;
@extend .align-items-center;
@extend .border-bottom;
@extend .border-secondary;
background-color: $blue-100;
@extend .border-2;
@extend .border-secondary-subtle;
// background-color: $blue-100;
@extend .bg-dark;
@extend .bg-gradient;
@extend .text-light;
@extend .rounded-top-2;
.panel-title {
@extend .ms-2;
@extend .fw-bold;
@extend .fs-5;
}
}
}
.panel.draft-live {
.bids-container {
overflow: scroll;
height: 85px;
}
#draft-live {
header.panel-header {
@extend .d-flex;
@extend .justify-content-between;
}
.draft-live-state-container {
@extend .d-flex;
background-color: $green-100;
#draft-clock {
@extend .row;
@extend .g-0;
// background-color: $green-100;
@extend .text-light;
@extend .text-bg-dark;
@extend .lh-1;
.countdown-clock {
@extend .fs-1;
@extend .fw-bold;
font-family: 'League Gothic';
font-size: $font-size-base * 5;
@extend .fw-bolder;
@extend .col;
@extend .align-content-center;
@extend .text-center;
}
.pick-description {
@extend .col;
@extend .align-content-center;
}
}
div:has(.pick-list), div:has(.bid-list){