feat: add CardPanel

This commit is contained in:
2025-12-19 00:30:35 +01:00
parent 4706af27f8
commit 4b969e6d69
9 changed files with 349 additions and 70 deletions

View File

@@ -0,0 +1,5 @@
.btn-xs {
--bs-btn-padding-y: .05rem;
--bs-btn-padding-x: .6rem;
--bs-btn-font-size: .75rem;
}

View File

@@ -10,6 +10,7 @@ import {scorePrint, win} from "../../../utils/Tools.js";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
import {toast} from "react-toastify";
import "./CMTMatchPanel.css"
function CupImg() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
@@ -105,15 +106,21 @@ function CMTMatchPanel({catId, cat, menuActions}) {
reducer({type: 'REMOVE', payload: data})
}
const sendCardboard = ({data}) => {
reducer({type: 'UPDATE_CARDBOARD', payload: {...data}})
}
dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}})
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
dispatch({type: 'addListener', payload: {callback: matchOrder, code: 'sendMatchOrder'}})
dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}})
dispatch({type: 'addListener', payload: {callback: sendCardboard, code: 'sendCardboard'}})
return () => {
dispatch({type: 'removeListener', payload: treeListener})
dispatch({type: 'removeListener', payload: matchListener})
dispatch({type: 'removeListener', payload: matchOrder})
dispatch({type: 'removeListener', payload: deleteMatch})
dispatch({type: 'removeListener', payload: sendCardboard})
}
}, [catId]);
@@ -164,6 +171,7 @@ function MatchList({matches, cat, menuActions}) {
const marches2 = matches.filter(m => m.categorie_ord !== -42)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, win: win(m.scores)}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
const match = matches.find(m => m.id === activeMatch)
useEffect(() => {
@@ -175,7 +183,10 @@ function MatchList({matches, cat, menuActions}) {
payload: {
c1: match.c1,
c2: match.c2,
next: marches2.filter(m => !m.end && m.poule === lice && m.id !== activeMatch).map(m => ({c1: m.c1, c2: m.c2}))
next: marches2.filter((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice && m.id !== activeMatch).map(m => ({
c1: m.c1,
c2: m.c2
}))
}
});
}
@@ -187,7 +198,7 @@ function MatchList({matches, cat, menuActions}) {
useEffect(() => {
if (match && match.poule !== lice)
setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id)
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id)
}, [lice]);
useEffect(() => {
@@ -196,10 +207,8 @@ function MatchList({matches, cat, menuActions}) {
if (marches2.some(m => m.id === activeMatch))
return;
setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id);
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id);
}, [matches])
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
return <>
{liceName.length > 1 &&
<div className="input-group" style={{maxWidth: "10em", marginTop: "0.5em"}}>
@@ -230,7 +239,8 @@ function MatchList({matches, cat, menuActions}) {
</thead>
<tbody className="table-group-divider">
{marches2.map((m, index) => (
<tr key={m.id} className={m.id === activeMatch ? "table-info" : (m.poule === lice ? "" : "table-warning")}
<tr key={m.id}
className={m.id === activeMatch ? "table-info" : (liceName[(index - firstIndex) % liceName.length] === lice ? "" : "table-warning")}
onClick={() => setActiveMatch(m.id)}>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{liceName[(index - firstIndex) % liceName.length]}</td>
@@ -326,6 +336,16 @@ function BuildTree({treeData, matches, menuActions}) {
}
function ScorePanel({matchId, match, menuActions}) {
const onClickVoid = useRef(() => {
});
return <div className="row" onClick={onClickVoid.current}>
<ScorePanel_ matchId={matchId} match={match} menuActions={menuActions} onClickVoid_={onClickVoid}/>
<CardPanel matchId={matchId} match={match}/>
</div>
}
function ScorePanel_({matchId, match, menuActions, onClickVoid_}) {
const {sendRequest} = useWS()
const setLoading = useLoadingSwitcher()
@@ -418,6 +438,7 @@ function ScorePanel({matchId, match, menuActions}) {
sel.style.display = "none";
lastScoreClick.current = null;
}
onClickVoid_.current = onClickVoid;
useEffect(() => {
if (!match || match?.end === end)
@@ -463,64 +484,143 @@ function ScorePanel({matchId, match, menuActions}) {
"-999 : forfait"
const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0;
return <div className="row" onClick={onClickVoid}>
<div ref={tableRef} className="col" style={{position: "relative"}}>
<h6>Scores <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
data-bs-html="true"/></h6>
<table className="table table-striped">
<thead>
<tr>
<th style={{textAlign: "center"}} scope="col">Manche</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Rouge</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Bleu</th>
return <div ref={tableRef} className="col" style={{position: "relative"}}>
<h6>Scores <FontAwesomeIcon icon={faCircleQuestion} role="button" data-bs-toggle="tooltip" data-bs-placement="right" data-bs-title={tt}
data-bs-html="true"/></h6>
<table className="table table-striped">
<thead>
<tr>
<th style={{textAlign: "center"}} scope="col">Manche</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Rouge</th>
<th style={{textAlign: "center", minWidth: "4em"}} scope="col">Bleu</th>
</tr>
</thead>
<tbody className="table-group-divider">
{match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => (
<tr key={score.n_round}>
<th style={{textAlign: "center"}}>{score.n_round + 1}</th>
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2] = e}
onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(score.s1)}</td>
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2 + 1] = e}
onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(score.s2)}</td>
</tr>
</thead>
<tbody className="table-group-divider">
{match?.scores && match.scores.sort((a, b) => a.n_round - b.n_round).map(score => (
<tr key={score.n_round}>
<th style={{textAlign: "center"}}>{score.n_round + 1}</th>
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2] = e}
onClick={e => handleScoreClick(e, score.n_round, 1)}>{scorePrint(score.s1)}</td>
<td style={{textAlign: "center"}} ref={e => scoreRef.current[score.n_round * 2 + 1] = e}
onClick={e => handleScoreClick(e, score.n_round, 2)}>{scorePrint(score.s2)}</td>
</tr>
))}
<tr>
<th style={{textAlign: "center"}}></th>
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>-</td>
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>-
</td>
</tr>
</tbody>
</table>
<div style={{textAlign: "right"}}>
<div className="form-check" style={{display: "inline-block"}}>
<input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end}
onChange={e => setEnd(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkboxEnd">Terminé</label>
))}
<tr>
<th style={{textAlign: "center"}}></th>
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2] = e} onClick={e => handleScoreClick(e, -1, 1)}>-</td>
<td style={{textAlign: "center"}} ref={e => scoreRef.current[maxRound * 2 + 1] = e} onClick={e => handleScoreClick(e, -1, 2)}>-
</td>
</tr>
</tbody>
</table>
<div style={{textAlign: "right"}}>
<div className="form-check" style={{display: "inline-block"}}>
<input className="form-check-input" type="checkbox" id="checkboxEnd" name="checkboxEnd" checked={end}
onChange={e => setEnd(e.target.checked)}/>
<label className="form-check-label" htmlFor="checkboxEnd">Terminé</label>
</div>
</div>
<input ref={inputRef} type="number" className="form-control" style={{position: "absolute", top: 0, left: 0, display: "none"}} min="-999"
max="999"
value={scoreIn} onChange={e => setScoreIn(e.target.value)}
onClick={e => e.stopPropagation()}
onKeyDown={e => {
if (e.key === "Tab") {
if (lastScoreClick.current !== null) {
const {round, comb} = lastScoreClick.current;
const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1);
if (nextIndex >= 0 && nextIndex < scoreRef.current.length) {
e.preventDefault();
scoreRef.current[nextIndex].click();
}
}
} else if (e.key === "Enter") {
e.preventDefault();
onClickVoid();
}
}}/>
</div>
}
function CardPanel({matchId, match}) {
const {sendRequest, dispatch} = useWS();
const setLoading = useLoadingSwitcher()
const {data, refresh} = useRequestWS('getCardboardWithoutThis', matchId, setLoading);
useEffect(() => {
refresh('getCardboardWithoutThis', matchId);
const sendCardboard = ({data}) => {
if (data.comb_id === match.c1 || data.comb_id === match.c2) {
refresh('getCardboardWithoutThis', matchId);
}
}
dispatch({type: 'addListener', payload: {callback: sendCardboard, code: 'sendCardboard'}})
return () => dispatch({type: 'removeListener', payload: sendCardboard})
}, [matchId])
if (!match) {
return <div className="col"></div>
}
const c1Cards = match.cardboard?.find(c => c.comb_id === match.c1) || {red: 0, yellow: 0};
const c2Cards = match.cardboard?.find(c => c.comb_id === match.c2) || {red: 0, yellow: 0};
const handleCard = (combId, yellow, red) => {
if (combId === match.c1) {
if (c1Cards.red + red < 0 || c1Cards.yellow + yellow < 0)
return;
} else if (combId === match.c2) {
if (c2Cards.red + red < 0 || c2Cards.yellow + yellow < 0)
return;
} else {
return;
}
setLoading(1)
sendRequest('sendCardboardChange', {matchId, combId, yellow, red})
.finally(() => {
setLoading(0)
})
}
return <div className="col">
<h6>Carton</h6>
<div className="bg-danger-subtle text-danger-emphasis" style={{padding: ".25em", borderRadius: "1em 1em 0 0"}}>
<div>Competition: <span className="badge text-bg-danger">{(data?.c1_red || 0) + c1Cards.red}</span> <span
className="badge text-bg-warning">{(data?.c1_yellow || 0) + c1Cards.yellow}</span></div>
<div className="d-flex justify-content-center align-items-center" style={{margin: ".25em"}}>
Match:
<div className="d-flex flex-column" style={{marginLeft: ".25em"}}>
<button className="col btn btn-xs btn-danger" onClick={__ => handleCard(match.c1, 0, +1)}>+</button>
<span className="badge text-bg-danger">{c1Cards.red}</span>
<button className="col btn btn-xs btn-danger" onClick={__ => handleCard(match.c1, 0, -1)}>-</button>
</div>
<div className="d-flex flex-column" style={{marginLeft: ".25em"}}>
<button className="col btn btn-xs btn-warning" onClick={__ => handleCard(match.c1, +1, 0)}>+</button>
<span className="badge text-bg-warning">{c1Cards.yellow}</span>
<button className="col btn btn-xs btn-warning" onClick={__ => handleCard(match.c1, -1, 0)}>-</button>
</div>
</div>
<input ref={inputRef} type="number" className="form-control" style={{position: "absolute", top: 0, left: 0, display: "none"}} min="-999"
max="999"
value={scoreIn} onChange={e => setScoreIn(e.target.value)}
onClick={e => e.stopPropagation()}
onKeyDown={e => {
if (e.key === "Tab") {
if (lastScoreClick.current !== null) {
const {round, comb} = lastScoreClick.current;
const nextIndex = (round * 2 + (comb - 1)) + (e.shiftKey ? -1 : 1);
if (nextIndex >= 0 && nextIndex < scoreRef.current.length) {
e.preventDefault();
scoreRef.current[nextIndex].click();
}
}
} else if (e.key === "Enter") {
e.preventDefault();
onClickVoid();
}
}}/>
</div>
<div className="col">
<div className="bg-info-subtle text-info-emphasis" style={{padding: ".25em", borderRadius: "0 0 1em 1em"}}>
<div>Competition: <span className="badge text-bg-danger">{(data?.c2_red || 0) + c2Cards.red}</span> <span
className="badge text-bg-warning">{(data?.c2_yellow || 0) + c2Cards.yellow}</span></div>
<div className="d-flex justify-content-center align-items-center" style={{margin: ".25em"}}>
Match:
<div className="d-flex flex-column" style={{marginLeft: ".25em"}}>
<button className="col btn btn-xs btn-danger" onClick={__ => handleCard(match.c2, 0, +1)}>+</button>
<span className="badge text-bg-danger">{c2Cards.red}</span>
<button className="col btn btn-xs btn-danger" onClick={__ => handleCard(match.c2, 0, -1)}>-</button>
</div>
<div className="d-flex flex-column" style={{marginLeft: ".25em"}}>
<button className="col btn btn-xs btn-warning" onClick={__ => handleCard(match.c2, +1, 0)}>+</button>
<span className="badge text-bg-warning">{c2Cards.yellow}</span>
<button className="col btn btn-xs btn-warning" onClick={__ => handleCard(match.c2, -1, 0)}>-</button>
</div>
</div>
</div>
</div>
}

View File

@@ -61,6 +61,8 @@ export function CMTable() {
const windowName = "FFSAFScorePublicWindow";
let tto = [];
function Menu({menuActions}) {
const e = document.getElementById("actionMenu")
const publicAffDispatch = usePubAffDispatch()
@@ -102,6 +104,11 @@ function Menu({menuActions}) {
}
}
for (const x of tto)
x.dispose();
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
tto = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
const handleScore = __ => {
setShowScore(!showScore);
publicAffDispatch({type: 'SET_DATA', payload: {showScore: !showScore}});
@@ -111,10 +118,6 @@ function Menu({menuActions}) {
menuActions.current.switchSore?.();
}
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
const o = [...tooltipTriggerList]
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
if (!e)
return <></>;
return <>
@@ -122,7 +125,8 @@ function Menu({menuActions}) {
<>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer"}} onClick={handleSwitchScore}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Inverser la position des combattants sur cette écran"/>
data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title="Inverser la position des combattants sur cette écran"/>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faDisplay} size="xl"
style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}}

View File

@@ -36,6 +36,22 @@ export function MarchReducer(datas, action) {
datas[index] = action.payload
return [...datas]
}
case 'UPDATE_CARDBOARD':
const idx = datas.findIndex(data => data.id === action.payload.match_id)
if (idx === -1)
return datas // Do nothing
const data = datas[idx]
const tmp = data.cardboard?.find(c => c.comb_id === action.payload.comb_id)
if (tmp) {
tmp.red = action.payload.red
tmp.yellow = action.payload.yellow
} else {
if (!data.cardboard)
data.cardboard = []
data.cardboard.push(action.payload)
}
return [...datas]
case 'SORT':
return datas.sort(action.payload)
case 'REORDER':