Add nomination submission and bidding start workflow
- Added `BID_START_REQUEST` and `NOMINATION_SUBMIT_REQUEST` handling in backend consumers. - Extended draft state to include `current_movie` and `bids` cache keys. - Updated frontend to: - Allow participants to nominate movies when it's their turn. - Enable admins to start bidding for the nominated movie. - Highlight the current nominated movie and the current user. - Synced state updates across clients via WebSocket events.
This commit is contained in:
@@ -6,6 +6,7 @@ import { ParticipantList } from "../common/ParticipantList.jsx";
|
||||
import { DraftMessage, DraftPhase, DraftPhaseLabel, DraftPhasesOrdered } from '../constants.js';
|
||||
import { fetchDraftDetails, isEmptyObject, handleDraftStatusMessages, handleUserIdentifyMessages } from "../common/utils.js"
|
||||
import { DraftMoviePool } from "../common/DraftMoviePool.jsx"
|
||||
import { jsxs } from "react/jsx-runtime";
|
||||
|
||||
|
||||
|
||||
@@ -65,12 +66,28 @@ export const DraftAdmin = ({ draftSessionId }) => {
|
||||
|
||||
const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState)
|
||||
const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser)
|
||||
const handleNominationRequest = (event)=> {
|
||||
const message = JSON.parse(event.data)
|
||||
const { type, payload } = message;
|
||||
console.log('passing through nomination request', message)
|
||||
if (type == DraftMessage.NOMINATION_SUBMIT_REQUEST) {
|
||||
socket.send(JSON.stringify(
|
||||
{
|
||||
type: DraftMessage.NOMINATION_SUBMIT_REQUEST,
|
||||
payload
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
socket.addEventListener('message', draftStatusMessageHandler );
|
||||
socket.addEventListener('message', userIdentifyMessageHandler );
|
||||
socket.addEventListener('message', handleNominationRequest );
|
||||
|
||||
|
||||
return () => {
|
||||
socket.removeEventListener('message', draftStatusMessageHandler)
|
||||
socket.removeEventListener('message', userIdentifyMessageHandler );
|
||||
socket.remove('message', handleNominationRequest );
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
@@ -114,6 +131,14 @@ export const DraftAdmin = ({ draftSessionId }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const handleStartBidding = () => {
|
||||
socket.send(
|
||||
JSON.stringify(
|
||||
{type: DraftMessage.BID_START_REQUEST}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container draft-panel admin">
|
||||
<div className="d-flex justify-content-between border-bottom mb-2 p-1">
|
||||
@@ -127,12 +152,16 @@ export const DraftAdmin = ({ draftSessionId }) => {
|
||||
</div>
|
||||
|
||||
<ParticipantList
|
||||
currentUser = {currentUser}
|
||||
draftState={draftState}
|
||||
draftDetails={draftDetails}
|
||||
isAdmin={true}
|
||||
/>
|
||||
<button onClick={handleAdvanceDraft} className="btn btn-primary">Advance Draft</button>
|
||||
<DraftMoviePool draftDetails={draftDetails}></DraftMoviePool>
|
||||
<div className="d-flex gap-1 m-1">
|
||||
<button onClick={handleAdvanceDraft} className="btn btn-primary">Advance Draft</button>
|
||||
<button onClick={handleStartBidding} className="btn btn-primary">Start Bidding</button>
|
||||
</div>
|
||||
<DraftMoviePool draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
||||
|
||||
<DraftPhaseDisplay draftPhase={draftState.phase} nextPhaseHandler={() => { handlePhaseChange('next') }} prevPhaseHandler={() => { handlePhaseChange('previous') }}></DraftPhaseDisplay>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from "react";
|
||||
import { isEmptyObject } from "./utils";
|
||||
|
||||
export const DraftMoviePool = ({ draftDetails }) => {
|
||||
export const DraftMoviePool = ({ isParticipant, draftDetails, draftState }) => {
|
||||
if(isEmptyObject(draftDetails)) {return}
|
||||
const {movies} = draftDetails
|
||||
const {current_movie} = draftState
|
||||
|
||||
return (
|
||||
<div className="movie-pool-container">
|
||||
<ul>
|
||||
{movies.map(m => (
|
||||
<li key={m.id}>
|
||||
<li key={m.id} className={`${current_movie == m.id ? "current-movie fw-bold" : null }`}>
|
||||
<a href={`/api/movie/${m.id}/detail`}>
|
||||
{m.title}
|
||||
</a>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from "react";
|
||||
import { fetchDraftDetails, isEmptyObject } from "../common/utils.js"
|
||||
|
||||
export const ParticipantList = ({ isAdmin, draftState, draftDetails }) => {
|
||||
export const ParticipantList = ({ isAdmin, draftState, draftDetails, currentUser }) => {
|
||||
if (isEmptyObject(draftState) || isEmptyObject(draftDetails)) { console.warn('empty draft state', draftState); return }
|
||||
const { draft_order, draft_index, connected_participants } = draftState
|
||||
const { participants } = draftDetails
|
||||
|
||||
const ListTag = draft_order.length > 0 ? "ol" : "ul"
|
||||
console.log
|
||||
const listItems = draft_order.length > 0 ? draft_order.map(d => participants.find(p => p.username == d)) : participants
|
||||
|
||||
|
||||
@@ -17,7 +16,7 @@ export const ParticipantList = ({ isAdmin, draftState, draftDetails }) => {
|
||||
<ListTag className="participant-list">
|
||||
{listItems.map((p, i) => (
|
||||
<li key={i} className={`${i == draft_index ? "fw-bold" : ""}`}>
|
||||
<span>{p?.full_name}</span>
|
||||
<span className={`${p.username == currentUser ? "current-user" : ""}`}>{p?.full_name}</span>
|
||||
{isAdmin ? (
|
||||
<div
|
||||
className={
|
||||
|
||||
@@ -40,7 +40,7 @@ export const handleDraftStatusMessages = (event, setDraftState) => {
|
||||
console.log("Message: ", type, event?.data)
|
||||
|
||||
if (!payload) return
|
||||
const {connected_participants, phase, draft_order, draft_index} = payload
|
||||
const {connected_participants, phase, draft_order, draft_index, current_movie} = payload
|
||||
|
||||
if (type == DraftMessage.STATUS_SYNC_INFORM) {
|
||||
setDraftState(payload)
|
||||
@@ -52,6 +52,7 @@ export const handleDraftStatusMessages = (event, setDraftState) => {
|
||||
...(draft_order ? { draft_order } : {}),
|
||||
...(draft_index ? { draft_index } : {}),
|
||||
...(phase ? { phase: Number(phase) } : {}),
|
||||
...(current_movie ? {current_movie} : {}),
|
||||
}))
|
||||
|
||||
}
|
||||
@@ -59,9 +60,10 @@ export const handleDraftStatusMessages = (event, setDraftState) => {
|
||||
export const handleUserIdentifyMessages = (event, setUser) => {
|
||||
const message = JSON.parse(event.data)
|
||||
const { type, payload } = message;
|
||||
console.log("Message: ", type, event?.data)
|
||||
|
||||
if (!payload) return
|
||||
const {current_user} = payload
|
||||
setUser(current_user)
|
||||
|
||||
if (type==DraftMessage.USER_IDENTIFICATION_INFORM){
|
||||
console.log("Message: ", type, event.data)
|
||||
const {user} = payload
|
||||
setUser(user)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export const DraftMessage = {
|
||||
ORDER_DETERMINE_REQUEST: "order.determine.request",
|
||||
ORDER_DETERMINE_CONFIRM: "order.determine.confirm",
|
||||
BID_START_INFORM: "bid.start.inform",
|
||||
BID_START_REQUEST: "bid.start.request",
|
||||
BID_PLACE_REQUEST: "bid.place.request",
|
||||
BID_UPDATE_INFORM: "bid.update.inform",
|
||||
BID_END_INFORM: "bid.end.inform",
|
||||
|
||||
@@ -3,17 +3,53 @@ import React, { useEffect, useState } from "react";
|
||||
|
||||
import { useWebSocket } from "../WebSocketContext.jsx";
|
||||
import { WebSocketStatus } from "../common/WebSocketStatus.jsx";
|
||||
import { DraftMessage, DraftPhases} from '../constants.js';
|
||||
import { fetchDraftDetails } from "../common/utils.js";
|
||||
import { DraftMessage, 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 { handleDraftStatusMessages } from '../common/utils.js'
|
||||
|
||||
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 requestNomination = (event) => {
|
||||
event.preventDefault()
|
||||
const formData = new FormData(event.target)
|
||||
socket.send(JSON.stringify({
|
||||
type: DraftMessage.NOMINATION_SUBMIT_REQUEST,
|
||||
payload: {
|
||||
id: formData.get('movie'),
|
||||
user: currentUser
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label>Nominate</label>
|
||||
{draftState.draft_order[draftState.draft_index]}
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const DraftParticipant = ({ draftSessionId }) => {
|
||||
const socket = useWebSocket();
|
||||
const [draftState, setDraftState] = useState({});
|
||||
const [draftDetails, setDraftDetails] = useState({});
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
|
||||
const [movies, setMovies] = useState([]);
|
||||
console.log(socket)
|
||||
@@ -27,7 +63,7 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
})
|
||||
}, [draftSessionId])
|
||||
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
socket.onclose = (event) => {
|
||||
console.log('Websocket Closed')
|
||||
@@ -37,12 +73,14 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
const handler = (event) => handleDraftStatusMessages(event, setDraftState)
|
||||
socket.addEventListener('message', handler );
|
||||
const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState)
|
||||
const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser)
|
||||
socket.addEventListener('message', draftStatusMessageHandler);
|
||||
socket.addEventListener('message', userIdentifyMessageHandler);
|
||||
|
||||
return () => {
|
||||
socket.addEventListener('message', handler );
|
||||
socket.close();
|
||||
socket.removeEventListener('message', draftStatusMessageHandler)
|
||||
socket.removeEventListener('message', userIdentifyMessageHandler);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
@@ -53,11 +91,13 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
<WebSocketStatus socket={socket} />
|
||||
</div>
|
||||
<ParticipantList
|
||||
currentUser={currentUser}
|
||||
draftState={draftState}
|
||||
draftDetails={draftDetails}
|
||||
isAdmin={false}
|
||||
/>
|
||||
<DraftMoviePool draftDetails={draftDetails}></DraftMoviePool>
|
||||
<DraftMoviePool isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
||||
|
||||
<NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -63,8 +63,6 @@
|
||||
@extend .fs-3;
|
||||
}
|
||||
.change-phase {
|
||||
|
||||
|
||||
button {
|
||||
@extend .btn;
|
||||
@extend .btn-light;
|
||||
@@ -118,4 +116,10 @@
|
||||
@extend .ps-1;
|
||||
}
|
||||
}
|
||||
.current-user {
|
||||
&::after {
|
||||
content: " *";
|
||||
font-size: 1em; // adjust as needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user