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:
39
frontend/src/apps/draft/constants.js
Normal file
39
frontend/src/apps/draft/constants.js
Normal 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,
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user