Improve draft UI state handling, layout, and order logic
- Added current/next pick info, updated server draft logic for order/snake - Refactored WebSocketContext, removed dead code, improved CSS/layout - Cleaned up template blocks, admin, and participant panel structure
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";;
|
||||
import { useWebSocket } from "./WebSocketContext.jsx";
|
||||
import { useWebSocket } from "./common/WebSocketContext.jsx";
|
||||
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from './constants.js';
|
||||
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./common/utils.js"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useWebSocket } from "../WebSocketContext.jsx";
|
||||
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';
|
||||
|
||||
@@ -25,8 +25,10 @@ export function DraftCountdownClock({ endTime, onFinish }) {
|
||||
const pad = n => String(n).padStart(2, "0");
|
||||
|
||||
return (
|
||||
<span>
|
||||
{minutes}:{pad(secs)}
|
||||
</span>
|
||||
<div className="countdown-clock">
|
||||
<span>
|
||||
{minutes}:{pad(secs)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// WebSocketContext.jsx
|
||||
import React, { useState, createContext, useContext, useRef, useEffect } from "react";
|
||||
import React, { useState, createContext, useContext } from "react";
|
||||
|
||||
const WebSocketContext = createContext(null);
|
||||
|
||||
@@ -47,7 +47,9 @@ export const handleDraftStatusMessages = (event, setDraftState) => {
|
||||
draft_index,
|
||||
current_movie,
|
||||
bidding_timer_end,
|
||||
bidding_timer_start
|
||||
bidding_timer_start,
|
||||
current_pick,
|
||||
next_picks
|
||||
} = payload;
|
||||
|
||||
if (type == DraftMessage.STATUS_SYNC_INFORM) {
|
||||
@@ -62,7 +64,8 @@ export const handleDraftStatusMessages = (event, setDraftState) => {
|
||||
...(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) } : {}),
|
||||
...(current_pick ? { current_pick } : {}),
|
||||
...(next_picks ? { next_picks } : {}),
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useRef } from "react";
|
||||
import { DraftMessage, DraftPhases } from './constants.js';
|
||||
|
||||
|
||||
const WebSocketContext = createContext(null);
|
||||
|
||||
export const WebSocketProvider = ({ url, children }) => {
|
||||
const socketRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socketRef.current) {
|
||||
socketRef.current = new WebSocket(url);
|
||||
}
|
||||
|
||||
return () => {
|
||||
socketRef.current?.close();
|
||||
socketRef.current = null;
|
||||
};
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider value={socketRef.current}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useWebSocket = () => {
|
||||
return useContext(WebSocketContext);
|
||||
};
|
||||
|
||||
export const WebSocketStatus = ({ socket }) => {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('socket changed', socket)
|
||||
if (!socket) return;
|
||||
|
||||
const handleOpen = () => {console.log('socket open'); setIsConnected(true)};
|
||||
const handleClose = () => setIsConnected(false);
|
||||
const handleError = () => setIsConnected(false);
|
||||
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
console.log('socket already connected')
|
||||
setIsConnected(true);
|
||||
}
|
||||
|
||||
socket.addEventListener("open", handleOpen);
|
||||
socket.addEventListener("close", handleClose);
|
||||
socket.addEventListener("error", handleError);
|
||||
|
||||
// 🧹 Cleanup to remove listeners when component unmounts or socket changes
|
||||
return () => {
|
||||
socket.removeEventListener("open", handleOpen);
|
||||
socket.removeEventListener("close", handleClose);
|
||||
socket.removeEventListener("error", handleError);
|
||||
};
|
||||
|
||||
}, [socket])
|
||||
return (
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span
|
||||
className="status-dot"
|
||||
style={{
|
||||
width: "10px",
|
||||
height: "10px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: isConnected ? "green" : "red",
|
||||
display: "inline-block",
|
||||
}}
|
||||
></span>
|
||||
<span>{isConnected ? "Connected" : "Disconnected"}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MessageLogger = ({ socket }) => {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const bottomRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
const handleMessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
setMessages((prev) => [...prev, data]);
|
||||
};
|
||||
|
||||
socket.addEventListener("message", handleMessage);
|
||||
|
||||
return () => {
|
||||
console.log('removing event listeners')
|
||||
socket.removeEventListener("message", handleMessage);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
useEffect(() => {
|
||||
// Scroll to bottom when messages update
|
||||
if (bottomRef.current) {
|
||||
bottomRef.current.scrollIntoView({ behavior: "smooth" , block: 'nearest', inline: 'start'});
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<div className="message-logger mt-4">
|
||||
<label>📥 Received Messages</label>
|
||||
<div style={{ maxHeight: '300px', overflowY: 'scroll', fontFamily: 'monospace', background: '#f8f9fa', padding: '1em', border: '1px solid #ccc' }}>
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i}>
|
||||
<pre style={{ margin: 0 }}>{JSON.stringify(msg, null, 2)}</pre>
|
||||
<hr />
|
||||
</div>
|
||||
))}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DraftAdmin = ({ draftSessionId }) => {
|
||||
const [connectedParticipants, setConnectedParticipants] = useState([]);
|
||||
const [draftPhase, setDraftPhase] = useState();
|
||||
|
||||
const socketRef = useWebSocket();
|
||||
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/admin`;
|
||||
|
||||
useEffect(() => {
|
||||
if (socketRef.current) return;
|
||||
console.log('socket created')
|
||||
socketRef.current = new WebSocket(wsUrl);
|
||||
|
||||
socketRef.current.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
const { type, payload } = message;
|
||||
console.log(type, event)
|
||||
if (type == DraftMessage.REQUEST.JOIN_PARTICIPANT) {
|
||||
console.log('join request', data)
|
||||
}
|
||||
else if (type == DraftMessage.CONFIRM.JOIN_PARTICIPANT) {
|
||||
setConnectedParticipants(data.connected_participants)
|
||||
}
|
||||
else if (type == DraftMessage.CONFIRM.PHASE_CHANGE) {
|
||||
console.log('phase_change')
|
||||
setDraftPhase(payload.phase)
|
||||
}
|
||||
};
|
||||
|
||||
socketRef.current.onclose = (event) => {
|
||||
console.log('Websocket Closed')
|
||||
socketRef.current = null;
|
||||
}
|
||||
|
||||
return () => {
|
||||
socketRef.current.close();
|
||||
};
|
||||
}, [wsUrl]);
|
||||
|
||||
const handlePhaseChange = (destinationPhase) => {
|
||||
socketRef.current.send(JSON.stringify({ type: DraftMessage.REQUEST.PHASE_CHANGE, "destination": destinationPhase }));
|
||||
}
|
||||
|
||||
|
||||
const handleRequestDraftSummary = () => {
|
||||
socketRef.current.send(JSON.stringify({ type: 'request_summary' }))
|
||||
}
|
||||
|
||||
return (
|
||||
<WebSocketContext url={wsUrl}>
|
||||
<div className="container draft-panel">
|
||||
<h3>Draft Admin Panel</h3>
|
||||
<WebSocketStatus socket={socket} />
|
||||
{/* <MessageLogger socket={socketRef.current} /> */}
|
||||
<label>Connected Particpants</label>
|
||||
<input
|
||||
type="text"
|
||||
readOnly disabled
|
||||
value={connectedParticipants ? JSON.stringify(connectedParticipants) : ""}
|
||||
/>
|
||||
<label>Draft Phase</label>
|
||||
<input
|
||||
type="text"
|
||||
readOnly disabled
|
||||
value={draftPhase ? JSON.stringify(draftPhase) : ""}
|
||||
/>
|
||||
<button onClick={() => handlePhaseChange(DraftPhase.DETERMINE_ORDER)} className="btn btn-primary mt-2 me-2">
|
||||
Determine Draft Order
|
||||
</button>
|
||||
<button onClick={handleRequestDraftSummary} className="btn btn-primary mt-2">
|
||||
Request status
|
||||
</button>
|
||||
</div>
|
||||
</WebSocketContext>
|
||||
);
|
||||
};
|
||||
|
||||
export const DraftParticipant = ({ draftSessionId }) => {
|
||||
const socketRef = useRef(null);
|
||||
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/participant`;
|
||||
|
||||
useEffect(() => {
|
||||
socketRef.current = new WebSocket(wsUrl);
|
||||
|
||||
socketRef.current.onmessage = (evt) => {
|
||||
const data = JSON.parse(evt.data);
|
||||
console.log(data)
|
||||
};
|
||||
|
||||
socketRef.current.onclose = () => {
|
||||
console.warn("WebSocket connection closed.");
|
||||
socketRef.current = null;
|
||||
};
|
||||
|
||||
return () => {
|
||||
socketRef.current.close();
|
||||
};
|
||||
}, [wsUrl]);
|
||||
|
||||
const handleStartDraft = () => {
|
||||
socketRef.current.send(JSON.stringify({ type: "start_draft" }));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container draft-panel">
|
||||
<h3 >Draft Participant Panel</h3>
|
||||
<WebSocketStatus socket={socketRef.current} />
|
||||
<label>Latest Message</label>
|
||||
<MessageLogger socket={socketRef.current}></MessageLogger>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +1,20 @@
|
||||
// DraftAdmin.jsx
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useWebSocket } from "../WebSocketContext.jsx";
|
||||
import { useWebSocket } from "../common/WebSocketContext.jsx";
|
||||
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
||||
import { DraftMessage, DraftPhases } from '../constants.js';
|
||||
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'
|
||||
|
||||
const NominateMenu = ({socket, draftState, draftDetails, currentUser}) => {
|
||||
const NominateMenu = ({ socket, draftState, draftDetails, currentUser }) => {
|
||||
if (!socket || isEmptyObject(draftDetails) || isEmptyObject(draftState)) return;
|
||||
const currentDrafter = draftState.draft_order[draftState.draft_index]
|
||||
if (currentUser != currentDrafter) return;
|
||||
const {movies} = draftDetails
|
||||
const { movies } = draftDetails
|
||||
|
||||
const requestNomination = (event) => {
|
||||
event.preventDefault()
|
||||
@@ -33,12 +33,12 @@ const NominateMenu = ({socket, draftState, draftDetails, currentUser}) => {
|
||||
<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>
|
||||
<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>
|
||||
@@ -79,26 +79,86 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
socket.addEventListener('message', userIdentifyMessageHandler);
|
||||
|
||||
return () => {
|
||||
socket.removeEventListener('message', draftStatusMessageHandler)
|
||||
socket.removeEventListener('message', draftStatusMessageHandler);
|
||||
socket.removeEventListener('message', userIdentifyMessageHandler);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
return (
|
||||
<div className="container draft-panel">
|
||||
<div className="d-flex justify-content-between border-bottom mb-2 p-1">
|
||||
<h3>Draft Panel</h3>
|
||||
<WebSocketStatus socket={socket} />
|
||||
</div>
|
||||
<ParticipantList
|
||||
currentUser={currentUser}
|
||||
draftState={draftState}
|
||||
draftDetails={draftDetails}
|
||||
/>
|
||||
<DraftMoviePool isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
||||
<div className="draft-participant">
|
||||
<section class="panel draft-live">
|
||||
<header class="panel-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="panel-title">Draft Live</h2>
|
||||
<div class="d-flex gap-1">
|
||||
<div class="phase-indicator badge bg-primary">{DraftPhaseLabel[draftState.phase]}</div>
|
||||
<WebSocketStatus socket={socket} />
|
||||
</div>
|
||||
</header>
|
||||
<div class="panel-body">
|
||||
<div class="draft-live-state-container">
|
||||
<DraftCountdownClock endTime={draftState.bidding_timer_end}></DraftCountdownClock>
|
||||
<div class="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 class="current-movie card"></div>
|
||||
<div class="bid-controls btn-group"></div>
|
||||
<ParticipantList
|
||||
currentUser={draftState.current_pick?.participant}
|
||||
draftState={draftState}
|
||||
draftDetails={draftDetails}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel draft-board">
|
||||
<header class="panel-header">
|
||||
<h2 class="panel-title">Draft Board</h2>
|
||||
</header>
|
||||
<div class="panel-body">
|
||||
<div class="current-movie-detail card"></div>
|
||||
<div class="movie-filters"></div>
|
||||
<DraftMoviePool isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="panel my-team">
|
||||
<header class="panel-header">
|
||||
<h2 class="panel-title">My Team</h2>
|
||||
</header>
|
||||
<div class="panel-body">
|
||||
<ul class="team-movie-list list-group">
|
||||
<li class="team-movie-item list-group-item"></li>
|
||||
</ul>
|
||||
<div class="budget-status"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="panel teams">
|
||||
<header class="panel-header">
|
||||
<h2 class="panel-title">Teams</h2>
|
||||
</header>
|
||||
<div class="panel-body">
|
||||
<ul class="team-list list-group">
|
||||
<li class="team-item list-group-item">
|
||||
<div class="team-name fw-bold"></div>
|
||||
<ul class="team-movie-list list-group list-group-flush">
|
||||
<li class="team-movie-item list-group-item"></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu>
|
||||
<DraftCountdownClock endTime={draftState.bidding_timer_end}></DraftCountdownClock>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import './scss/styles.scss'
|
||||
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { WebSocketProvider } from "./apps/draft/WebSocketContext.jsx";
|
||||
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 { DraftDebug} from './apps/draft/DraftDebug.jsx'
|
||||
|
||||
@@ -123,3 +123,31 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.draft-participant {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* allow panels to wrap */
|
||||
gap: 1rem; /* space between panels */
|
||||
justify-content: center; /* center the panels horizontally */
|
||||
|
||||
.panel {
|
||||
flex: 1 1 350px; /* grow/shrink, base width */
|
||||
max-width: 450px; /* never go beyond this */
|
||||
min-width: 300px; /* keeps them from getting too small */
|
||||
}
|
||||
.panel.draft-live {
|
||||
.draft-live-state-container {
|
||||
@extend .d-flex;
|
||||
.countdown-clock {
|
||||
@extend .fs-1;
|
||||
@extend .fw-bold;
|
||||
@extend .col;
|
||||
@extend .align-content-center;
|
||||
@extend .text-center;
|
||||
}
|
||||
.pick-description{
|
||||
@extend .col;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user