Integrate draft session support with phase handling and real-time updates

- Added user authentication UI in the base template for navbar.
- Expanded `league.dj.html` to include a new "Draft Sessions" tab showing active drafts.
- Refactored Django views and models to support `DraftSession` with participants and movies.
- Replaced deprecated models like `DraftParticipant` and `DraftMoviePool` with a new schema using `DraftSessionParticipant`.
- Introduced WebSocket consumers (`DraftAdminConsumer`, `DraftParticipantConsumer`) with structured phase logic and caching.
- Added `DraftStateManager` for managing draft state in Django cache.
- Created frontend UI components in React for draft admin and participants, including phase control and WebSocket message logging.
- Updated SCSS styles for improved UI structure and messaging area.
This commit is contained in:
2025-08-02 08:56:41 -05:00
parent 1a7a6a2d50
commit c9ce7a36d0
16 changed files with 811 additions and 484 deletions

View File

@@ -0,0 +1,39 @@
export const DraftMessage = {
// Server to Client
INFORM: {
PHASE_CHANGE: "inform.phase.change",
STATUS: "inform.status",
JOIN_USER: "inform.join.user",
},
// Client to Server
REQUEST: {
PHASE_CHANGE: "request.phase.change",
INFORM_STATUS: "request.inform.status",
JOIN_PARTICIPANT: "request.join.participant",
JOIN_ADMIN: "request.join.admin",
DETERMINE_DRAFT_ORDER: "request.determine.draft_order",
},
// Confirmation messages (Server to Client)
CONFIRM: {
PHASE_CHANGE: "confirm.phase.change",
JOIN_PARTICIPANT: "confirm.join.participant",
JOIN_ADMIN: "confirm.join.admin",
DETERMINE_DRAFT_ORDER: "confirm.determine.draft_order",
},
// Client-side notification (to server)
NOTIFY: {
JOIN_USER: "notify.join.user",
},
};
export const DraftPhase = {
WAITING: 0,
DETERMINE_ORDER: 10,
NOMINATION: 20,
BIDDING: 30,
AWARD: 40,
FINALIZE: 50,
}

View File

@@ -1,24 +1,22 @@
import React, { useEffect, useState, useRef } from "react";
import { DraftMessage, DraftPhase } from './constants.js';
export const WebSocketStatus = ({ socket }) => {
export const useWebSocketStatus = (wsUrl) => {
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const socket = new WebSocket(wsUrl);
socket.onopen = () => setIsConnected(true);
socket.onclose = () => setIsConnected(false);
socket.onerror = () => setIsConnected(false);
if (!socket) return;
return () => socket.close();
}, [wsUrl]);
return isConnected;
};
export const WebSocketStatus = ({ wsUrl }) => {
const isConnected = useWebSocketStatus(wsUrl);
if (socket.readyState === WebSocket.OPEN) {
setIsConnected(true);
}
socket.addEventListener("open", () => setIsConnected(true));
socket.addEventListener("close", () => setIsConnected(false));
socket.addEventListener("error", () => setIsConnected(false));
}, [socket])
return (
<div className="d-flex align-items-center gap-2">
<span
@@ -36,10 +34,52 @@ export const WebSocketStatus = ({ wsUrl }) => {
);
};
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 () => {
socket.removeEventListener("message", handleMessage);
};
}, [socket]);
useEffect(() => {
// Scroll to bottom when messages update
if (bottomRef.current) {
bottomRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [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 [latestMessage, setLatestMessage] = useState(null);
const [connectedParticipants, setConnectedParticipants] = useState([]);
const [isConnected, setIsConnected] = useState(false);
const [draftPhase, setDraftPhase] = useState();
const socketRef = useRef(null);
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/admin`;
@@ -48,28 +88,34 @@ export const DraftAdmin = ({ draftSessionId }) => {
socketRef.current = new WebSocket(wsUrl);
socketRef.current.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(event)
setLatestMessage(data);
if (data.type == "user.joined") {
// setConnectedParticipants =
const message = JSON.parse(event.data)
const { type, payload } = message;
console.log(type, event)
setLatestMessage(message);
if (type == DraftMessage.REQUEST.JOIN_PARTICIPANT) {
console.log('join request', data)
}
else if (data.type == "draft_summary"){
console.log(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 = () => {
console.warn("WebSocket connection closed.");
};
socketRef.current.onclose = (event) => {
console.log('Websocket Closed')
socketRef.current = null;
}
return () => {
socketRef.current.close();
};
}, [wsUrl]);
const handleStartDraft = () => {
socketRef.current.send(JSON.stringify({ type: "start.draft" }));
const handlePhaseChange = (destinationPhase) => {
socketRef.current.send(JSON.stringify({ type: DraftMessage.REQUEST.PHASE_CHANGE, "destination": destinationPhase }));
}
@@ -80,21 +126,22 @@ export const DraftAdmin = ({ draftSessionId }) => {
return (
<div className="container draft-panel">
<h3>Draft Admin Panel</h3>
<WebSocketStatus wsUrl={wsUrl} />
<label>Latest Message</label>
<input
type="text"
readOnly disabled
value={latestMessage ? JSON.stringify(latestMessage) : ""}
/>
<WebSocketStatus socket={socketRef.current} />
<MessageLogger socket={socketRef.current}></MessageLogger>
<label>Connected Particpants</label>
<input
type="text"
readOnly disabled
value={connectedParticipants ? JSON.stringify(connectedParticipants) : ""}
/>
<button onClick={handleStartDraft} className="btn btn-primary mt-2">
Start Draft
<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
@@ -111,16 +158,15 @@ export const DraftParticipant = ({ draftSessionId }) => {
useEffect(() => {
socketRef.current = new WebSocket(wsUrl);
socketRef.current.onmessage = (event) => {
const data = JSON.parse(event.data);
socketRef.current.onmessage = (evt) => {
const data = JSON.parse(evt.data);
console.log(data)
setLatestMessage(data);
if (data.type == "draft_summary") {
console.log('draft_summary', data)
}
};
socketRef.current.onclose = () => {
console.warn("WebSocket connection closed.");
socketRef.current = null;
};
return () => {
@@ -135,7 +181,7 @@ export const DraftParticipant = ({ draftSessionId }) => {
return (
<div className="container draft-panel">
<h3 >Draft Participant Panel</h3>
<WebSocketStatus wsUrl={wsUrl} />
<WebSocketStatus socket={socketRef.current} />
<label>Latest Message</label>
<input
type="text"

View File

@@ -1,10 +1,8 @@
@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');
@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");
.navbar{
.navbar {
// background-color: #582f0e;
@extend .border-bottom;
// font-family: "Bebas Neue";
@@ -18,9 +16,9 @@
}
.draft-panel {
@extend .mt-4 ;
@extend .border ;
@extend .rounded-2 ;
@extend .mt-4;
@extend .border;
@extend .rounded-2;
@extend .p-2;
@extend .pt-1;
label {
@@ -29,4 +27,13 @@
input {
@extend .form-control;
}
}
}
.message-log {
max-height: 300px;
overflow-y: scroll;
font-family: monospace;
background: #f8f9fa;
padding: 1em;
border: 1px solid #ccc;
}