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,16 +2,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}My Site{% endblock %}</title>
|
||||
<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">
|
||||
<title>
|
||||
{% block title %}My Site{% endblock %}
|
||||
</title>
|
||||
<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">
|
||||
{% if DEBUG %}
|
||||
<script defer src="http://localhost:3000/dist/bundle.js"></script>
|
||||
<script defer src="http://localhost:3000/dist/bundle.js"></script>
|
||||
{% else %}
|
||||
<script defer src="{% static 'bundle.js' %}"></script>
|
||||
<script defer src="{% static 'bundle.js' %}"></script>
|
||||
{% endif %}
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.3/js/bootstrap.bundle.min.js"></script>
|
||||
@@ -27,39 +28,40 @@
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
{%block navbar%}
|
||||
{%endblock%}
|
||||
{% block navbar %}{% endblock %}
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<div>
|
||||
<div class="border border-secondary rounded p-1">{{ user.username }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="border border-secondary rounded p-1">{{ user.username }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div>
|
||||
<div class="btn btn-outline-secondary">Login</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn btn-outline-secondary">Login</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
|
||||
<main class="container mt-4">
|
||||
{% block breadcrumbs%}
|
||||
<nav aria-label="breadcrumb">
|
||||
{%if breadcrumbs%}
|
||||
<ol class="breadcrumb">
|
||||
{% 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>
|
||||
{%endfor%}
|
||||
</ol>
|
||||
{%endif%}
|
||||
</nav>
|
||||
{% endblock%} {% block content %}
|
||||
<!-- Default content -->
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="text-muted text-center mt-5">
|
||||
<small>© Sack Lunch</small>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
{% block body %}
|
||||
<main class="container mt-4">
|
||||
{% block breadcrumbs %}
|
||||
<nav aria-label="breadcrumb">
|
||||
{% if breadcrumbs %}
|
||||
<ol class="breadcrumb">
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endblock breadcrumbs %}
|
||||
{% block content %}{% endblock content %}
|
||||
{% endblock body %}
|
||||
</main>
|
||||
<footer class="text-muted text-center mt-5">
|
||||
<small>© Sack Lunch</small>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -40,7 +40,7 @@ class DraftConsumerBase(AsyncJsonWebsocketConsumer):
|
||||
|
||||
self.group_names = DraftGroupChannelNames(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"]
|
||||
if not self.should_accept_user():
|
||||
@@ -172,7 +172,7 @@ class DraftAdminConsumer(DraftConsumerBase):
|
||||
await self.start_nominate()
|
||||
|
||||
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(
|
||||
self.group_names.session,
|
||||
{
|
||||
@@ -229,18 +229,22 @@ class DraftAdminConsumer(DraftConsumerBase):
|
||||
)
|
||||
|
||||
async def determine_draft_order(self):
|
||||
draft_order = random.sample(
|
||||
self.draft_participants, len(self.draft_participants)
|
||||
)
|
||||
self.draft_state.draft_order = [p.username for p in draft_order]
|
||||
draft_order = self.draft_state.determine_draft_order(self.draft_participants)
|
||||
self.draft_state.draft_index = 0
|
||||
await self.set_draft_phase(DraftPhase.DETERMINE_ORDER)
|
||||
next_picks = self.draft_state.next_picks(include_current=True)
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
self.group_names.session,
|
||||
{
|
||||
"type": "broadcast.session",
|
||||
"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 django.contrib.auth.models import User
|
||||
from draft.constants import DraftPhase
|
||||
from draft.models import DraftSessionSettings
|
||||
from draft.models import DraftSession
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple
|
||||
import random
|
||||
|
||||
class DraftCacheKeys:
|
||||
def __init__(self, id):
|
||||
@@ -73,12 +76,12 @@ class DraftCacheKeys:
|
||||
# return f"{self.prefix}:user:{user_id}:channel"
|
||||
|
||||
class DraftStateManager:
|
||||
def __init__(self, session_id: int, settings: DraftSessionSettings):
|
||||
self.session_id = session_id
|
||||
def __init__(self, session: DraftSession):
|
||||
self.session_id = session.hashid
|
||||
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.settings = settings
|
||||
self.settings = session.settings
|
||||
|
||||
# === Phase Management ===
|
||||
@property
|
||||
@@ -114,6 +117,13 @@ class DraftStateManager:
|
||||
if not isinstance(draft_order, list):
|
||||
return
|
||||
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
|
||||
def draft_index(self):
|
||||
@@ -122,6 +132,42 @@ class DraftStateManager:
|
||||
@draft_index.setter
|
||||
def draft_index(self, draft_index: int):
|
||||
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 ===
|
||||
def start_nomination(self, movie_id: int):
|
||||
@@ -155,6 +201,7 @@ class DraftStateManager:
|
||||
|
||||
# === Sync Snapshot ===
|
||||
def get_summary(self) -> dict:
|
||||
picks = self.next_picks(include_current=True)
|
||||
return {
|
||||
"phase": self.phase,
|
||||
"draft_order": self.draft_order,
|
||||
@@ -164,4 +211,18 @@ class DraftStateManager:
|
||||
# "bids": self.get_bids(),
|
||||
"bidding_timer_end": self.get_timer_end(),
|
||||
"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" %}
|
||||
{% block content %}
|
||||
<h1>Draft Room: {{ league.name }} – {{ season.label }} {{ season.year }}</h1>
|
||||
{% block body %}
|
||||
{% load static %}
|
||||
<script>
|
||||
window.draftSessionId = "{{ draft_id_hashed }}"
|
||||
</script>
|
||||
<div id="draft-participant-root" data-draft-id="{{ draft_id_hashed }}"></div>
|
||||
{% if DEBUG %}
|
||||
<script src="http://localhost:3000/dist/bundle.js"></script>
|
||||
{% else %}
|
||||
<script src="{% static 'bundle.js' %}"></script>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
{% endblock body %}
|
||||
@@ -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