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:
@@ -2,12 +2,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}My Site{% endblock %}</title>
|
<title>
|
||||||
<link
|
{% block title %}My Site{% endblock %}
|
||||||
rel="stylesheet"
|
</title>
|
||||||
href="https://cdn.datatables.net/2.3.2/css/dataTables.bootstrap5.css"
|
<link rel="stylesheet"
|
||||||
/>
|
href="https://cdn.datatables.net/2.3.2/css/dataTables.bootstrap5.css" />
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
|
<link rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
|
||||||
{% if DEBUG %}
|
{% if DEBUG %}
|
||||||
<script defer src="http://localhost:3000/dist/bundle.js"></script>
|
<script defer src="http://localhost:3000/dist/bundle.js"></script>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -27,8 +28,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{%block navbar%}
|
{% block navbar %}{% endblock %}
|
||||||
{%endblock%}
|
|
||||||
</div>
|
</div>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<div>
|
<div>
|
||||||
@@ -40,24 +40,26 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
<main class="container mt-4">
|
<main class="container mt-4">
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
{% if breadcrumbs %}
|
{% if breadcrumbs %}
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
{% for crumb in breadcrumbs %}
|
{% for crumb in breadcrumbs %}
|
||||||
<li class="breadcrumb-item {% if forloop.last %}active{% endif %}" aria-current="page">{% if not forloop.last %}<a href="{{crumb.url}}">{{crumb.label}}</a>{%else%}{{crumb.label}}{%endif%}</li>
|
<li class="breadcrumb-item {% if forloop.last %}active{% endif %}"
|
||||||
|
aria-current="page">
|
||||||
|
{% if not forloop.last %}
|
||||||
|
<a href="{{ crumb.url }}">{{ crumb.label }}</a>{% else %}{{ crumb.label }}{% endif %}
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock%} {% block content %}
|
{% endblock breadcrumbs %}
|
||||||
<!-- Default content -->
|
{% block content %}{% endblock content %}
|
||||||
{% endblock %}
|
{% endblock body %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="text-muted text-center mt-5">
|
<footer class="text-muted text-center mt-5">
|
||||||
<small>© Sack Lunch</small>
|
<small>© Sack Lunch</small>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
|||||||
|
|
||||||
self.group_names = DraftGroupChannelNames(draft_hashid)
|
self.group_names = DraftGroupChannelNames(draft_hashid)
|
||||||
self.cache_keys = DraftCacheKeys(draft_hashid)
|
self.cache_keys = DraftCacheKeys(draft_hashid)
|
||||||
self.draft_state = DraftStateManager(draft_hashid, self.draft_session.settings)
|
self.draft_state = DraftStateManager(self.draft_session)
|
||||||
|
|
||||||
self.user = self.scope["user"]
|
self.user = self.scope["user"]
|
||||||
if not self.should_accept_user():
|
if not self.should_accept_user():
|
||||||
@@ -172,7 +172,7 @@ class DraftAdminConsumer(DraftConsumerBase):
|
|||||||
await self.start_nominate()
|
await self.start_nominate()
|
||||||
|
|
||||||
if event_type == DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST:
|
if event_type == DraftMessage.DRAFT_INDEX_ADVANCE_REQUEST:
|
||||||
self.draft_state.draft_index += 1
|
self.draft_state.draft_index_advance()
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.group_names.session,
|
self.group_names.session,
|
||||||
{
|
{
|
||||||
@@ -229,18 +229,22 @@ class DraftAdminConsumer(DraftConsumerBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def determine_draft_order(self):
|
async def determine_draft_order(self):
|
||||||
draft_order = random.sample(
|
draft_order = self.draft_state.determine_draft_order(self.draft_participants)
|
||||||
self.draft_participants, len(self.draft_participants)
|
self.draft_state.draft_index = 0
|
||||||
)
|
|
||||||
self.draft_state.draft_order = [p.username for p in draft_order]
|
|
||||||
await self.set_draft_phase(DraftPhase.DETERMINE_ORDER)
|
await self.set_draft_phase(DraftPhase.DETERMINE_ORDER)
|
||||||
|
next_picks = self.draft_state.next_picks(include_current=True)
|
||||||
|
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.group_names.session,
|
self.group_names.session,
|
||||||
{
|
{
|
||||||
"type": "broadcast.session",
|
"type": "broadcast.session",
|
||||||
"subtype": DraftMessage.ORDER_DETERMINE_CONFIRM,
|
"subtype": DraftMessage.ORDER_DETERMINE_CONFIRM,
|
||||||
"payload": {"draft_order": self.draft_state.draft_order},
|
"payload": {
|
||||||
|
"draft_order": draft_order,
|
||||||
|
"draft_index": self.draft_state.draft_index,
|
||||||
|
"current_pick": next_picks[0],
|
||||||
|
"next_picks": next_picks[1:]
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ from datetime import datetime, timedelta
|
|||||||
from boxofficefantasy.models import Movie
|
from boxofficefantasy.models import Movie
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from draft.constants import DraftPhase
|
from draft.constants import DraftPhase
|
||||||
from draft.models import DraftSessionSettings
|
from draft.models import DraftSession
|
||||||
import time
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple
|
||||||
|
import random
|
||||||
|
|
||||||
class DraftCacheKeys:
|
class DraftCacheKeys:
|
||||||
def __init__(self, id):
|
def __init__(self, id):
|
||||||
@@ -73,12 +76,12 @@ class DraftCacheKeys:
|
|||||||
# return f"{self.prefix}:user:{user_id}:channel"
|
# return f"{self.prefix}:user:{user_id}:channel"
|
||||||
|
|
||||||
class DraftStateManager:
|
class DraftStateManager:
|
||||||
def __init__(self, session_id: int, settings: DraftSessionSettings):
|
def __init__(self, session: DraftSession):
|
||||||
self.session_id = session_id
|
self.session_id = session.hashid
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.keys = DraftCacheKeys(session_id)
|
self.keys = DraftCacheKeys(self.session_id)
|
||||||
self._initial_phase = self.cache.get(self.keys.phase, DraftPhase.WAITING.value)
|
self._initial_phase = self.cache.get(self.keys.phase, DraftPhase.WAITING.value)
|
||||||
self.settings = settings
|
self.settings = session.settings
|
||||||
|
|
||||||
# === Phase Management ===
|
# === Phase Management ===
|
||||||
@property
|
@property
|
||||||
@@ -115,6 +118,13 @@ class DraftStateManager:
|
|||||||
return
|
return
|
||||||
self.cache.set(self.keys.draft_order,json.dumps(draft_order))
|
self.cache.set(self.keys.draft_order,json.dumps(draft_order))
|
||||||
|
|
||||||
|
def determine_draft_order(self, users: list[User]):
|
||||||
|
draft_order = random.sample(
|
||||||
|
users, len(users)
|
||||||
|
)
|
||||||
|
self.draft_order = [user.username for user in draft_order]
|
||||||
|
return self.draft_order
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def draft_index(self):
|
def draft_index(self):
|
||||||
return self.cache.get(self.keys.draft_index,0)
|
return self.cache.get(self.keys.draft_index,0)
|
||||||
@@ -123,6 +133,42 @@ class DraftStateManager:
|
|||||||
def draft_index(self, draft_index: int):
|
def draft_index(self, draft_index: int):
|
||||||
self.cache.set(self.keys.draft_index, int(draft_index))
|
self.cache.set(self.keys.draft_index, int(draft_index))
|
||||||
|
|
||||||
|
def draft_index_advance(self, n: int = 1):
|
||||||
|
self.draft_index += n
|
||||||
|
return self.draft_index
|
||||||
|
|
||||||
|
def next_picks(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
from_overall: int | None = None,
|
||||||
|
count: int | None = None,
|
||||||
|
include_current: bool = False,
|
||||||
|
) -> List[dict]:
|
||||||
|
"""
|
||||||
|
Convenience: return the next `count` picks starting after `from_overall`
|
||||||
|
(or after current draft_index if omitted). Each item:
|
||||||
|
{overall, round, pick_in_round, participant}
|
||||||
|
"""
|
||||||
|
if not self.draft_order:
|
||||||
|
return []
|
||||||
|
n = len(self.draft_order)
|
||||||
|
count = count if count else len(self.draft_order)
|
||||||
|
start = self.draft_index if from_overall is None else int(from_overall)
|
||||||
|
start = start if include_current else start + 1
|
||||||
|
|
||||||
|
out: List[dict] = []
|
||||||
|
for overall in range(start, start + count):
|
||||||
|
r, p = _round_and_pick(overall, n)
|
||||||
|
order_type = "snake"
|
||||||
|
order = _round_order(r, order_type, self.draft_order)
|
||||||
|
out.append({
|
||||||
|
"overall": overall,
|
||||||
|
"round": r,
|
||||||
|
"pick_in_round": p,
|
||||||
|
"participant": order[p - 1],
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
# === Current Nomination / Bid ===
|
# === Current Nomination / Bid ===
|
||||||
def start_nomination(self, movie_id: int):
|
def start_nomination(self, movie_id: int):
|
||||||
self.cache.set(self.keys.current_movie, movie_id)
|
self.cache.set(self.keys.current_movie, movie_id)
|
||||||
@@ -155,6 +201,7 @@ class DraftStateManager:
|
|||||||
|
|
||||||
# === Sync Snapshot ===
|
# === Sync Snapshot ===
|
||||||
def get_summary(self) -> dict:
|
def get_summary(self) -> dict:
|
||||||
|
picks = self.next_picks(include_current=True)
|
||||||
return {
|
return {
|
||||||
"phase": self.phase,
|
"phase": self.phase,
|
||||||
"draft_order": self.draft_order,
|
"draft_order": self.draft_order,
|
||||||
@@ -164,4 +211,18 @@ class DraftStateManager:
|
|||||||
# "bids": self.get_bids(),
|
# "bids": self.get_bids(),
|
||||||
"bidding_timer_end": self.get_timer_end(),
|
"bidding_timer_end": self.get_timer_end(),
|
||||||
"bidding_timer_start": self.get_timer_start(),
|
"bidding_timer_start": self.get_timer_start(),
|
||||||
|
"current_pick": picks[0] if picks else None,
|
||||||
|
"next_picks": picks[1:] if picks else []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OrderType = Literal["snake", "linear"]
|
||||||
|
def _round_and_pick(overall: int, n: int) -> Tuple[int, int]:
|
||||||
|
"""overall -> (round_1_based, pick_in_round_1_based)"""
|
||||||
|
r = overall // n + 1
|
||||||
|
p = overall % n + 1
|
||||||
|
return r, p
|
||||||
|
|
||||||
|
def _round_order(round_num: int, order_type: OrderType, r1: Sequence[Any]) -> Sequence[Any]:
|
||||||
|
if order_type == "linear" or (round_num % 2 == 1):
|
||||||
|
return r1
|
||||||
|
return list(reversed(r1)) # even rounds in snake
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
{% extends "base.dj.html" %}
|
{% extends "base.dj.html" %}
|
||||||
{% block content %}
|
{% block body %}
|
||||||
<h1>Draft Room: {{ league.name }} – {{ season.label }} {{ season.year }}</h1>
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<script>
|
<script>
|
||||||
window.draftSessionId = "{{ draft_id_hashed }}"
|
window.draftSessionId = "{{ draft_id_hashed }}"
|
||||||
</script>
|
</script>
|
||||||
<div id="draft-participant-root" data-draft-id="{{ draft_id_hashed }}"></div>
|
<div id="draft-participant-root" data-draft-id="{{ draft_id_hashed }}"></div>
|
||||||
{% if DEBUG %}
|
{% endblock body %}
|
||||||
<script src="http://localhost:3000/dist/bundle.js"></script>
|
|
||||||
{% else %}
|
|
||||||
<script src="{% static 'bundle.js' %}"></script>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock content %}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";;
|
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 { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from './constants.js';
|
||||||
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./common/utils.js"
|
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "./common/utils.js"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useWebSocket } from "../WebSocketContext.jsx";
|
import { useWebSocket } from "../common/WebSocketContext.jsx";
|
||||||
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
||||||
import { ParticipantList } from "../common/ParticipantList.jsx";
|
import { ParticipantList } from "../common/ParticipantList.jsx";
|
||||||
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '../constants.js';
|
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");
|
const pad = n => String(n).padStart(2, "0");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="countdown-clock">
|
||||||
<span>
|
<span>
|
||||||
{minutes}:{pad(secs)}
|
{minutes}:{pad(secs)}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// WebSocketContext.jsx
|
// WebSocketContext.jsx
|
||||||
import React, { useState, createContext, useContext, useRef, useEffect } from "react";
|
import React, { useState, createContext, useContext } from "react";
|
||||||
|
|
||||||
const WebSocketContext = createContext(null);
|
const WebSocketContext = createContext(null);
|
||||||
|
|
||||||
@@ -47,7 +47,9 @@ export const handleDraftStatusMessages = (event, setDraftState) => {
|
|||||||
draft_index,
|
draft_index,
|
||||||
current_movie,
|
current_movie,
|
||||||
bidding_timer_end,
|
bidding_timer_end,
|
||||||
bidding_timer_start
|
bidding_timer_start,
|
||||||
|
current_pick,
|
||||||
|
next_picks
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
if (type == DraftMessage.STATUS_SYNC_INFORM) {
|
if (type == DraftMessage.STATUS_SYNC_INFORM) {
|
||||||
@@ -62,7 +64,8 @@ export const handleDraftStatusMessages = (event, setDraftState) => {
|
|||||||
...(phase ? { phase: Number(phase) } : {}),
|
...(phase ? { phase: Number(phase) } : {}),
|
||||||
...(current_movie ? { current_movie } : {}),
|
...(current_movie ? { current_movie } : {}),
|
||||||
...(bidding_timer_end ? { bidding_timer_end: Number(bidding_timer_end) } : {}),
|
...(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,9 +1,9 @@
|
|||||||
// DraftAdmin.jsx
|
// DraftAdmin.jsx
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useWebSocket } from "../WebSocketContext.jsx";
|
import { useWebSocket } from "../common/WebSocketContext.jsx";
|
||||||
import { WebSocketStatus } from "../common/WebSocketStatus.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 { fetchDraftDetails, handleUserIdentifyMessages, isEmptyObject } from "../common/utils.js";
|
||||||
import { DraftMoviePool } from "../common/DraftMoviePool.jsx";
|
import { DraftMoviePool } from "../common/DraftMoviePool.jsx";
|
||||||
import { ParticipantList } from "../common/ParticipantList.jsx";
|
import { ParticipantList } from "../common/ParticipantList.jsx";
|
||||||
@@ -79,26 +79,86 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
|||||||
socket.addEventListener('message', userIdentifyMessageHandler);
|
socket.addEventListener('message', userIdentifyMessageHandler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket.removeEventListener('message', draftStatusMessageHandler)
|
socket.removeEventListener('message', draftStatusMessageHandler);
|
||||||
socket.removeEventListener('message', userIdentifyMessageHandler);
|
socket.removeEventListener('message', userIdentifyMessageHandler);
|
||||||
};
|
};
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container draft-panel">
|
<div className="draft-participant">
|
||||||
<div className="d-flex justify-content-between border-bottom mb-2 p-1">
|
<section class="panel draft-live">
|
||||||
<h3>Draft Panel</h3>
|
<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} />
|
<WebSocketStatus socket={socket} />
|
||||||
</div>
|
</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
|
<ParticipantList
|
||||||
currentUser={currentUser}
|
currentUser={draftState.current_pick?.participant}
|
||||||
draftState={draftState}
|
draftState={draftState}
|
||||||
draftDetails={draftDetails}
|
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>
|
<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>
|
<NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu>
|
||||||
<DraftCountdownClock endTime={draftState.bidding_timer_end}></DraftCountdownClock>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -2,7 +2,7 @@ import './scss/styles.scss'
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
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 { DraftAdmin } from "./apps/draft/admin/DraftAdmin.jsx";
|
||||||
import { DraftParticipant} from './apps/draft/participant/DraftParticipant.jsx'
|
import { DraftParticipant} from './apps/draft/participant/DraftParticipant.jsx'
|
||||||
import { DraftDebug} from './apps/draft/DraftDebug.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