Add user state updates and bidding error handling in draft consumers
- Implement user state tracking and broadcasting on connect/disconnect and phase changes - Add bid start and place rejection handling with error messages to frontend and backend - Enhance movie serializer with TMDB integration and update relevant frontend components
This commit is contained in:
@@ -38,7 +38,6 @@ export const DraftAdmin = ({ draftSessionId }) => {
|
||||
useEffect(() => {
|
||||
fetchDraftDetails(draftSessionId)
|
||||
.then((data) => {
|
||||
console.log("Fetched draft data", data)
|
||||
setDraftDetails(data)
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -3,73 +3,24 @@ import React, { useEffect, useState, useRef } from "react";
|
||||
|
||||
import { useWebSocket } from "./components/WebSocketContext.jsx";
|
||||
import { WebSocketStatus } from "./components/WebSocketStatus.jsx";
|
||||
import { DraftMessage, DraftPhaseLabel, DraftPhases } from './constants.js';
|
||||
import { fetchDraftDetails, handleUserIdentifyMessages, isEmptyObject } from "./utils.js";
|
||||
import { DraftMessage, DraftPhaseLabel, DraftPhase } from './constants.js';
|
||||
import { fetchDraftDetails, isEmptyObject } from "./utils.js";
|
||||
import { DraftMoviePool } from "./components/DraftMoviePool.jsx";
|
||||
import { ParticipantList } from "./components/ParticipantList.jsx";
|
||||
import { DraftCountdownClock } from "./components/DraftCountdownClock.jsx"
|
||||
import { handleDraftStatusMessages } from './utils.js'
|
||||
import { handleDraftStatusMessages, handleUserStatusMessages, handleUserIdentifyMessages } from './utils.js'
|
||||
// import { Collapse } from 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||
import { Collapse, ListGroup } from "react-bootstrap";
|
||||
|
||||
|
||||
const NominateMenu = ({ socket, draftState, draftDetails, currentUser, }) => {
|
||||
if (!socket || isEmptyObject(draftDetails) || isEmptyObject(draftState)) return;
|
||||
const [open, setOpen] = useState(false);
|
||||
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
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isEmptyObject(draftState) || isEmptyObject(draftState.current_pick)) return;
|
||||
|
||||
if (currentUser == draftState.current_pick.participant) {
|
||||
setOpen(true)
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
// collapse.toggle()
|
||||
}, [draftState])
|
||||
|
||||
return (
|
||||
<Collapse in={open} className="nominate-menu">
|
||||
<div> {/* Everything must be wrapped in one parent */}
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
import { Collapse } from "react-bootstrap";
|
||||
|
||||
export const DraftParticipant = ({ draftSessionId }) => {
|
||||
|
||||
const socket = useWebSocket();
|
||||
const [draftState, setDraftState] = useState({});
|
||||
const [userStatus, setUserState] = useState([]);
|
||||
const [draftDetails, setDraftDetails] = useState({});
|
||||
const [currentUser, setCurrentUser] = useState(null);
|
||||
|
||||
const [movies, setMovies] = useState([]);
|
||||
console.log(socket)
|
||||
|
||||
useEffect(() => {
|
||||
fetchDraftDetails(draftSessionId)
|
||||
@@ -85,12 +36,15 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
|
||||
const draftStatusMessageHandler = (event) => handleDraftStatusMessages(event, setDraftState)
|
||||
const userIdentifyMessageHandler = (event) => handleUserIdentifyMessages(event, setCurrentUser)
|
||||
const userStatusMessageHandler = (event) => handleUserStatusMessages(event, setUserState)
|
||||
socket.addEventListener('message', draftStatusMessageHandler);
|
||||
socket.addEventListener('message', userIdentifyMessageHandler);
|
||||
socket.addEventListener('message', userStatusMessageHandler);
|
||||
|
||||
return () => {
|
||||
socket.removeEventListener('message', draftStatusMessageHandler);
|
||||
socket.removeEventListener('message', userIdentifyMessageHandler);
|
||||
socket.removeEventListener('message', userStatusMessageHandler);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
@@ -98,7 +52,6 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
event.preventDefault()
|
||||
const form = event.target
|
||||
const formData = new FormData(form)
|
||||
console.log('submitting bid...')
|
||||
socket.send(JSON.stringify({
|
||||
type: DraftMessage.BID_PLACE_REQUEST,
|
||||
payload: {
|
||||
@@ -108,8 +61,12 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
const currentUserStatus = userStatus.find(u => u.user == currentUser)
|
||||
const currentMovie = movies.find(i => draftState.current_movie == i.id)
|
||||
|
||||
return (
|
||||
<div className="wrapper">
|
||||
<div className={`wrapper`}>
|
||||
<section id="draft-live">
|
||||
<div className="panel">
|
||||
<header className="panel-header">
|
||||
@@ -120,64 +77,55 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
</div>
|
||||
</header>
|
||||
<div className="panel-body">
|
||||
<div id="draft-clock">
|
||||
<DraftCountdownClock draftState={draftState}></DraftCountdownClock>
|
||||
<div className="pick-description">
|
||||
<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 className="bid-controls btn-group d-flex flex-column">
|
||||
|
||||
<a className="btn btn-primary d-none" data-bs-toggle="collapse" aria-expanded="true" aria-controls="collapse-1" href="#collapse-1" role="button">Show Content</a>
|
||||
<div id="collapse-1" className="collapse show">
|
||||
<div>
|
||||
<div className="row g-0 border rounded-2 m-2">
|
||||
<div className="col-3"><img className="img-fluid flex-fill" /></div>
|
||||
<div className="col d-flex justify-content-center align-items-center">
|
||||
<span className="fw-bold">Movie title</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="lh-sm text-center border p-0 border-bottom"><span>Bids</span></div>
|
||||
<div className="bids-container">
|
||||
<ol className="list-group list-group-flush">
|
||||
{draftState.bids?.reverse().map((b,idx) => (
|
||||
<li key={idx} className="list-group-item p-0">
|
||||
<div className="row g-0">
|
||||
<div className="col-8 col-xl-9 col-xxl-10"><span>{b.user}</span></div>
|
||||
<div className="col"><span>{b.amount}</span></div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
<form id="bid" onSubmit={submitBidRequest}>
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-text">Bid</span>
|
||||
<input className="form-control" type="number" id="bidAmount" name="bidAmount"/>
|
||||
<button className="btn btn-primary" type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<ul className="pick-list">
|
||||
<li>
|
||||
<div>Current Pick: {draftState.current_pick?.participant}</div>
|
||||
</li>
|
||||
<li
|
||||
>
|
||||
<div>Next Pick: {draftState.next_picks ? draftState.next_picks[0]?.participant : ""}</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div id="draft-clock">
|
||||
<DraftCountdownClock draftState={draftState}></DraftCountdownClock>
|
||||
<div className="pick-description">
|
||||
<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 className="bid-controls btn-group d-flex flex-column">
|
||||
<Collapse in={draftState.phase == DraftPhase.BIDDING}>
|
||||
<div>
|
||||
<div>
|
||||
<div className="row g-0 border rounded-2 m-2">
|
||||
<div className="col-3">
|
||||
<img className="img-fluid flex-fill" src={currentMovie?.tmdb_data?.poster_url}/>
|
||||
</div>
|
||||
<div className="col d-flex justify-content-center align-items-center">
|
||||
<span className="fw-bold">{currentMovie?.title}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="lh-sm text-center border p-0 border-bottom"><span>Bids</span></div>
|
||||
<div className="bids-container">
|
||||
<ol className="list-group list-group-flush">
|
||||
{draftState.bids?.reverse().map((b, idx) => (
|
||||
<li key={idx} className="list-group-item p-0">
|
||||
<div className="row g-0">
|
||||
<div className="col-8 col-xl-9 col-xxl-10"><span>{b.user}</span></div>
|
||||
<div className="col"><span>{b.amount}</span></div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-1">
|
||||
<form id="bid" onSubmit={submitBidRequest}>
|
||||
<div className="input-group input-group-sm">
|
||||
<span className="input-group-text">Bid</span>
|
||||
<input className="form-control" type="number" id="bidAmount" name="bidAmount" />
|
||||
<button className="btn btn-primary" type="submit">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -185,16 +133,11 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
|
||||
<section className="panel" id="draft-slate">
|
||||
<header className="panel-header">
|
||||
<div className="panel-title"><span>Draft Catalog</span></div>
|
||||
<div className="panel-title"><span>Films</span></div>
|
||||
</header>
|
||||
<div className="panel-body">
|
||||
<div className="current-movie card">
|
||||
<span>Current Nomination: {movies.find(i => draftState.current_movie == i.id)?.title}</span>
|
||||
</div>
|
||||
{/* <NominateMenu socket={socket} currentUser={currentUser} draftState={draftState} draftDetails={draftDetails}></NominateMenu> */}
|
||||
<div className="movie-filters"></div>
|
||||
|
||||
<DraftMoviePool isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
||||
<DraftMoviePool currentUserStatus={currentUserStatus} currentUser={currentUser} socket={socket} isParticipant={true} draftDetails={draftDetails} draftState={draftState}></DraftMoviePool>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -217,16 +160,7 @@ export const DraftParticipant = ({ draftSessionId }) => {
|
||||
<div className="panel-title"><span>Teams</span></div>
|
||||
</header>
|
||||
<div className="panel-body">
|
||||
<ul className="team-list list-group">
|
||||
{draftState.participants?.map(p => (
|
||||
<li className="team-item list-group-item" key={p}>
|
||||
<div className="team-name fw-bold">{p}</div>
|
||||
<ul className="team-movie-list list-group list-group-flush">
|
||||
<li className="team-movie-item list-group-item"></li>
|
||||
</ul>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ParticipantList currentUser={currentUser} className="team-list" draftDetails={draftDetails} draftState={draftState} isAdmin={isAdmin}></ParticipantList>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export function DraftCountdownClock({ draftState }) {
|
||||
// endTime is in seconds (Unix time)
|
||||
const {bidding_timer_end, onFinish} = draftState
|
||||
const { bidding_timer_end, onFinish } = draftState;
|
||||
const getTimeLeft = (et) => Math.max(0, Math.floor(et - Date.now() / 1000));
|
||||
const [timeLeft, setTimeLeft] = useState(getTimeLeft(bidding_timer_end));
|
||||
|
||||
useEffect(() => {
|
||||
if (timeLeft <= 0) {
|
||||
setTimeLeft(getTimeLeft(bidding_timer_end)); // reset timer when bidding_timer_end changes
|
||||
|
||||
if (getTimeLeft(bidding_timer_end) <= 0) {
|
||||
if (onFinish) onFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setInterval(() => {
|
||||
const t = getTimeLeft(bidding_timer_end);
|
||||
setTimeLeft(t);
|
||||
if (t <= 0 && onFinish) onFinish();
|
||||
}, 100);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
// eslint-disable-next-line
|
||||
}, [bidding_timer_end, onFinish, timeLeft]);
|
||||
}, [bidding_timer_end, onFinish]);
|
||||
|
||||
const minutes = Math.floor(timeLeft / 60);
|
||||
const secs = timeLeft % 60;
|
||||
|
||||
@@ -1,23 +1,87 @@
|
||||
import React from "react";
|
||||
import { isEmptyObject } from "../utils";
|
||||
import { DraftMessage } from "../constants";
|
||||
|
||||
export const DraftMoviePool = ({ isParticipant, draftDetails, draftState }) => {
|
||||
if(isEmptyObject(draftDetails)) {return}
|
||||
const {movies} = draftDetails
|
||||
const {current_movie} = draftState
|
||||
const NominateForm = ({ socket, currentUser, movie, className}) => {
|
||||
|
||||
const requestNomination = (event) => {
|
||||
event.preventDefault()
|
||||
const formData = new FormData(event.target)
|
||||
socket.send(JSON.stringify({
|
||||
type: DraftMessage.NOMINATION_SUBMIT_REQUEST,
|
||||
payload: {
|
||||
movie_id: formData.get('movie_id'),
|
||||
user: currentUser
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={requestNomination} className={className}>
|
||||
<input type="hidden" name="movie_id" value={movie.id} />
|
||||
<button type="submit" className="btn btn-primary nominate">
|
||||
Nominate
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const DraftMoviePool = ({ socket, currentUser, currentUserStatus, draftDetails, draftState, isNominating = false }) => {
|
||||
if (isEmptyObject(draftDetails)) { return }
|
||||
const { movies } = draftDetails
|
||||
const { current_movie } = draftState
|
||||
const can_nominate = currentUserStatus?.can_nominate
|
||||
const is_admin = currentUserStatus?.is_admin
|
||||
|
||||
const nominateHandler = (event) => {
|
||||
event.preventDefault()
|
||||
const formData = new FormData(event.target)
|
||||
const movieId = formData.get('movie_id');
|
||||
socket.send(JSON.stringify({
|
||||
type: DraftMessage.NOMINATION_SUBMIT_REQUEST,
|
||||
payload: {
|
||||
movie_id: movieId,
|
||||
user: currentUser
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="movie-pool-container">
|
||||
<label>Movies</label>
|
||||
<ul>
|
||||
{movies.map(m => (
|
||||
<li key={m.id} className={`${current_movie == m.id ? "current-movie fw-bold" : null }`}>
|
||||
<a href={`/api/movie/${m.id}/detail`}>
|
||||
{m.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Poster</th>
|
||||
<th>Title</th>
|
||||
<th>Release Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{movies.map(m => (
|
||||
<tr key={m.id} className={`${current_movie == m.id ? "current-movie fw-bold" : null}`}>
|
||||
<td><img src={m.tmdb_data.poster_url}></img></td>
|
||||
<td>
|
||||
<div>
|
||||
<a href={`/api/movie/${m.id}/detail`} className="fs-5">
|
||||
{m.title}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href={`https://www.themoviedb.org/movie/${m.tmdb_data.id}`}>
|
||||
TMDB
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
{can_nominate || is_admin ? (
|
||||
<NominateForm socket={socket} currentUser={currentUser} movie={m} className={!can_nominate && is_admin ? 'admin-override' : ''}></NominateForm>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
<td>{m.tmdb_data.release_date}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +1,38 @@
|
||||
import React from "react";
|
||||
import { fetchDraftDetails, isEmptyObject } from "../utils.js"
|
||||
import Badge from 'react-bootstrap/Badge';
|
||||
|
||||
export const ParticipantList = ({ isAdmin, draftState, draftDetails, currentUser }) => {
|
||||
if (isEmptyObject(draftState) || isEmptyObject(draftDetails)) { console.warn('empty draft state', draftState); return }
|
||||
if (isEmptyObject(draftState) || isEmptyObject(draftDetails)) { return }
|
||||
const { draft_order, draft_index, connected_participants } = draftState
|
||||
const { participants } = draftDetails
|
||||
|
||||
const ListTag = draft_order?.length > 0 ? "ol" : "ul"
|
||||
const listItems = draft_order?.length > 0 ? draft_order.map(d => participants.find(p => p.username == d)) : participants
|
||||
|
||||
|
||||
return (
|
||||
<div className="participant-list-container">
|
||||
<label>Particpants</label>
|
||||
<ListTag className="participant-list">
|
||||
{listItems.map((p, i) => (
|
||||
<li key={i} className={`${i == draft_index ? "fw-bold" : ""}`}>
|
||||
<span className={`${p.username == currentUser ? "current-user" : ""}`}>{p?.full_name}</span>
|
||||
{isAdmin ? (
|
||||
<div
|
||||
<ListTag className="participant-list">
|
||||
{listItems.map((p, idx) => (
|
||||
<li className="team-item" key={idx}>
|
||||
|
||||
<div className={`team-name ${p.username == currentUser ? "current-user" : ""}`}>
|
||||
<div>
|
||||
{p.full_name}
|
||||
{p.username == draftState.current_pick?.participant ? (<Badge bg="warning" className="ms-1">Current Pick</Badge>) : null}
|
||||
</div>
|
||||
<ul className="team-movie-list list-group list-group-flush">
|
||||
<li className="team-movie-item list-group-item"></li>
|
||||
</ul>
|
||||
</div>
|
||||
{isAdmin === "True" ? (
|
||||
<div
|
||||
className={
|
||||
`ms-2 stop-light ${connected_participants.includes(p?.username) ? "success" : "danger"}`
|
||||
}
|
||||
></div>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export const DraftMessage = {
|
||||
USER_JOIN_INFORM: "user.join.inform",
|
||||
USER_LEAVE_INFORM: "user.leave.inform",
|
||||
USER_IDENTIFICATION_INFORM: "user.identification.inform",
|
||||
USER_STATE_INFORM: "user.state.inform",
|
||||
PHASE_CHANGE_INFORM: "phase.change.inform",
|
||||
PHASE_CHANGE_REQUEST: "phase.change.request",
|
||||
PHASE_CHANGE_CONFIRM: "phase.change.confirm",
|
||||
@@ -21,8 +22,10 @@ export const DraftMessage = {
|
||||
ORDER_DETERMINE_CONFIRM: "order.determine.confirm",
|
||||
BID_START_INFORM: "bid.start.inform",
|
||||
BID_START_REQUEST: "bid.start.request",
|
||||
BID_START_REJECT: "bid.start.reject",
|
||||
BID_PLACE_REQUEST: "bid.place.request",
|
||||
BID_PLACE_CONFIRM: "bid.update.confirm",
|
||||
BID_PLACE_REJECT: "bid.place.reject",
|
||||
BID_PLACE_CONFIRM: "bid.place.confirm",
|
||||
BID_UPDATE_INFORM: "bid.update.inform",
|
||||
BID_END_INFORM: "bid.end.inform",
|
||||
NOMINATION_SUBMIT_REQUEST: "nomination.submit.request",
|
||||
|
||||
@@ -50,8 +50,16 @@ export const handleUserIdentifyMessages = (event, setUser) => {
|
||||
const { type, payload } = message;
|
||||
|
||||
if (type == DraftMessage.USER_IDENTIFICATION_INFORM) {
|
||||
console.log("Message: ", type, event.data);
|
||||
const { user } = payload;
|
||||
setUser(user);
|
||||
}
|
||||
};
|
||||
|
||||
export const handleUserStatusMessages = (event, setUserStatus) => {
|
||||
const message = JSON.parse(event.data);
|
||||
const { type, payload } = message;
|
||||
|
||||
if (type == DraftMessage.USER_STATE_INFORM) {
|
||||
setUserStatus(payload);
|
||||
}
|
||||
};
|
||||
@@ -11,13 +11,13 @@ import { DraftDebug} from './apps/draft/DraftDebug.jsx'
|
||||
const draftAdminBarRoot = document.getElementById("draft-admin-bar-root");
|
||||
const draftPartipantRoot = document.getElementById("draft-participant-root")
|
||||
const draftDebugRoot = document.getElementById("draft-debug-root")
|
||||
const {draftSessionId} = window; // from backend template
|
||||
const {draftSessionId, isAdmin} = window; // from backend template
|
||||
|
||||
if (draftPartipantRoot) {
|
||||
const wsUrl = `ws://${window.location.host}/ws/draft/session/${draftSessionId}/participant`;
|
||||
createRoot(draftPartipantRoot).render(
|
||||
<WebSocketProvider url={wsUrl}>
|
||||
<DraftParticipant draftSessionId={draftSessionId} />
|
||||
<DraftParticipant draftSessionId={draftSessionId} className={`${isAdmin ? 'admin':''}`}/>
|
||||
</WebSocketProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use "../../node_modules/bootstrap/scss/bootstrap.scss";
|
||||
@use "./fonts/graphique.css";
|
||||
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=League+Gothic&family=Oswald:wght@200..700&display=swap');
|
||||
@import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&family=League+Gothic&family=Oswald:wght@200..700&display=swap");
|
||||
// Import only functions & variables
|
||||
@import "~bootstrap/scss/functions";
|
||||
@import "~bootstrap/scss/variables";
|
||||
@@ -95,38 +95,62 @@
|
||||
}
|
||||
}
|
||||
|
||||
.participant-list-container,
|
||||
.movie-pool-container {
|
||||
max-width: 575.98px;
|
||||
label {
|
||||
@extend .fs-3;
|
||||
}
|
||||
ol.participant-list {
|
||||
@extend .list-group-numbered;
|
||||
}
|
||||
|
||||
ol.participant-list,
|
||||
ul.participant-list {
|
||||
@extend .list-group;
|
||||
ol,
|
||||
ul {
|
||||
@extend .p-0;
|
||||
}
|
||||
ol {
|
||||
@extend .list-group-numbered;
|
||||
}
|
||||
li {
|
||||
@extend .list-group-item;
|
||||
@extend .d-flex;
|
||||
@extend .justify-content-between;
|
||||
@extend .align-items-center;
|
||||
span {
|
||||
@extend .me-auto;
|
||||
@extend .ps-1;
|
||||
.team-name {
|
||||
@extend .flex-grow-1;
|
||||
@extend .ps-2;
|
||||
}
|
||||
.team-movie-list {
|
||||
li {
|
||||
@extend .p-0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.current-user {
|
||||
@extend .fw-bold;
|
||||
&::after {
|
||||
content: " *";
|
||||
// content: " *";
|
||||
font-size: 1em; // adjust as needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.movie-pool-container {
|
||||
img {
|
||||
height: 128px;
|
||||
}
|
||||
a {
|
||||
@extend .text-decoration-none;
|
||||
@extend .text-reset;
|
||||
}
|
||||
thead {
|
||||
display: block;
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: block;
|
||||
// height: 200px; /* or any desired height */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
width: 150px; /* Set consistent widths to align columns */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
#draft-admin-bar {
|
||||
@extend .d-flex;
|
||||
@extend .flex-column;
|
||||
@@ -137,7 +161,13 @@
|
||||
@extend .shadow-sm;
|
||||
div {
|
||||
@extend .d-flex;
|
||||
@extend .justify-content-center
|
||||
@extend .justify-content-center;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-override {
|
||||
button {
|
||||
@extend .btn-warning;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +225,7 @@
|
||||
@extend .text-bg-dark;
|
||||
@extend .lh-1;
|
||||
.countdown-clock {
|
||||
font-family: 'League Gothic';
|
||||
font-family: "League Gothic";
|
||||
font-size: $font-size-base * 5;
|
||||
@extend .fw-bolder;
|
||||
@extend .col;
|
||||
@@ -207,7 +237,8 @@
|
||||
@extend .align-content-center;
|
||||
}
|
||||
}
|
||||
div:has(.pick-list), div:has(.bid-list){
|
||||
div:has(.pick-list),
|
||||
div:has(.bid-list) {
|
||||
ul {
|
||||
@extend .list-group;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user