1 Commits

Author SHA1 Message Date
56c4b143d0 feat: add public screen 2025-12-15 23:07:41 +01:00
22 changed files with 552 additions and 1208 deletions

View File

@@ -66,19 +66,15 @@ public class MatchModel {
List<CardboardModel> cardboard = new ArrayList<>();
public String getC1Name() {
if (c1_id != null)
return c1_id.fname + " " + c1_id.lname;
if (c1_guest != null)
if (c1_id == null)
return c1_guest.fname + " " + c1_guest.lname;
return "";
return c1_id.fname + " " + c1_id.lname;
}
public String getC2Name() {
if (c2_id != null)
return c2_id.fname + " " + c2_id.lname;
if (c2_guest != null)
if (c2_id == null)
return c2_guest.fname + " " + c2_guest.lname;
return "";
return c2_id.fname + " " + c2_id.lname;
}
public int win() {

View File

@@ -1,9 +0,0 @@
package fr.titionfire.ffsaf.data.repository;
import fr.titionfire.ffsaf.data.model.CardboardModel;
import io.quarkus.hibernate.reactive.panache.PanacheRepositoryBase;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CardboardRepository implements PanacheRepositoryBase<CardboardModel, Long> {
}

View File

@@ -17,10 +17,7 @@ public class CardboardEntity {
int yellow;
public static CardboardEntity fromModel(CardboardModel model) {
return new CardboardEntity(
model.getComb() != null ? model.getComb().getId() : model.getGuestComb().getId() * -1,
model.getMatch().getId(),
model.getCompet().getId(),
return new CardboardEntity(model.getComb().getId(), model.getMatch().getId(), model.getCompet().getId(),
model.getRed(), model.getYellow());
}
}

View File

@@ -56,7 +56,7 @@ public class CompetPermService {
CompletableFuture<SimpleCompet> f = new CompletableFuture<>();
SReqCompet.getConfig(serverCustom.clients, id, f);
try {
return f.get(500, TimeUnit.MILLISECONDS);
return f.get(1500, TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
}
@@ -71,8 +71,7 @@ public class CompetPermService {
.chain(competitionModels -> {
CompletableFuture<HashMap<String, String>> f = new CompletableFuture<>();
SReqCompet.getAllHaveAccess(serverCustom.clients, securityCtx.getSubject(), f);
return Uni.createFrom().future(f, Duration.ofMillis(500))
.onFailure().recoverWithItem(new HashMap<>())
return Uni.createFrom().future(f, Duration.ofMillis(1500))
.map(map_ -> {
HashMap<Long, String> map = new HashMap<>();
map_.forEach((key, value) -> map.put(Long.parseLong(key), value));

View File

@@ -162,14 +162,6 @@ public class CompetitionService {
.map(pouleModels -> pouleModels.stream().map(CompetitionData::fromModel).toList());
}
public Uni<List<CompetitionData>> getAllSystemTable(SecurityCtx securityCtx,
CompetitionSystem system) {
return repository.list("system = ?1", system)
.chain(l -> Uni.join().all(l.stream().map(cm -> permService.hasTablePerm(securityCtx, cm)).toList())
.andCollectFailures())
.map(l -> l.stream().filter(Objects::nonNull).map(CompetitionData::fromModel).toList());
}
public Uni<CompetitionData> addOrUpdate(SecurityCtx securityCtx, CompetitionData data) {
if (data.getId() == null) {
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
@@ -504,10 +496,6 @@ public class CompetitionService {
.andCollectFailures()))
.call(competitionModel -> Panache.withTransaction(
() -> categoryRepository.delete("compet = ?1", competitionModel)))
.call(competitionModel -> Panache.withTransaction(
() -> registerRepository.delete("competition = ?1", competitionModel)))
.call(competitionModel -> Panache.withTransaction(
() -> competitionGuestRepository.delete("competition = ?1", competitionModel)))
.chain(model -> Panache.withTransaction(() -> repository.delete("id", model.getId())))
.invoke(o -> SReqCompet.rmCompet(serverCustom.clients, id))
.call(__ -> cache.invalidate(id));

View File

@@ -47,12 +47,4 @@ public class CompetitionAdminEndpoints {
public Uni<List<CompetitionData>> getAllSystemAdmin(@PathParam("system") CompetitionSystem system) {
return service.getAllSystemAdmin(securityCtx, system);
}
@GET
@Path("all/{system}/table")
@Authenticated
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<CompetitionData>> getAllSystemTable(@PathParam("system") CompetitionSystem system) {
return service.getAllSystemTable(securityCtx, system);
}
}

View File

@@ -6,7 +6,10 @@ import fr.titionfire.ffsaf.domain.service.CompetPermService;
import fr.titionfire.ffsaf.net2.MessageType;
import fr.titionfire.ffsaf.utils.SecurityCtx;
import fr.titionfire.ffsaf.ws.data.WelcomeInfo;
import fr.titionfire.ffsaf.ws.recv.*;
import fr.titionfire.ffsaf.ws.recv.RCategorie;
import fr.titionfire.ffsaf.ws.recv.RMatch;
import fr.titionfire.ffsaf.ws.recv.RRegister;
import fr.titionfire.ffsaf.ws.recv.WSReceiver;
import fr.titionfire.ffsaf.ws.send.JsonUni;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.security.Authenticated;
@@ -41,9 +44,6 @@ public class CompetitionWS {
@Inject
RRegister rRegister;
@Inject
RCardboard rCardboard;
@Inject
SecurityCtx securityCtx;
@@ -77,7 +77,6 @@ public class CompetitionWS {
getWSReceiverMethods(RMatch.class, rMatch);
getWSReceiverMethods(RCategorie.class, rCategorie);
getWSReceiverMethods(RRegister.class, rRegister);
getWSReceiverMethods(RCardboard.class, rCardboard);
}
@OnOpen

View File

@@ -1,128 +0,0 @@
package fr.titionfire.ffsaf.ws.recv;
import fr.titionfire.ffsaf.data.model.CardboardModel;
import fr.titionfire.ffsaf.data.model.MatchModel;
import fr.titionfire.ffsaf.data.repository.CardboardRepository;
import fr.titionfire.ffsaf.data.repository.MatchRepository;
import fr.titionfire.ffsaf.domain.entity.CardboardEntity;
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
import fr.titionfire.ffsaf.ws.PermLevel;
import fr.titionfire.ffsaf.ws.send.SSCardboard;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.Data;
import java.util.Objects;
@WithSession
@ApplicationScoped
@RegisterForReflection
public class RCardboard {
@Inject
MatchRepository matchRepository;
@Inject
CardboardRepository cardboardRepository;
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
return matchRepository.findById(id)
.invoke(Unchecked.consumer(o -> {
if (o == null)
throw new DNotFoundException("Matche non trouver");
if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid")))
throw new DForbiddenException("Permission denied");
}));
}
@WSReceiver(code = "sendCardboardChange", permission = PermLevel.TABLE)
public Uni<Void> sendCardboardChange(WebSocketConnection connection, SendCardboard card) {
return getById(card.matchId, connection)
.chain(matchModel -> cardboardRepository.find("(comb.id = ?1 OR guestComb.id = ?2) AND match.id = ?3",
card.combId, card.combId * -1, card.matchId).firstResult()
.chain(model -> {
if (model != null) {
model.setRed(model.getRed() + card.red);
model.setYellow(model.getYellow() + card.yellow);
return Panache.withTransaction(() -> cardboardRepository.persist(model));
}
CardboardModel cardboardModel = new CardboardModel();
cardboardModel.setCompet(matchModel.getCategory().getCompet());
cardboardModel.setMatch(matchModel);
cardboardModel.setRed(card.red);
cardboardModel.setYellow(card.yellow);
cardboardModel.setComb(null);
cardboardModel.setGuestComb(null);
if (card.combId >= 0) {
if (matchModel.getC1_id() != null && matchModel.getC1_id().getId() == card.combId)
cardboardModel.setComb(matchModel.getC1_id());
if (matchModel.getC2_id() != null && matchModel.getC2_id().getId() == card.combId)
cardboardModel.setComb(matchModel.getC2_id());
} else {
if (matchModel.getC1_guest() != null && matchModel.getC1_guest()
.getId() == card.combId * -1)
cardboardModel.setGuestComb(matchModel.getC1_guest());
if (matchModel.getC2_guest() != null && matchModel.getC2_guest()
.getId() == card.combId * -1)
cardboardModel.setGuestComb(matchModel.getC2_guest());
}
if (cardboardModel.getComb() == null && cardboardModel.getGuestComb() == null)
return Uni.createFrom().nullItem();
return Panache.withTransaction(() -> cardboardRepository.persist(cardboardModel));
}))
.call(model -> SSCardboard.sendCardboard(connection, CardboardEntity.fromModel(model)))
.replaceWithVoid();
}
@WSReceiver(code = "getCardboardWithoutThis", permission = PermLevel.VIEW)
public Uni<CardboardAllMatch> getCardboardWithoutThis(WebSocketConnection connection, Long matchId) {
return getById(matchId, connection)
.chain(matchModel -> cardboardRepository.list("compet = ?1 AND match != ?2", matchModel.getCategory().getCompet(), matchModel)
.map(models -> {
CardboardAllMatch out = new CardboardAllMatch();
models.stream().filter(c -> (matchModel.getC1_id() != null
&& Objects.equals(c.getComb(), matchModel.getC1_id()))
|| (matchModel.getC1_guest() != null
&& Objects.equals(c.getGuestComb(), matchModel.getC1_guest())))
.forEach(c -> {
out.c1_yellow += c.getYellow();
out.c1_red += c.getRed();
});
models.stream().filter(c -> (matchModel.getC2_id() != null
&& Objects.equals(c.getComb(), matchModel.getC2_id()))
|| (matchModel.getC2_guest() != null
&& Objects.equals(c.getGuestComb(), matchModel.getC2_guest())))
.forEach(c -> {
out.c2_yellow += c.getYellow();
out.c2_red += c.getRed();
});
return out;
}));
}
@RegisterForReflection
public record SendCardboard(long matchId, long combId, int yellow, int red) {
}
@Data
@RegisterForReflection
public static class CardboardAllMatch {
int c1_yellow = 0;
int c1_red = 0;
int c2_yellow = 0;
int c2_red = 0;
}
}

View File

@@ -1,13 +0,0 @@
package fr.titionfire.ffsaf.ws.send;
import fr.titionfire.ffsaf.domain.entity.CardboardEntity;
import fr.titionfire.ffsaf.ws.CompetitionWS;
import io.quarkus.websockets.next.WebSocketConnection;
import io.smallrye.mutiny.Uni;
public class SSCardboard {
public static Uni<Void> sendCardboard(WebSocketConnection connection, CardboardEntity cardboardEntity) {
return CompetitionWS.sendNotifyToOtherEditor(connection, "sendCardboard", cardboardEntity);
}
}

View File

@@ -43,9 +43,9 @@ function AffiliationMenu() {
}
function CompMenu() {
const {is_authenticated, userinfo} = useAuth()
const {is_authenticated} = useAuth()
if (!is_authenticated || !userinfo?.roles?.includes("federation_admin"))
if (!is_authenticated)
return <></>
return <li className="nav-item dropdown">

View File

@@ -106,7 +106,6 @@ export function SmartLogoBackground({
<img className={imgClassName} src={src} alt={alt} style={{...style, filter: `drop-shadow(0 0 1rem ${background})`}}
onError={e => {
e.target.style.opacity = "0"
setLoad(false)
}}
onLoad={e => {
e.target.style.opacity = "1"

View File

@@ -1,6 +1,6 @@
import {createContext, useContext, useReducer} from "react";
const PubAffContext = createContext({next: [], c1: undefined, c2: undefined, showScore: true, timeCb: undefined, scoreRouge: 0, scoreBleu: 0});
const PubAffContext = createContext({next: [], c1: undefined, c2: undefined, showScore: true});
const PubAffDispatchContext = createContext(() => {
});
@@ -8,12 +8,6 @@ function reducer(state, action) {
switch (action.type) {
case 'SET_DATA':
return {...state, ...action.payload}
case 'CALL_TIME':
if (state.timeCb)
state.timeCb(action.payload)
return state
case 'CLEAR_CB_TIME':
return {...state, timeCb: undefined}
default:
return state
}

View File

@@ -36,10 +36,6 @@ export function CompetitionEdit() {
})
}
useEffect(() => {
refresh(`/competition/${id}?light=false`)
}, [id]);
return <>
<button type="button" className="btn btn-link" onClick={() => navigate("/competition")}>
&laquo; retour
@@ -288,18 +284,17 @@ function Content({data}) {
toast.promise(
apiAxios.post(`/competition`, out),
{
pending: "Enregistrement de la competition en cours",
success: "Competition enregistrée avec succès 🎉",
pending: "Enregistrement du club en cours",
success: "Club enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement de la competition")
return errFormater(data, "Échec de l'enregistrement du club")
}
},
}
).then(data => {
console.log(data.data)
if (data.data.id !== undefined)
navigate("/competition/" + data.data.id)
if (data.id !== undefined)
navigate("/competition/" + data.id)
})
}
@@ -320,7 +315,7 @@ function Content({data}) {
<div id="collapseOne" className="accordion-collapse collapse" data-bs-parent="#accordionExample">
<div className="accordion-body">
<TextField name="uuid" text="UUID" value={data.uuid} disabled={true}/>
<OptionField name="system" text="System" value={data.system} values={{SAFCA: 'SAFCA', INTERNAL: "Intranet"}}
<OptionField name="system" text="System" value={data.system} values={{SAFCA: 'SAFCA', NONE: "intranet"}}
disabled={data.id !== null}/>
{data.id !== null &&
<div className="row">
@@ -388,11 +383,11 @@ function Content({data}) {
<span className="input-group-text" id="startRegister">Du</span>
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="date"
name="startRegister" aria-describedby="startRegister"
defaultValue={data.startRegister ? data.startRegister.substring(0, 16) : ''}/>
defaultValue={data.startRegister ? data.startRegister.split('+')[0] : ''}/>
<span className="input-group-text" id="endRegister">Au</span>
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="endRegister"
name="endRegister" aria-describedby="endRegister"
defaultValue={data.endRegister ? data.endRegister.substring(0, 16) : ''}/>
defaultValue={data.endRegister ? data.endRegister.split('+')[0] : ''}/>
</div>
<div style={{display: registerMode === "HELLOASSO" ? "initial" : "none"}}>

View File

@@ -1,183 +0,0 @@
import React, {useEffect, useRef, useState} from "react";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {timePrint} from "../../../utils/Tools.js";
export function ChronoPanel() {
const [config, setConfig] = useState({
time: Number(sessionStorage.getItem("chronoTime") || "90999"),
pause: Number(sessionStorage.getItem("chronoPause") || "60999")
})
const [chrono, setChrono] = useState({time: 0, startTime: 0})
const chronoText = useRef(null)
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "black", lastTimeStr: "00:00"})
const publicAffDispatch = usePubAffDispatch();
const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time}))
const isRunning = () => chrono.startTime !== 0
const getTime = () => {
if (chrono.startTime === 0)
return chrono.time
return chrono.time + Date.now() - chrono.startTime
}
useEffect(() => {
publicAffDispatch({type: 'CALL_TIME', payload: {timeStr: state.current.lastTimeStr, timeColor: state.current.color}})
}, [])
useEffect(() => {
const blinkRfDuration = 20
const state_ = state.current
const text_ = chronoText.current
publicAffDispatch({type: 'CALL_TIME', payload: {timeStr: state.current.lastTimeStr, timeColor: state.current.color}})
const timer = setInterval(() => {
let currentDuration = config.time
let color = "black"
if (state_.chronoState === 1) {
color = (state_.countBlink < blinkRfDuration) ? "black" : "red"
} else if (state_.chronoState === 2) {
currentDuration = (state_.chronoState === 0) ? 10000 : config.pause
color = (state_.countBlink < blinkRfDuration) ? "green" : "red"
}
const timeStr = timePrint(currentDuration - getTime())
if (timeStr !== state_.lastTimeStr || color !== state_.lastColor)
publicAffDispatch({type: 'CALL_TIME', payload: {timeStr: timeStr, timeColor: color}})
if (timeStr !== state_.lastTimeStr) {
text_.textContent = timePrint(currentDuration - getTime())
state_.lastTimeStr = timeStr
}
if (color !== state_.lastColor) {
text_.style.color = color
state_.lastColor = color
}
if (state_.chronoState === 0 && isRunning()) {
state_.chronoState = 1
} else if (state_.chronoState === 1 && getTime() >= config.time) {
setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
state_.chronoState = 2
} else if (state_.chronoState === 2 && getTime() >= config.pause) {
setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
state_.chronoState = 1
}
if (isRunning()) {
state_.countBlink = 19
} else {
state_.countBlink++
if (state_.countBlink > 40)
state_.countBlink = 0
}
if (state_.chronoState === 0) {
clearInterval(timer)
}
}, 50);
return () => clearInterval(timer)
}, [chrono, config])
const handleSubmit = (e) => {
e.preventDefault();
const form = e.target;
const timeStr = form[0].value;
const pauseStr = form[1].value;
const parseTime = (str) => {
const parts = str.split(":").map(part => parseInt(part, 10));
if (parts.length === 1) {
return parts[0] * 1000;
} else if (parts.length === 2) {
return (parts[0] * 60 + parts[1]) * 1000;
} else {
return 0;
}
}
const newTime = parseTime(timeStr) + 999;
const newPause = parseTime(pauseStr) + 999;
sessionStorage.setItem("chronoPause", newPause);
sessionStorage.setItem("chronoTime", newTime);
setConfig({time: newTime, pause: newPause});
}
return <div>
<div className="row">
<button className="btn btn-primary col-6 col-sm-8 col-md-9"
onClick={__ => isRunning() ?
setChrono(prev => ({...prev, time: prev.time + Date.now() - prev.startTime, startTime: 0})) :
setChrono(prev => ({...prev, startTime: Date.now()}))}>
{isRunning() ? "Arrêter" : "Démarrer"}</button>
<button className="btn btn-danger col" onClick={__ => {
setChrono(prev => ({...prev, time: 0, startTime: 0}))
state.current.chronoState = 0
}}>Réinitialiser
</button>
</div>
<div className="row" style={{marginTop: "0.5em"}}>
<div className="col-12 col-sm-8 col-md-9">
<h1 ref={chronoText}
style={{fontSize: "min(19vw, 7.5em)", textAlign: "center", color: state.current.lastColor}}>{state.current.lastTimeStr}</h1>
</div>
<div className="col" style={{margin: "auto 0"}}>
<div className="row">
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-10000)}>-10 s</button>
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(10000)}>+10 s</button>
</div>
<div className="row" style={{marginTop: "0.5em"}}>
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(-1000)}>-1 s</button>
<button className="btn btn-outline-secondary col-6" onClick={__ => addTime(1000)}>+1 s</button>
</div>
<div className="row" style={{marginTop: "0.5em"}}>
<button className="btn btn-outline-secondary col-12" onClick={__ => {
const timeStr = prompt("Entrez le temps en s", "0");
if (timeStr === null)
return;
addTime(parseInt(timeStr, 10) * 1000);
}}>+/- ... s
</button>
</div>
</div>
</div>
<div className="row" style={{marginTop: "0.5em"}}>
<div className="col-12 col-sm-8" style={{margin: 'auto 0'}}>
<div>Temps: {timePrint(config.time)}, pause: {timePrint(config.pause)}</div>
</div>
<button className="btn btn-secondary col" data-bs-toggle="modal" data-bs-target="#timeModal">Définir le temps
</button>
</div>
<div className="modal fade" id="timeModal" tabIndex="-1" aria-labelledby="timeModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Edition temps</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body">
<div className="input-group mb-3">
<span className="input-group-text">Durée round</span>
<input type="text" className="form-control" placeholder="0" aria-label="Min" defaultValue={timePrint(config.time)}/>
<span className="input-group-text">(mm:ss)</span>
</div>
<div className="input-group mb-3">
<span className="input-group-text">Durée pause</span>
<input type="text" className="form-control" placeholder="0" aria-label="Min" defaultValue={timePrint(config.pause)}/>
<span className="input-group-text">(mm:ss)</span>
</div>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal">Valider</button>
</div>
</form>
</div>
</div>
</div>
</div>
}

View File

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

View File

@@ -1,626 +0,0 @@
import React, {useEffect, useRef, useState, useReducer} from "react";
import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
import {DrawGraph} from "../../result/DrawGraph.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
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"
style={{width: "16px"}} src="/img/171891.png"
alt=""/>
}
export function CategorieSelect({catId, setCatId, menuActions}) {
const setLoading = useLoadingSwitcher()
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS();
useEffect(() => {
const categoryListener = ({data}) => {
setCats([...cats.filter(c => c.id !== data.id), data])
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
return () => dispatch({type: 'removeListener', payload: categoryListener})
}, [cats]);
const cat = cats?.find(c => c.id === catId);
return <>
<div className="input-group">
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>
<select className="form-select" onChange={e => setCatId(Number(e.target.value))} value={catId}>
{cats && <option value={-1}></option>}
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
<option key={c.id} value={c.id}>{c.name}</option>))}
</select>
</div>
{catId !== -1 && <CMTMatchPanel catId={catId} cat={cat} menuActions={menuActions}/>}
</>
}
function CMTMatchPanel({catId, cat, menuActions}) {
const setLoading = useLoadingSwitcher()
const {sendRequest, dispatch} = useWS();
const [trees, setTrees] = useState([]);
const [matches, reducer] = useReducer(MarchReducer, []);
const combDispatch = useCombsDispatch();
function readAndConvertMatch(matches, data, combsToAdd) {
matches.push({...data, c1: data.c1?.id, c2: data.c2?.id})
if (data.c1)
combsToAdd.push(data.c1)
if (data.c2)
combsToAdd.push(data.c2)
}
useEffect(() => {
if (!catId)
return;
setLoading(1);
sendRequest('getFullCategory', catId)
.then((data) => {
setTrees(data.trees.map(d => from_sendTree(d, true)))
let matches2 = [];
let combsToAdd = [];
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
reducer({type: 'REPLACE_ALL', payload: matches2});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
}).finally(() => setLoading(0))
const treeListener = ({data}) => {
if (data.length < 1 || data[0].categorie !== catId)
return
setTrees(data.map(d => from_sendTree(d, true)))
let matches2 = [];
let combsToAdd = [];
data.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
reducer({type: 'REPLACE_TREE', payload: matches2});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
}
const matchListener = ({data: datas}) => {
for (const data of datas) {
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}})
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}})
}
}
const matchOrder = ({data}) => {
reducer({type: 'REORDER', payload: data})
}
const deleteMatch = ({data: datas}) => {
for (const data of datas)
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]);
return <ListMatch cat={cat} matches={matches} trees={trees} menuActions={menuActions}/>
}
function ListMatch({cat, matches, trees, menuActions}) {
const [type, setType] = useState(1);
useEffect(() => {
if ((cat.type & type) === 0)
setType(cat.type);
}, [cat]);
return <div style={{marginTop: "1em"}}>
{cat && cat.type === 3 && <>
<ul className="nav nav-tabs">
<li className="nav-item">
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
onClick={_ => setType(1)}>Poule
</div>
</li>
<li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
onClick={_ => setType(2)}>Tournois
</div>
</li>
</ul>
</>
}
{type === 1 && <>
<MatchList matches={matches} cat={cat} menuActions={menuActions}/>
</>}
{type === 2 && <>
<BuildTree treeData={trees} matches={matches} menuActions={menuActions}/>
</>}
</div>
}
function MatchList({matches, cat, menuActions}) {
const [activeMatch, setActiveMatch] = useState(null)
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "A")
const publicAffDispatch = usePubAffDispatch();
const liceName = (cat.liceName || "N/A").split(";");
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(() => {
if (!match) {
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}});
} else {
publicAffDispatch({
type: 'SET_DATA',
payload: {
c1: match.c1,
c2: match.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
}))
}
});
}
}, [match]);
//useEffect(() => {
// if (activeMatch !== null)
// setActiveMatch(null);
//}, [cat])
useEffect(() => {
if (match && match.poule !== lice)
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id)
}, [lice]);
useEffect(() => {
if (marches2.length === 0)
return;
if (marches2.some(m => m.id === activeMatch))
return;
setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id);
}, [matches])
return <>
{liceName.length > 1 &&
<div className="input-group" style={{maxWidth: "10em", marginTop: "0.5em"}}>
<label className="input-group-text" htmlFor="selectLice">Lice</label>
<select className="form-select" id="selectLice" value={lice} onChange={e => {
setLice(e.target.value);
localStorage.setItem("cm_lice", e.target.value);
}}>
{liceName.map((l, index) => (
<option key={index} value={l}>{l}</option>
))}
</select>
</div>
}
<div className="table-responsive-xxl">
<table className="table table-striped table-hover">
<thead>
<tr>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">L</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th>
<th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">Rouge</th>
<th style={{textAlign: "center"}} scope="col">Blue</th>
<th style={{textAlign: "center"}} scope="col"></th>
</tr>
</thead>
<tbody className="table-group-divider">
{marches2.map((m, index) => (
<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>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{m.poule}</td>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{index >= firstIndex ? index + 1 - firstIndex : ""}</th>
<td style={{textAlign: "right", paddingRight: "0"}}>{m.end && m.win > 0 && <CupImg/>}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}>
<small><CombName combId={m.c1}/></small></td>
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}>
<small><CombName combId={m.c2}/></small></td>
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td>
</tr>
))}
</tbody>
</table>
</div>
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} match={match} menuActions={menuActions}/></LoadingProvider>}
</>
}
function BuildTree({treeData, matches, menuActions}) {
const scrollRef = useRef(null)
const [currentMatch, setCurrentMatch] = useState(null)
const {getComb} = useCombs()
const publicAffDispatch = usePubAffDispatch();
const match = matches.find(m => m.id === currentMatch?.matchSelect)
useEffect(() => {
if (!match) {
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined}});
} else {
publicAffDispatch({type: 'SET_DATA', payload: {c1: match.c1, c2: match.c2}});
}
}, [match]);
const next_match = matches.find(m => m.id === currentMatch?.matchNext)
useEffect(() => {
if (!next_match) {
publicAffDispatch({type: 'SET_DATA', payload: {next: []}});
} else {
publicAffDispatch({type: 'SET_DATA', payload: {next: [{c1: next_match.c1, c2: next_match.c2}]}});
}
}, [next_match]);
function parseTree(data_in) {
if (data_in?.data == null)
return null
const matchData = matches.find(m => m.id === data_in.data)
const c1 = getComb(matchData?.c1)
const c2 = getComb(matchData?.c2)
let node = new TreeNode({
...matchData,
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
})
node.left = parseTree(data_in?.left)
node.right = parseTree(data_in?.right)
return node
}
function initTree(data_in) {
let out = []
for (const din of data_in) {
out.push(parseTree(din))
}
return out
}
const trees = initTree(treeData);
const onMatchClick = (rect, matchId, __) => {
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())});
}
const onClickVoid = () => {
}
return <div>
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23}/>
</div>
{currentMatch?.matchSelect &&
<LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} match={match} menuActions={menuActions}/></LoadingProvider>}
</div>
}
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()
const [end, setEnd] = useState(match?.end || false)
const [scoreIn, setScoreIn] = useState("")
const inputRef = useRef(null)
const tableRef = useRef(null)
const scoreRef = useRef([])
const lastScoreClick = useRef(null)
useEffect(() => {
menuActions.current.saveScore = (scoreRed, scoreBlue) => {
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue};
toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}),
{
pending: 'Sauvegarde du score...',
success: 'Score sauvegardé !',
error: 'Erreur lors de la sauvegarde du score'
}
);
}
return () => menuActions.current.saveScore = undefined;
}, [matchId])
const handleScoreClick = (e, round, comb) => {
e.stopPropagation();
const tableRect = tableRef.current.getBoundingClientRect();
const rect = e.currentTarget.getBoundingClientRect();
updateScore();
const sel = inputRef.current;
sel.style.top = (rect.y - tableRect.y) + "px";
sel.style.left = (rect.x - tableRect.x) + "px";
sel.style.width = rect.width + "px";
sel.style.height = rect.height + "px";
sel.style.display = "block";
if (round === -1) {
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
setScoreIn("");
console.log("Setting for new round", maxRound);
lastScoreClick.current = {matchId: matchId, round: maxRound, comb};
} else {
const score = match.scores.find(s => s.n_round === round);
setScoreIn((comb === 1 ? score?.s1 : score?.s2) || "");
lastScoreClick.current = {matchId: matchId, round, comb};
setTimeout(() => inputRef.current.select(), 100);
}
}
const updateScore = () => {
if (lastScoreClick.current !== null) {
const {matchId, round, comb} = lastScoreClick.current;
lastScoreClick.current = null;
const scoreIn_ = String(scoreIn).trim() === "" ? -1000 : Number(scoreIn);
const score = match.scores.find(s => s.n_round === round);
let newScore;
if (score) {
if (comb === 1)
newScore = {...score, s1: scoreIn_};
else
newScore = {...score, s2: scoreIn_};
if (newScore.s1 === score?.s1 && newScore.s2 === score?.s2)
return
} else {
newScore = {n_round: round, s1: (comb === 1 ? scoreIn_ : -1000), s2: (comb === 2 ? scoreIn_ : -1000)};
if (newScore.s1 === -1000 && newScore.s2 === -1000)
return
}
console.log("Updating score", matchId, newScore);
setLoading(1)
sendRequest('updateMatchScore', {matchId: matchId, ...newScore})
.finally(() => {
setLoading(0)
})
}
}
const onClickVoid = () => {
updateScore();
const sel = inputRef.current;
sel.style.display = "none";
lastScoreClick.current = null;
}
onClickVoid_.current = onClickVoid;
useEffect(() => {
if (!match || match?.end === end)
return;
if (end) {
if (win(match?.scores) === 0 && match.categorie_ord === -42) {
toast.error("Impossible de terminer un match nul en tournois.");
setEnd(false);
return;
}
}
setLoading(1)
sendRequest('updateMatchEnd', {matchId: matchId, end})
.finally(() => {
setLoading(0)
})
}, [end]);
useEffect(() => {
onClickVoid()
}, [matchId]);
useEffect(() => {
if (match?.scores)
scoreRef.current = scoreRef.current.slice(0, match.scores.length);
}, [match?.scores]);
useEffect(() => {
if (!match)
return;
setEnd(match.end);
}, [match]);
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const o = [...tooltipTriggerList]
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
const tt = "Score speciaux : <br/>" +
"-997 : disqualifié <br/>" +
"-998 : absent <br/>" +
"-999 : forfait"
const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0;
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>
))}
<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>
</div>
<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

@@ -1,51 +0,0 @@
import {useEffect, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
export function PointPanel({menuActions}) {
const [revers, setRevers] = useState(false)
const [scoreRouge, setScoreRouge] = useState(0)
const [scoreBleu, setScoreBleu] = useState(0)
const publicAffDispatch = usePubAffDispatch()
menuActions.current.switchSore = () => {
setRevers(!revers)
}
const handleSave = () => {
menuActions.current.saveScore?.(scoreRouge, scoreBleu)
handleReset();
}
const handleReset = () => {
setScoreRouge(0);
setScoreBleu(0);
}
useEffect(() => {
publicAffDispatch({type: 'SET_DATA', payload: {scoreRouge: scoreRouge, scoreBleu: scoreBleu}})
}, [scoreRouge, scoreBleu])
const red =
<div className="col-5 row align-items-center" style={{padding: "0 1em"}}>
<button className="btn btn-secondary" onClick={() => setScoreRouge(scoreRouge + 1)}><FontAwesomeIcon icon={faChevronUp}/></button>
<h1 style={{color: "red", fontSize: "min(15vw, 7em)", textAlign: "center"}}>{scoreRouge}</h1>
<button className="btn btn-secondary" onClick={() => setScoreRouge(scoreRouge - 1)}><FontAwesomeIcon icon={faChevronDown}/></button>
</div>
return <div className="row">
{!revers && red}
<div className="col-5 row align-items-center" style={{padding: "0 1em"}}>
<button className="btn btn-secondary" onClick={() => setScoreBleu(scoreBleu + 1)}><FontAwesomeIcon icon={faChevronUp}/></button>
<h1 style={{color: "blue", fontSize: "min(15vw, 7em)", textAlign: "center"}}>{scoreBleu}</h1>
<button className="btn btn-secondary" onClick={() => setScoreBleu(scoreBleu - 1)}><FontAwesomeIcon icon={faChevronDown}/></button>
</div>
{revers && red}
<div className="col row align-items-center">
<button className="btn btn-danger" onClick={handleReset}>Réinitialiser</button>
<button className="btn btn-success" onClick={handleSave}>Sauvegarder</button>
</div>
</div>
}

View File

@@ -1,21 +1,30 @@
import React, {useEffect, useRef, useState} from "react";
import {useRequestWS} from "../../../hooks/useWS.jsx";
import {useCombsDispatch} from "../../../hooks/useComb.jsx";
import React, {useEffect, useReducer, useRef, useState} from "react";
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons";
import {DrawGraph} from "../../result/DrawGraph.jsx";
import {scorePrint, win} from "../../../utils/Tools.js";
import {toast} from "react-toastify";
import {createPortal} from "react-dom";
import {copyStyles} from "../../../utils/copyStyles.js";
import {PubAffProvider, usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {faArrowRightArrowLeft, faDisplay} from "@fortawesome/free-solid-svg-icons";
import {faDisplay} from "@fortawesome/free-solid-svg-icons";
import {PubAffWindow} from "./PubAffWindow.jsx";
import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts";
import {ChronoPanel} from "./CMTChronoPanel.jsx";
import {CategorieSelect} from "./CMTMatchPanel.jsx";
import {PointPanel} from "./CMTPoint.jsx";
function CupImg() {
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
style={{width: "16px"}} src="/img/171891.png"
alt=""/>
}
export function CMTable() {
const combDispatch = useCombsDispatch()
const [catId, setCatId] = useState(-1);
const menuActions = useRef({});
const {data} = useRequestWS("getRegister", null)
useEffect(() => {
@@ -28,25 +37,18 @@ export function CMTable() {
<div className="text-center">
<div className="row">
<div className="col-md-12 col-lg">
<div className="card mb-3">
<div className="card-header">Chronomètre</div>
<div className="card-body">
<ChronoPanel/>
</div>
<div style={{backgroundColor: "#00c700"}}>
A
</div>
<div className="card mb-3">
<div className="card-header">Score</div>
<div className="card-body">
<PointPanel menuActions={menuActions}/>
</div>
<div style={{backgroundColor: "#0099c7"}}>
B
</div>
</div>
<div className="col-md-12 col-xl-6 col-xxl-5">
<div className="card mb-3">
<div className="card">
<div className="card-header">Matches</div>
<div className="card-body">
<CategorieSelect catId={catId} setCatId={setCatId} menuActions={menuActions}/>
<CategorieSelect catId={catId} setCatId={setCatId}/>
</div>
</div>
<div style={{backgroundColor: "#c70000"}}>
@@ -54,16 +56,14 @@ export function CMTable() {
</div>
</div>
</div>
<Menu menuActions={menuActions}/>
<Menu/>
</div>
</PubAffProvider>
}
const windowName = "FFSAFScorePublicWindow";
let tto = [];
function Menu({menuActions}) {
function Menu() {
const e = document.getElementById("actionMenu")
const publicAffDispatch = usePubAffDispatch()
const [showPubAff, setShowPubAff] = useState(false)
@@ -94,7 +94,6 @@ function Menu({menuActions}) {
setShowPubAff(false);
externalWindow.current.close();
externalWindow.current = null;
publicAffDispatch({type: 'CLEAR_CB_TIME', payload: null});
sessionStorage.removeItem(windowName + "_open");
});
setShowPubAff(true);
@@ -104,38 +103,511 @@ 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}});
}
const handleSwitchScore = () => {
menuActions.current.switchSore?.();
}
if (!e)
return <></>;
return <>
{createPortal(
<>
<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"/>
<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"}}
onClick={handlePubAff}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Ouvrir l'affichage public"/>
onClick={handlePubAff}/>
<FontAwesomeIcon icon={SimpleIconsScore} size="xl" style={{color: showScore ? "#00c700" : "#6c757d", cursor: "pointer"}}
onClick={handleScore}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title="Afficher les scores sur l'affichage public"/>
onClick={handleScore}/>
</>, document.getElementById("actionMenu"))}
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
</>
}
function CategorieSelect({catId, setCatId}) {
const setLoading = useLoadingSwitcher()
const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading);
const {dispatch} = useWS();
useEffect(() => {
const categoryListener = ({data}) => {
setCats([...cats.filter(c => c.id !== data.id), data])
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
return () => dispatch({type: 'removeListener', payload: categoryListener})
}, [cats]);
const cat = cats?.find(c => c.id === catId);
return <>
<div className="input-group">
<h6 style={{margin: "auto 0.5em auto 0"}}>Catégorie</h6>
<select className="form-select" onChange={e => setCatId(Number(e.target.value))} value={catId}>
{cats && <option value={-1}></option>}
{cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => (
<option key={c.id} value={c.id}>{c.name}</option>))}
</select>
</div>
{catId !== -1 && <MatchPanel catId={catId} cat={cat}/>}
</>
}
function MatchPanel({catId, cat}) {
const setLoading = useLoadingSwitcher()
const {sendRequest, dispatch} = useWS();
const [trees, setTrees] = useState([]);
const [matches, reducer] = useReducer(MarchReducer, []);
const combDispatch = useCombsDispatch();
function readAndConvertMatch(matches, data, combsToAdd) {
matches.push({...data, c1: data.c1?.id, c2: data.c2?.id})
if (data.c1)
combsToAdd.push(data.c1)
if (data.c2)
combsToAdd.push(data.c2)
}
useEffect(() => {
if (!catId)
return;
setLoading(1);
sendRequest('getFullCategory', catId)
.then((data) => {
setTrees(data.trees.map(d => from_sendTree(d, true)))
let matches2 = [];
let combsToAdd = [];
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
reducer({type: 'REPLACE_ALL', payload: matches2});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
}).finally(() => setLoading(0))
const treeListener = ({data}) => {
if (data.length < 1 || data[0].categorie !== catId)
return
setTrees(data.map(d => from_sendTree(d, true)))
let matches2 = [];
let combsToAdd = [];
data.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
reducer({type: 'REPLACE_TREE', payload: matches2});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
}
const matchListener = ({data: datas}) => {
for (const data of datas) {
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}})
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}})
}
}
const matchOrder = ({data}) => {
reducer({type: 'REORDER', payload: data})
}
const deleteMatch = ({data: datas}) => {
for (const data of datas)
reducer({type: 'REMOVE', 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'}})
return () => {
dispatch({type: 'removeListener', payload: treeListener})
dispatch({type: 'removeListener', payload: matchListener})
dispatch({type: 'removeListener', payload: matchOrder})
dispatch({type: 'removeListener', payload: deleteMatch})
}
}, [catId]);
return <ListMatch cat={cat} matches={matches} trees={trees}/>
}
function ListMatch({cat, matches, trees}) {
const [type, setType] = useState(1);
useEffect(() => {
if ((cat.type & type) === 0)
setType(cat.type);
}, [cat]);
return <div style={{marginTop: "1em"}}>
{cat && cat.type === 3 && <>
<ul className="nav nav-tabs">
<li className="nav-item">
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
onClick={_ => setType(1)}>Poule
</div>
</li>
<li className="nav-item">
<div className={"nav-link" + (type === 2 ? " active" : "")} aria-current={(type === 2 ? " page" : "false")}
onClick={_ => setType(2)}>Tournois
</div>
</li>
</ul>
</>
}
{type === 1 && <>
<MatchList matches={matches} cat={cat}/>
</>}
{type === 2 && <>
<BuildTree treeData={trees} matches={matches}/>
</>}
</div>
}
function MatchList({matches, cat}) {
const [activeMatch, setActiveMatch] = useState(null)
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "A")
const publicAffDispatch = usePubAffDispatch();
const liceName = (cat.liceName || "N/A").split(";");
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 match = matches.find(m => m.id === activeMatch)
useEffect(() => {
if (!match) {
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}});
} else {
publicAffDispatch({
type: 'SET_DATA',
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}))}
});
}
}, [match]);
//useEffect(() => {
// if (activeMatch !== null)
// setActiveMatch(null);
//}, [cat])
useEffect(() => {
if (match && match.poule !== lice)
setActiveMatch(marches2.find(m => !m.end && m.poule === lice)?.id)
}, [lice]);
useEffect(() => {
if (marches2.length === 0)
return;
if (marches2.some(m => m.id === activeMatch))
return;
setActiveMatch(marches2.find(m => !m.end && m.poule === 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"}}>
<label className="input-group-text" htmlFor="selectLice">Lice</label>
<select className="form-select" id="selectLice" value={lice} onChange={e => {
setLice(e.target.value);
localStorage.setItem("cm_lice", e.target.value);
}}>
{liceName.map((l, index) => (
<option key={index} value={l}>{l}</option>
))}
</select>
</div>
}
<div className="table-responsive-xxl">
<table className="table table-striped table-hover">
<thead>
<tr>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">L</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">P</th>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}} scope="col">N°</th>
<th style={{textAlign: "center"}} scope="col"></th>
<th style={{textAlign: "center"}} scope="col">Rouge</th>
<th style={{textAlign: "center"}} scope="col">Blue</th>
<th style={{textAlign: "center"}} scope="col"></th>
</tr>
</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")} onClick={() => setActiveMatch(m.id)}>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{liceName[(index - firstIndex) % liceName.length]}</td>
<td style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>{m.poule}</td>
<th style={{textAlign: "center", paddingLeft: "0.2em", paddingRight: "0.2em"}}>
{index >= firstIndex ? index + 1 - firstIndex : ""}</th>
<td style={{textAlign: "right", paddingRight: "0"}}>{m.end && m.win > 0 && <CupImg/>}</td>
<td style={{textAlign: "center", minWidth: "11em", paddingLeft: "0.2em"}}>
<small><CombName combId={m.c1}/></small></td>
<td style={{textAlign: "center", minWidth: "11em", paddingRight: "0.2em"}}>
<small><CombName combId={m.c2}/></small></td>
<td style={{textAlign: "left", paddingLeft: "0"}}>{m.end && m.win < 0 && <CupImg/>}</td>
</tr>
))}
</tbody>
</table>
</div>
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} match={match}/></LoadingProvider>}
</>
}
function BuildTree({treeData, matches}) {
const scrollRef = useRef(null)
const [currentMatch, setCurrentMatch] = useState(null)
const {getComb} = useCombs()
const publicAffDispatch = usePubAffDispatch();
const match = matches.find(m => m.id === currentMatch?.matchSelect)
useEffect(() => {
if (!match) {
publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined}});
} else {
publicAffDispatch({type: 'SET_DATA', payload: {c1: match.c1, c2: match.c2}});
}
}, [match]);
const next_match = matches.find(m => m.id === currentMatch?.matchNext)
useEffect(() => {
if (!next_match) {
publicAffDispatch({type: 'SET_DATA', payload: {next: []}});
} else {
publicAffDispatch({type: 'SET_DATA', payload: {next: [{c1: next_match.c1, c2: next_match.c2}]}});
}
}, [next_match]);
function parseTree(data_in) {
if (data_in?.data == null)
return null
const matchData = matches.find(m => m.id === data_in.data)
const c1 = getComb(matchData?.c1)
const c2 = getComb(matchData?.c2)
let node = new TreeNode({
...matchData,
c1FullName: c1 !== null ? c1.fname + " " + c1.lname : null,
c2FullName: c2 !== null ? c2.fname + " " + c2.lname : null
})
node.left = parseTree(data_in?.left)
node.right = parseTree(data_in?.right)
return node
}
function initTree(data_in) {
let out = []
for (const din of data_in) {
out.push(parseTree(din))
}
return out
}
const trees = initTree(treeData);
const onMatchClick = (rect, matchId, __) => {
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())});
}
const onClickVoid = () => {
}
return <div>
<div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
<DrawGraph root={trees} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}
matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23}/>
</div>
{currentMatch?.matchSelect && <LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} match={match}/></LoadingProvider>}
</div>
}
function ScorePanel({matchId, match}) {
const {sendRequest} = useWS()
const setLoading = useLoadingSwitcher()
const [end, setEnd] = useState(match?.end || false)
const [scoreIn, setScoreIn] = useState("")
const inputRef = useRef(null)
const tableRef = useRef(null)
const scoreRef = useRef([])
const lastScoreClick = useRef(null)
const handleScoreClick = (e, round, comb) => {
e.stopPropagation();
const tableRect = tableRef.current.getBoundingClientRect();
const rect = e.currentTarget.getBoundingClientRect();
updateScore();
const sel = inputRef.current;
sel.style.top = (rect.y - tableRect.y) + "px";
sel.style.left = (rect.x - tableRect.x) + "px";
sel.style.width = rect.width + "px";
sel.style.height = rect.height + "px";
sel.style.display = "block";
if (round === -1) {
const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0;
setScoreIn("");
console.log("Setting for new round", maxRound);
lastScoreClick.current = {matchId: matchId, round: maxRound, comb};
} else {
const score = match.scores.find(s => s.n_round === round);
setScoreIn((comb === 1 ? score?.s1 : score?.s2) || "");
lastScoreClick.current = {matchId: matchId, round, comb};
setTimeout(() => inputRef.current.select(), 100);
}
}
const updateScore = () => {
if (lastScoreClick.current !== null) {
const {matchId, round, comb} = lastScoreClick.current;
lastScoreClick.current = null;
const scoreIn_ = String(scoreIn).trim() === "" ? -1000 : Number(scoreIn);
const score = match.scores.find(s => s.n_round === round);
let newScore;
if (score) {
if (comb === 1)
newScore = {...score, s1: scoreIn_};
else
newScore = {...score, s2: scoreIn_};
if (newScore.s1 === score?.s1 && newScore.s2 === score?.s2)
return
} else {
newScore = {n_round: round, s1: (comb === 1 ? scoreIn_ : -1000), s2: (comb === 2 ? scoreIn_ : -1000)};
if (newScore.s1 === -1000 && newScore.s2 === -1000)
return
}
console.log("Updating score", matchId, newScore);
setLoading(1)
sendRequest('updateMatchScore', {matchId: matchId, ...newScore})
.finally(() => {
setLoading(0)
})
}
}
const onClickVoid = () => {
updateScore();
const sel = inputRef.current;
sel.style.display = "none";
lastScoreClick.current = null;
}
useEffect(() => {
if (!match || match?.end === end)
return;
if (end) {
if (win(match?.scores) === 0 && match.categorie_ord === -42) {
toast.error("Impossible de terminer un match nul en tournois.");
setEnd(false);
return;
}
}
setLoading(1)
sendRequest('updateMatchEnd', {matchId: matchId, end})
.finally(() => {
setLoading(0)
})
}, [end]);
useEffect(() => {
onClickVoid()
}, [matchId]);
useEffect(() => {
if (match?.scores)
scoreRef.current = scoreRef.current.slice(0, match.scores.length);
}, [match?.scores]);
useEffect(() => {
if (!match)
return;
setEnd(match.end);
}, [match]);
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const o = [...tooltipTriggerList]
o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
const tt = "Score speciaux : <br/>" +
"-997 : disqualifié <br/>" +
"-998 : absent <br/>" +
"-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>
</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>
</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>
</div>
}

View File

@@ -1,14 +1,11 @@
import {Route, Routes, useNavigate, useParams} from "react-router-dom";
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
import {LoadingProvider} from "../../../hooks/useLoading.jsx";
import {useEffect, useState} from "react";
import {useWS, WSProvider} from "../../../hooks/useWS.jsx";
import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
import {CMAdmin} from "./CMAdmin.jsx";
import {CombsProvider} from "../../../hooks/useComb.jsx";
import {CMTable} from "./CMTable.jsx";
import {ThreeDots} from "react-loader-spinner";
import {AxiosError} from "../../../components/AxiosError.jsx";
import {useFetch} from "../../../hooks/useFetch.js";
const vite_url = import.meta.env.VITE_URL;
@@ -25,33 +22,13 @@ export default function CompetitionManagerRoot() {
}
function Home() {
const navigate = useNavigate();
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/competition/admin/all/INTERNAL/table`, setLoading, 1)
return <div className="row">
{data
? <MakeCentralPanel data={data} navigate={navigate}/>
: error
? <AxiosError error={error}/>
: <Def/>
}
const nav = useNavigate();
return <div>
<h2>Home</h2>
<button onClick={() => nav("d3dc76a6-2058-423a-b34b-6d15d7ae5848")}>Go comp</button>
</div>
}
function MakeCentralPanel({data, navigate}) {
return <>
<div className="mb-4">
<h4>Compétition:</h4>
<div className="list-group">
{data.sort((a, b) => new Date(b.date.split('T')[0]) - new Date(a.date.split('T')[0])).map((o) => (
<li className="list-group-item list-group-item-action" key={o.id}
onClick={__ => navigate(o.uuid)}>{o.name}</li>))}
</div>
</div>
</>
}
function HomeComp() {
let {compUuid} = useParams();
const [perm, setPerm] = useState("")
@@ -121,12 +98,11 @@ function Home2({perm}) {
</div>
}
function Def() {
return <div className="list-group">
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
function Test2() {
let {compUuid} = useParams();
const nav = useNavigate();
return <div>
<h2>Product ID: {compUuid}</h2>
<button onClick={() => nav(-1)}>Go Back</button>
</div>
}

View File

@@ -1,7 +1,7 @@
import {useCombs} from "../../../hooks/useComb.jsx";
import {usePubAffState} from "../../../hooks/useExternalWindow.jsx";
import {SmartLogoBackgroundMemo} from "../../../components/SmartLogoBackground.jsx";
import {useMemo, useRef} from 'react';
import {useMemo} from 'react';
const vite_url = import.meta.env.VITE_URL;
@@ -13,36 +13,27 @@ const text1Style = {fontSize: "min(2.25vw, 8vh)", fontWeight: "bold", marginLeft
const text2Style = {fontSize: "min(1.7vw, 7vh)", fontWeight: "bold"};
export function PubAffWindow({document}) {
const chronoText = useRef(null)
const state2 = useRef({lastColor: "white", lastTimeStr: "--:--"})
const state = usePubAffState();
document.title = "A React portal window"
document.body.className = "bg-dark text-white overflow-hidden";
state.timeCb = (payload) => {
state2.current = {lastColor: payload.timeColor === "black" ? "white" : payload.timeColor, lastTimeStr: payload.timeStr}
chronoText.current.textContent = payload.timeStr
chronoText.current.style.color = payload.timeColor === "black" ? "white" : payload.timeColor
}
const showScore = state.showScore ?? true;
return <>
<div className="row text-center"
style={{background: "linear-gradient(to bottom, #000000, #323232)", height: `calc(100vh - ${combHeight} * 2)`, ...noMP}}>
<div>
<div ref={chronoText}
style={{fontSize: "30vh", lineHeight: "30vh", color: state2.current.lastColor}}>{state2.current.lastTimeStr}</div>
<div style={{fontSize: "30vh", lineHeight: "30vh"}}>01:30</div>
{showScore &&
<div className="row" style={noMP}>
<div className="col-4" style={noMP}>
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#ff1414"}}>{state.scoreRouge}</div>
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#ff1414"}}>0</div>
</div>
<div className="col-4" style={noMP}>
</div>
<div className="col-4" style={noMP}>
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#14adff"}}>{state.scoreBleu}</div>
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#14adff"}}>0</div>
</div>
</div>}
</div>

View File

@@ -36,22 +36,6 @@ 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':

View File

@@ -130,26 +130,3 @@ export function scorePrint(s1) {
return String(s1)
}
}
export function timePrint(time, negSign = false) {
if (time === null || time === undefined)
return ""
const neg = time < 0
if (neg){
if (!negSign)
return "00:00"
time = -time
}
const ms = time % 1000
time = (time - ms) / 1000
const sec = time % 60
time = (time - sec) / 60
const min = time % 60
const hr = (time - min) / 60
return (neg ? "-" : "") +
(hr > 0 ? String(hr).padStart(2, '0') + ":" : "") +
String(min).padStart(2, '0') + ":" +
String(sec).padStart(2, '0')
}