Compare commits
5 Commits
c9c8c8536d
...
c21eda9df7
| Author | SHA1 | Date | |
|---|---|---|---|
| c21eda9df7 | |||
| 160c7d59e3 | |||
| 9689201a8c | |||
| 083d72fbfa | |||
| 1c7466d883 |
@ -16,7 +16,7 @@ import lombok.Setter;
|
||||
@RegisterForReflection
|
||||
|
||||
@Entity
|
||||
@Table(name = "cardboard")
|
||||
@Table(name = "competition_guest")
|
||||
public class CompetitionGuestModel {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@ -37,6 +37,8 @@ public class CompetitionGuestModel {
|
||||
|
||||
String country = "fr";
|
||||
|
||||
Integer weight = null;
|
||||
|
||||
public CompetitionGuestModel(String s) {
|
||||
this.fname = s.substring(0, s.indexOf(" "));
|
||||
this.lname = s.substring(s.indexOf(" ") + 1);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package fr.titionfire.ffsaf.net2.data;
|
||||
package fr.titionfire.ffsaf.domain.entity;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CardboardModel;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
@ -0,0 +1,56 @@
|
||||
package fr.titionfire.ffsaf.domain.entity;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.data.model.RegisterModel;
|
||||
import fr.titionfire.ffsaf.utils.Categorie;
|
||||
import fr.titionfire.ffsaf.utils.Genre;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class CombEntity {
|
||||
private long id;
|
||||
private String lname;
|
||||
private String fname;
|
||||
Categorie categorie;
|
||||
Long club;
|
||||
String club_str;
|
||||
Genre genre;
|
||||
String country;
|
||||
int overCategory;
|
||||
Integer weight;
|
||||
|
||||
public static CombEntity fromModel(MembreModel model) {
|
||||
if (model == null)
|
||||
return null;
|
||||
|
||||
return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
|
||||
model.getClub() == null ? null : model.getClub().getId(),
|
||||
model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(),
|
||||
0, null);
|
||||
}
|
||||
|
||||
|
||||
public static CombEntity fromModel(CompetitionGuestModel model) {
|
||||
if (model == null)
|
||||
return null;
|
||||
|
||||
return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null,
|
||||
model.getClub(), model.getGenre(), model.getCountry(), 0, model.getWeight());
|
||||
}
|
||||
|
||||
public static CombEntity fromModel(RegisterModel registerModel) {
|
||||
if (registerModel == null || registerModel.getMembre() == null)
|
||||
return null;
|
||||
MembreModel model = registerModel.getMembre();
|
||||
|
||||
return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(),
|
||||
registerModel.getClub2() == null ? null : registerModel.getClub2().getId(),
|
||||
registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(),
|
||||
model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight());
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package fr.titionfire.ffsaf.net2.data;
|
||||
package fr.titionfire.ffsaf.domain.entity;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import fr.titionfire.ffsaf.utils.ScoreEmbeddable;
|
||||
@ -1,4 +1,4 @@
|
||||
package fr.titionfire.ffsaf.net2.data;
|
||||
package fr.titionfire.ffsaf.domain.entity;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.TreeModel;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
@ -1,40 +0,0 @@
|
||||
package fr.titionfire.ffsaf.net2.data;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.CompetitionGuestModel;
|
||||
import fr.titionfire.ffsaf.data.model.MembreModel;
|
||||
import fr.titionfire.ffsaf.utils.Categorie;
|
||||
import fr.titionfire.ffsaf.utils.Genre;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RegisterForReflection
|
||||
public class CombEntity {
|
||||
private long id;
|
||||
private String lname = "";
|
||||
private String fname = "";
|
||||
Categorie categorie = null;
|
||||
Long club = null;
|
||||
String club_str = null;
|
||||
Genre genre = null;
|
||||
String country = "fr";
|
||||
|
||||
public static CombEntity fromModel(MembreModel model) {
|
||||
if (model == null)
|
||||
return null;
|
||||
|
||||
return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
|
||||
model.getClub().getId(), model.getClub().getName(), model.getGenre(), model.getCountry());
|
||||
}
|
||||
|
||||
|
||||
public static CombEntity fromModel(CompetitionGuestModel model) {
|
||||
if (model == null)
|
||||
return null;
|
||||
|
||||
return new CombEntity(model.getId() * -1, model.getLname(), model.getFname(), model.getCategorie(), null,
|
||||
model.getClub(), model.getGenre(), model.getCountry());
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import fr.titionfire.ffsaf.utils.SecurityCtx;
|
||||
import fr.titionfire.ffsaf.ws.data.WelcomeInfo;
|
||||
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;
|
||||
@ -40,6 +41,9 @@ public class CompetitionWS {
|
||||
@Inject
|
||||
RCategorie rCategorie;
|
||||
|
||||
@Inject
|
||||
RRegister rRegister;
|
||||
|
||||
@Inject
|
||||
SecurityCtx securityCtx;
|
||||
|
||||
@ -72,6 +76,7 @@ public class CompetitionWS {
|
||||
void init() {
|
||||
getWSReceiverMethods(RMatch.class, rMatch);
|
||||
getWSReceiverMethods(RCategorie.class, rCategorie);
|
||||
getWSReceiverMethods(RRegister.class, rRegister);
|
||||
}
|
||||
|
||||
@OnOpen
|
||||
|
||||
@ -7,8 +7,9 @@ import fr.titionfire.ffsaf.data.repository.CategoryRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.MatchRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.TreeRepository;
|
||||
import fr.titionfire.ffsaf.net2.data.MatchEntity;
|
||||
import fr.titionfire.ffsaf.net2.data.TreeEntity;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
|
||||
import fr.titionfire.ffsaf.domain.entity.TreeEntity;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.utils.TreeNode;
|
||||
import fr.titionfire.ffsaf.ws.CompetitionWS;
|
||||
@ -45,6 +46,16 @@ public class RCategorie {
|
||||
@Inject
|
||||
TreeRepository treeRepository;
|
||||
|
||||
private Uni<CategoryModel> getById(long id, WebSocketConnection connection) {
|
||||
return categoryRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException("Catégorie non trouver");
|
||||
if (!o.getCompet().getUuid().equals(connection.pathParam("uuid")))
|
||||
throw new DForbiddenException("Permission denied");
|
||||
}));
|
||||
}
|
||||
|
||||
@WSReceiver(code = "getAllCategory", permission = PermLevel.VIEW)
|
||||
public Uni<List<JustCategorie>> getAllCategory(WebSocketConnection connection, Object o) {
|
||||
return categoryRepository.list("compet.uuid", connection.pathParam("uuid"))
|
||||
@ -55,11 +66,7 @@ public class RCategorie {
|
||||
public Uni<FullCategory> getFullCategory(WebSocketConnection connection, Long id) {
|
||||
FullCategory fullCategory = new FullCategory();
|
||||
|
||||
return categoryRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException("Catégorie non trouver");
|
||||
}))
|
||||
return getById(id, connection)
|
||||
.invoke(cat -> {
|
||||
fullCategory.setId(cat.getId());
|
||||
fullCategory.setName(cat.getName());
|
||||
@ -97,11 +104,7 @@ public class RCategorie {
|
||||
|
||||
@WSReceiver(code = "updateCategory", permission = PermLevel.ADMIN)
|
||||
public Uni<Void> updateCategory(WebSocketConnection connection, JustCategorie categorie) {
|
||||
return categoryRepository.findById(categorie.id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException("Catégorie non trouver");
|
||||
}))
|
||||
return getById(categorie.id, connection)
|
||||
.chain(cat -> {
|
||||
cat.setName(categorie.name);
|
||||
cat.setLiceName(categorie.liceName);
|
||||
@ -175,11 +178,7 @@ public class RCategorie {
|
||||
|
||||
@WSReceiver(code = "updateTrees", permission = PermLevel.ADMIN)
|
||||
public Uni<Void> updateTrees(WebSocketConnection connection, TreeUpdate data) {
|
||||
return categoryRepository.findById(data.categoryId)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
if (o == null)
|
||||
throw new DNotFoundException("Catégorie non trouver");
|
||||
}))
|
||||
return getById(data.categoryId, connection)
|
||||
.call(cat -> treeRepository.update("level = -1, left = NULL, right = NULL WHERE category = ?1",
|
||||
cat.getId()))
|
||||
.call(cat -> {
|
||||
|
||||
@ -1,10 +1,20 @@
|
||||
package fr.titionfire.ffsaf.ws.recv;
|
||||
|
||||
import fr.titionfire.ffsaf.data.model.MatchModel;
|
||||
import fr.titionfire.ffsaf.data.repository.CombRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionGuestRepository;
|
||||
import fr.titionfire.ffsaf.data.repository.MatchRepository;
|
||||
import fr.titionfire.ffsaf.domain.entity.MatchEntity;
|
||||
import fr.titionfire.ffsaf.rest.exception.DForbiddenException;
|
||||
import fr.titionfire.ffsaf.rest.exception.DNotFoundException;
|
||||
import fr.titionfire.ffsaf.ws.CompetitionWS;
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
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 org.jboss.logging.Logger;
|
||||
@ -18,6 +28,22 @@ public class RMatch {
|
||||
@Inject
|
||||
MatchRepository matchRepository;
|
||||
|
||||
@Inject
|
||||
CombRepository combRepository;
|
||||
|
||||
@Inject
|
||||
CompetitionGuestRepository competitionGuestRepository;
|
||||
|
||||
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 = "getAllMatch")
|
||||
public Uni<?> getAllMatch(WebSocketConnection connection, Long l) {
|
||||
LOGGER.info("getAllMatch " + l);
|
||||
@ -26,4 +52,45 @@ public class RMatch {
|
||||
//return matchRepository.count();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "updateMatchComb", permission = PermLevel.ADMIN)
|
||||
public Uni<Void> updateMatchComb(WebSocketConnection connection, MatchComb match) {
|
||||
return getById(match.id(), connection)
|
||||
.call(mm -> match.c1() != null ?
|
||||
match.c1() >= 0 ?
|
||||
combRepository.findById(match.c1()).invoke(model -> {
|
||||
mm.setC1_id(model);
|
||||
mm.setC1_guest(null);
|
||||
}) :
|
||||
competitionGuestRepository.findById(match.c1() * -1).invoke(model -> {
|
||||
mm.setC1_id(null);
|
||||
mm.setC1_guest(model);
|
||||
|
||||
}) :
|
||||
Uni.createFrom().nullItem().invoke(__ -> {
|
||||
mm.setC1_id(null);
|
||||
mm.setC1_guest(null);
|
||||
}))
|
||||
.call(mm -> match.c2() != null ?
|
||||
match.c2() >= 0 ?
|
||||
combRepository.findById(match.c2()).invoke(model -> {
|
||||
mm.setC2_id(model);
|
||||
mm.setC2_guest(null);
|
||||
}) :
|
||||
competitionGuestRepository.findById(match.c2() * -1).invoke(model -> {
|
||||
mm.setC2_id(null);
|
||||
mm.setC2_guest(model);
|
||||
}) :
|
||||
Uni.createFrom().nullItem().invoke(__ -> {
|
||||
mm.setC2_id(null);
|
||||
mm.setC2_guest(null);
|
||||
}))
|
||||
.chain(mm -> Panache.withTransaction(() -> matchRepository.persist(mm)))
|
||||
.call(mm -> CompetitionWS.sendNotifyToOtherEditor(connection, "sendMatch",
|
||||
MatchEntity.fromModel(mm)))
|
||||
.replaceWithVoid();
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record MatchComb(long id, Long c1, Long c2) {
|
||||
}
|
||||
}
|
||||
|
||||
36
src/main/java/fr/titionfire/ffsaf/ws/recv/RRegister.java
Normal file
36
src/main/java/fr/titionfire/ffsaf/ws/recv/RRegister.java
Normal file
@ -0,0 +1,36 @@
|
||||
package fr.titionfire.ffsaf.ws.recv;
|
||||
|
||||
import fr.titionfire.ffsaf.data.repository.CompetitionRepository;
|
||||
import fr.titionfire.ffsaf.domain.entity.CombEntity;
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
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 jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import org.hibernate.reactive.mutiny.Mutiny;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@WithSession
|
||||
@ApplicationScoped
|
||||
@RegisterForReflection
|
||||
public class RRegister {
|
||||
|
||||
@Inject
|
||||
CompetitionRepository competitionRepository;
|
||||
|
||||
@WSReceiver(code = "getRegister", permission = PermLevel.ADMIN)
|
||||
public Uni<?> getRegister(WebSocketConnection connection, Object o) {
|
||||
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
||||
.call(cm -> Mutiny.fetch(cm.getInsc()))
|
||||
.call(cm -> Mutiny.fetch(cm.getGuests()))
|
||||
.map(cm -> {
|
||||
ArrayList<CombEntity> combEntities = new ArrayList<>();
|
||||
combEntities.addAll(cm.getInsc().stream().map(CombEntity::fromModel).toList());
|
||||
combEntities.addAll(cm.getGuests().stream().map(CombEntity::fromModel).toList());
|
||||
return combEntities;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -8,12 +8,8 @@
|
||||
|
||||
<link href="/index.css" rel="stylesheet">
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
@ -27,11 +23,8 @@
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
|
||||
<script async type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
91
src/main/webapp/src/hooks/useComb.jsx
Normal file
91
src/main/webapp/src/hooks/useComb.jsx
Normal file
@ -0,0 +1,91 @@
|
||||
import {createContext, useContext, useReducer} from "react";
|
||||
|
||||
const CombsContext = createContext({});
|
||||
const CombsDispatchContext = createContext(() => {
|
||||
});
|
||||
|
||||
function compareCombs(a, b) {
|
||||
for (const keys of Object.keys(a)) {
|
||||
if (a[keys] !== b[keys]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'SET_COMB':
|
||||
if (state[action.payload.id] === undefined || !compareCombs(state[action.payload.id], action.payload)) {
|
||||
console.debug("Updating comb", action.payload);
|
||||
return {
|
||||
...state,
|
||||
[action.payload.id]: action.payload.value
|
||||
}
|
||||
}
|
||||
return state
|
||||
case 'SET_ALL':
|
||||
// By default, we only update some fields to avoid overwriting with incomplete data
|
||||
const combs = (action.payload.source === "register") ? action.payload.data : action.payload.data.map(e => {
|
||||
return {
|
||||
id: e.id,
|
||||
fname: e.fname,
|
||||
lname: e.lname,
|
||||
genre: e.genre,
|
||||
country: e.country,
|
||||
}
|
||||
});
|
||||
|
||||
if (combs.some(e => state[e.id] === undefined || !compareCombs(e, state[e.id]))) {
|
||||
const newCombs = {};
|
||||
for (const o of combs) {
|
||||
newCombs[o.id] = o;
|
||||
}
|
||||
console.debug("Updating combs", newCombs);
|
||||
|
||||
return {
|
||||
...state,
|
||||
...newCombs
|
||||
}
|
||||
}
|
||||
return state
|
||||
case 'REMOVE_COMB':
|
||||
const newState = {...state}
|
||||
delete newState[action.payload]
|
||||
return newState
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export function CombsProvider({children}) {
|
||||
const [combs, dispatch] = useReducer(reducer, {})
|
||||
|
||||
return <CombsContext.Provider value={combs}>
|
||||
<CombsDispatchContext.Provider value={dispatch}>
|
||||
{children}
|
||||
</CombsDispatchContext.Provider>
|
||||
</CombsContext.Provider>
|
||||
}
|
||||
|
||||
export function useCombs() {
|
||||
const combs = useContext(CombsContext);
|
||||
const getComb = (id, defaultValue = null) => {
|
||||
return combs[id] !== undefined ? combs[id] : defaultValue;
|
||||
}
|
||||
return {getComb, combs};
|
||||
}
|
||||
|
||||
export function useCombsDispatch() {
|
||||
return useContext(CombsDispatchContext);
|
||||
}
|
||||
|
||||
export function CombName({combId}) {
|
||||
const {getComb} = useCombs();
|
||||
const comb = getComb(combId, null);
|
||||
if (comb) {
|
||||
return <>{comb.fname} {comb.lname}</>
|
||||
} else {
|
||||
return <>[Comb #{combId}]</>
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import {createContext, useContext, useState} from "react";
|
||||
import {createContext, useContext, useEffect, useState} from "react";
|
||||
import './useLoading.css'
|
||||
import {FallingLines} from "react-loader-spinner";
|
||||
|
||||
@ -28,9 +28,22 @@ export function LoadingProvider({children}) {
|
||||
|
||||
function LoadingOverLay() {
|
||||
const showOverlay = useLoading()
|
||||
const [realShow, setRealShow] = useState(showOverlay)
|
||||
|
||||
if (showOverlay) {
|
||||
return <div className="overlayBG" style={{position: showOverlay === 1 ? 'absolute' : 'fixed'}}>
|
||||
useEffect(() => {
|
||||
if (showOverlay === 0) {
|
||||
setRealShow(showOverlay)
|
||||
return
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setRealShow(showOverlay)
|
||||
}, 100)
|
||||
return () => clearTimeout(timer)
|
||||
}, [showOverlay]);
|
||||
|
||||
if (realShow) {
|
||||
return <div className="overlayBG" style={{position: realShow === 1 ? 'absolute' : 'fixed'}}>
|
||||
<div className="overlayContent" onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
}}>
|
||||
@ -40,4 +53,4 @@ function LoadingOverLay() {
|
||||
} else {
|
||||
return <></>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,14 @@
|
||||
import {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {CheckField, TextField} from "../../../components/MemberCustomFiels.jsx";
|
||||
import {toast} from "react-toastify";
|
||||
import {build_tree, from_sendTree, resize_tree, TreeNode} from "../../../utils/TreeUtils.js"
|
||||
import {build_tree, resize_tree} from "../../../utils/TreeUtils.js"
|
||||
import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx";
|
||||
import {SimpleReducer} from "../../../utils/SimpleReducer.jsx";
|
||||
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
|
||||
import {CategoryContent} from "./CategoryAdminContent.jsx";
|
||||
|
||||
export function CMAdmin() {
|
||||
const [catId, setCatId] = useState(null);
|
||||
const [cat, setCat] = useState(null);
|
||||
const [combs, setCombs] = useState([])
|
||||
// const [cats, setCats] = useState([])
|
||||
|
||||
const {dispatch} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
@ -31,18 +26,6 @@ export function CMAdmin() {
|
||||
return () => dispatch({type: 'removeListener', payload: categoryListener})
|
||||
}, []);
|
||||
|
||||
/*useEffect(() => {
|
||||
toast.promise(sendRequest("getAllCategory", {}),
|
||||
{
|
||||
pending: 'Chargement des catégories...',
|
||||
success: 'Catégories chargées !',
|
||||
error: 'Erreur lors du chargement des catégories'
|
||||
}
|
||||
).then((data) => {
|
||||
setCats(data);
|
||||
})
|
||||
}, []);*/
|
||||
|
||||
return <>
|
||||
<div className="card">
|
||||
<div className='card-header'>
|
||||
@ -391,59 +374,3 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
function CategoryContent({cat, catId, setCat}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {sendRequest, dispatch} = useWS();
|
||||
const [matches, reducer] = useReducer(MarchReducer, []);
|
||||
|
||||
useEffect(() => {
|
||||
const treeListener = ({data}) => {
|
||||
if (!cat || data.length < 1 || data[0].categorie !== cat.id)
|
||||
return
|
||||
setCat({
|
||||
...cat,
|
||||
trees: data.map(d => from_sendTree(d, true))
|
||||
})
|
||||
let matches2 = [];
|
||||
data.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => matches2.push({...data_}));
|
||||
reducer({type: 'REPLACE_TREE', payload: matches2});
|
||||
}
|
||||
dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}})
|
||||
return () => dispatch({type: 'removeListener', payload: treeListener})
|
||||
}, [cat]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!catId)
|
||||
return;
|
||||
setLoading(1);
|
||||
sendRequest('getFullCategory', catId)
|
||||
.then((data) => {
|
||||
setCat({
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
liceName: data.liceName,
|
||||
type: data.type,
|
||||
trees: data.trees.map(d => from_sendTree(d, true))
|
||||
})
|
||||
|
||||
let matches2 = [];
|
||||
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => matches2.push({...data_}));
|
||||
data.matches.forEach((data_) => matches2.push({...data_}));
|
||||
|
||||
reducer({type: 'REPLACE_ALL', payload: matches2});
|
||||
}).finally(() => setLoading(0))
|
||||
}, [catId]);
|
||||
|
||||
console.log("Matches in category content:", matches);
|
||||
|
||||
return <>
|
||||
<div className="col">
|
||||
|
||||
|
||||
<div className="vr"></div>
|
||||
</div>
|
||||
<div className="col">
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -0,0 +1,269 @@
|
||||
import {useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
||||
import {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
|
||||
import {CombName, useCombs, useCombsDispatch} from "../../../hooks/useComb.jsx";
|
||||
import {from_sendTree, TreeNode} from "../../../utils/TreeUtils.js";
|
||||
import {DrawGraph} from "../../result/DrawGraph.jsx";
|
||||
import {SelectCombModalContent} from "./SelectCombModalContent.jsx";
|
||||
|
||||
export function CategoryContent({cat, catId, setCat}) {
|
||||
const setLoading = useLoadingSwitcher()
|
||||
const {sendRequest, dispatch} = useWS();
|
||||
const [matches, reducer] = useReducer(MarchReducer, []);
|
||||
const [groups, setGroups] = useState([])
|
||||
const groupsRef = useRef(groups);
|
||||
const combDispatch = useCombsDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
groupsRef.current = groups
|
||||
}, [groups]);
|
||||
|
||||
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(() => {
|
||||
const treeListener = ({data}) => {
|
||||
if (!cat || data.length < 1 || data[0].categorie !== cat.id)
|
||||
return
|
||||
setCat({
|
||||
...cat,
|
||||
trees: 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}) => {
|
||||
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)}});
|
||||
|
||||
if (data.c1 !== null && !groupsRef.current.some(g => g.id === data.c1?.id))
|
||||
setGroups(prev => [...prev, {id: data.c1?.id, poule: data.poule}])
|
||||
if (data.c2 !== null && !groupsRef.current.some(g => g.id === data.c2?.id))
|
||||
setGroups(prev => [...prev, {id: data.c2?.id, poule: data.poule}])
|
||||
}
|
||||
|
||||
dispatch({type: 'addListener', payload: {callback: treeListener, code: 'sendTreeCategory'}})
|
||||
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
|
||||
return () => {
|
||||
dispatch({type: 'removeListener', payload: treeListener})
|
||||
dispatch({type: 'removeListener', payload: matchListener})
|
||||
}
|
||||
}, [cat]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!catId)
|
||||
return;
|
||||
setLoading(1);
|
||||
sendRequest('getFullCategory', catId)
|
||||
.then((data) => {
|
||||
setCat({
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
liceName: data.liceName,
|
||||
type: data.type,
|
||||
trees: 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}});
|
||||
|
||||
const activeMatches = matches2.filter(m => m.poule !== '-')
|
||||
const combsIDs = activeMatches.flatMap(d => [d.c1, d.c2]).filter((v, i, a) => v != null && a.indexOf(v) === i)
|
||||
.map(d => {
|
||||
return {id: d, poule: activeMatches.find(m => m.c1 === d || m.c2 === d)?.poule}
|
||||
})
|
||||
setGroups(combsIDs)
|
||||
}).finally(() => setLoading(0))
|
||||
}, [catId]);
|
||||
|
||||
|
||||
return <>
|
||||
<div className="col-md-3">
|
||||
<AddComb groups={groups} setGroups={setGroups}/>
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
{cat && <ListMatch cat={cat} matches={matches} groups={groups}/>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function AddComb({groups, setGroups}) {
|
||||
const {data} = useRequestWS("getRegister", null)
|
||||
const combDispatch = useCombsDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (data === null)
|
||||
return;
|
||||
combDispatch({type: 'SET_ALL', payload: {source: "register", data: data}});
|
||||
}, [data]);
|
||||
|
||||
return <>
|
||||
<ol className="list-group list-group-numbered">
|
||||
{groups.map((comb) => (
|
||||
<li key={comb.id} className="list-group-item list-group-item-action d-flex justify-content-between align-items-start">
|
||||
<div className="ms-2 me-auto"><CombName combId={comb.id}/></div>
|
||||
<span className="badge text-bg-primary rounded-pill">{comb.poule}</span>
|
||||
</li>)
|
||||
)}
|
||||
</ol>
|
||||
<button type="button" className="btn btn-primary mt-3 w-100" data-bs-toggle="modal" data-bs-target="#selectCombModal"
|
||||
disabled={data === null}>Ajouter des combattants
|
||||
</button>
|
||||
|
||||
<div className="modal fade" id="selectCombModal" tabIndex="-1" aria-labelledby="selectCombModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog modal-dialog-scrollable modal-lg modal-fullscreen-lg-down">
|
||||
<div className="modal-content">
|
||||
<SelectCombModalContent data={data} setGroups={setGroups}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function ListMatch({cat, matches, groups}) {
|
||||
const [type, setType] = useState(1);
|
||||
|
||||
return <>
|
||||
{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 === 2 && <>
|
||||
<BuildTree treeData={cat.trees} matches={matches} groups={groups}/>
|
||||
</>}
|
||||
</>
|
||||
}
|
||||
|
||||
function BuildTree({treeData, matches, groups}) {
|
||||
const scrollRef = useRef(null)
|
||||
const selectRef = useRef(null)
|
||||
const lastMatchClick = useRef(null)
|
||||
const [combSelect, setCombSelect] = useState(0)
|
||||
const {getComb} = useCombs()
|
||||
const {sendRequest} = useWS();
|
||||
const setLoading = useLoadingSwitcher()
|
||||
|
||||
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 onMatchClick = (rect, matchId, comb) => {
|
||||
if (!treeData.some(t => t.isEnd(matchId)))
|
||||
return
|
||||
|
||||
const match = matches.find(m => m.id === matchId);
|
||||
if (!match)
|
||||
return;
|
||||
|
||||
const sel = selectRef.current;
|
||||
sel.style.top = rect.y + "px";
|
||||
sel.style.left = rect.x + "px";
|
||||
sel.style.width = rect.width + "px";
|
||||
sel.style.height = rect.height + "px";
|
||||
sel.style.display = "block";
|
||||
|
||||
lastMatchClick.current = {matchId, comb};
|
||||
setCombSelect((comb === 1 ? match.c1 : match.c2) || 0);
|
||||
}
|
||||
|
||||
const onClickVoid = () => {
|
||||
const sel = selectRef.current;
|
||||
sel.style.display = "none";
|
||||
|
||||
lastMatchClick.current = null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (lastMatchClick.current == null)
|
||||
return
|
||||
|
||||
const {matchId, comb} = lastMatchClick.current
|
||||
const match = matches.find(m => m.id === matchId)
|
||||
if (!match)
|
||||
return
|
||||
|
||||
const combSelect_ = combSelect === 0 ? null : combSelect;
|
||||
|
||||
const data = {id: match.id, c1: match.c1, c2: match.c2}
|
||||
if (comb === 1) {
|
||||
if (match.c1 === combSelect_)
|
||||
return
|
||||
data.c1 = combSelect_
|
||||
} else if (comb === 2) {
|
||||
if (match.c2 === combSelect_)
|
||||
return
|
||||
data.c2 = combSelect_
|
||||
}
|
||||
|
||||
setLoading(1)
|
||||
sendRequest('updateMatchComb', data)
|
||||
.finally(() => {
|
||||
setLoading(0)
|
||||
onClickVoid()
|
||||
})
|
||||
}, [combSelect])
|
||||
|
||||
const combsIDs = groups.map(m => m.id);
|
||||
|
||||
return <div ref={scrollRef} className="overflow-x-auto" style={{position: "relative"}}>
|
||||
<DrawGraph root={initTree(treeData)} scrollRef={scrollRef} onMatchClick={onMatchClick} onClickVoid={onClickVoid}/>
|
||||
<select ref={selectRef} className="form-select" style={{position: "absolute", top: 0, left: 0, display: "none"}}
|
||||
value={combSelect} onChange={e => setCombSelect(Number(e.target.value))}>
|
||||
<option value={0}>-- Sélectionner un combattant --</option>
|
||||
{combsIDs.map((combId) => (
|
||||
<option key={combId} value={combId}><CombName combId={combId}/></option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
@ -4,6 +4,7 @@ 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";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@ -37,13 +38,15 @@ function HomeComp() {
|
||||
|
||||
return <WSProvider url={`${vite_url.replace('http', 'ws')}/api/ws/competition/${compUuid}`} onmessage={messageHandler}>
|
||||
<WSStatus setPerm={setPerm}/>
|
||||
<LoadingProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home2 perm={perm}/>}/>
|
||||
<Route path="/admin" element={<CMAdmin/>}/>
|
||||
<Route path="/test" element={<Test2/>}/>
|
||||
</Routes>
|
||||
</LoadingProvider>
|
||||
<CombsProvider>
|
||||
<LoadingProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home2 perm={perm}/>}/>
|
||||
<Route path="/admin" element={<CMAdmin/>}/>
|
||||
<Route path="/test" element={<Test2/>}/>
|
||||
</Routes>
|
||||
</LoadingProvider>
|
||||
</CombsProvider>
|
||||
</WSProvider>
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,266 @@
|
||||
import {useCountries} from "../../../hooks/useCountries.jsx";
|
||||
import {useEffect, useReducer, useState} from "react";
|
||||
import {CatList, getCatName} from "../../../utils/Tools.js";
|
||||
import {CombName} from "../../../hooks/useComb.jsx";
|
||||
|
||||
function SelectReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_ID':
|
||||
return {
|
||||
...state,
|
||||
[action.payload]: !(state[action.payload] || false)
|
||||
}
|
||||
case 'ADD_ALL':
|
||||
return {
|
||||
...state,
|
||||
...action.payload.reduce((acc, id) => {
|
||||
acc[id] = false;
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
case 'CLEAR_ACTIVE':
|
||||
const newState = {...state};
|
||||
Object.keys(newState).forEach(id => {
|
||||
newState[id] = false;
|
||||
});
|
||||
return newState;
|
||||
case 'REMOVE_ACTIVE':
|
||||
const filteredState = {...state};
|
||||
Object.keys(filteredState).forEach(id => {
|
||||
if (filteredState[id]) {
|
||||
delete filteredState[id];
|
||||
}
|
||||
});
|
||||
return filteredState;
|
||||
case 'REMOVE_IN':
|
||||
const filteredState2 = {...state};
|
||||
Object.keys(filteredState2).forEach(id => {
|
||||
if (action.payload.includes(id)) {
|
||||
delete filteredState2[id];
|
||||
}
|
||||
});
|
||||
return filteredState2;
|
||||
case 'REMOVE_ALL':
|
||||
return {};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function SelectCombModalContent({data, setGroups}) {
|
||||
const country = useCountries('fr')
|
||||
const [dispo, dispoReducer] = useReducer(SelectReducer, {})
|
||||
const [select, selectReducer] = useReducer(SelectReducer, {})
|
||||
|
||||
const [targetGroupe, setTargetGroupe] = useState("A")
|
||||
const [search, setSearch] = useState("")
|
||||
const [country_, setCountry_] = useState("")
|
||||
const [club, setClub] = useState("")
|
||||
const [gender, setGender] = useState({H: true, F: true, NA: true})
|
||||
const [cat, setCat] = useState(-1)
|
||||
const [weightMin, setWeightMin] = useState(0)
|
||||
const [weightMax, setWeightMax] = useState(0)
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setGroups(prev => [...prev.filter(d => select[d.id] === undefined), ...Object.keys(select).map(id => {
|
||||
return {id: Number(id), poule: targetGroupe}
|
||||
})])
|
||||
|
||||
dispoReducer({type: 'REMOVE_ALL'})
|
||||
selectReducer({type: 'REMOVE_ALL'})
|
||||
if (data == null)
|
||||
return
|
||||
dispoReducer({type: 'ADD_ALL', payload: data.map(d => d.id)})
|
||||
}
|
||||
|
||||
useEffect(() => { // TODO: add ws listener
|
||||
if (data == null)
|
||||
return
|
||||
const selectedIds = Object.keys(select).map(g => Number(g))
|
||||
dispoReducer({type: 'ADD_ALL', payload: data.map(d => d.id).filter(id => !selectedIds.includes(id))})
|
||||
}, [data])
|
||||
|
||||
function applyFilter(dataIn, dataOut) {
|
||||
Object.keys(dataIn).forEach((id) => {
|
||||
const comb = data.find(d => d.id === Number(id));
|
||||
if (comb == null)
|
||||
return;
|
||||
if ((search === "" || comb.fname.toLowerCase().includes(search.toLowerCase()) || comb.lname.toLowerCase().includes(search.toLowerCase())
|
||||
|| (comb.fname + " " + comb.lname).toLowerCase().includes(search.toLowerCase()))
|
||||
&& (country_ === "" || comb.country === country_)
|
||||
&& (club === "" || comb.club_str === club)
|
||||
&& (gender.H && comb.genre === 'H' || gender.F && comb.genre === 'F' || gender.NA && comb.genre === 'NA')
|
||||
&& (cat === -1 || cat === Math.min(CatList.length, CatList.indexOf(comb.categorie) + comb.overCategory))
|
||||
&& (weightMin === 0 || comb.weight !== null && comb.weight >= weightMin)
|
||||
&& (weightMax === 0 || comb.weight !== null && comb.weight <= weightMax)) {
|
||||
dataOut[id] = dataIn[id];
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let clubList = [];
|
||||
if (data != null) {
|
||||
clubList = data.map(d => d.club_str).filter((v, i, a) => v !== "" && a.indexOf(v) === i);
|
||||
}
|
||||
|
||||
const dispoFiltered = {};
|
||||
applyFilter(dispo, dispoFiltered);
|
||||
|
||||
const selectFiltered = {};
|
||||
applyFilter(select, selectFiltered);
|
||||
|
||||
const moveComb = (event) => {
|
||||
event.preventDefault();
|
||||
switch (event.target.textContent) {
|
||||
case '>>':
|
||||
selectReducer({type: 'ADD_ALL', payload: Object.keys(dispoFiltered)})
|
||||
dispoReducer({type: 'REMOVE_IN', payload: Object.keys(dispoFiltered)});
|
||||
break;
|
||||
case '<<':
|
||||
dispoReducer({type: 'ADD_ALL', payload: Object.keys(selectFiltered)})
|
||||
selectReducer({type: 'REMOVE_IN', payload: Object.keys(selectFiltered)});
|
||||
break;
|
||||
case '>':
|
||||
selectReducer({type: 'ADD_ALL', payload: Object.keys(dispo).filter(id => dispo[id])})
|
||||
dispoReducer({type: 'REMOVE_ACTIVE'});
|
||||
break;
|
||||
case '<':
|
||||
dispoReducer({type: 'ADD_ALL', payload: Object.keys(select).filter(id => select[id])})
|
||||
selectReducer({type: 'REMOVE_ACTIVE'});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className="modal-header">
|
||||
<h1 className="modal-title fs-5" id="CategorieModalLabel">Sélectionner des combatants</h1>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="d-flex flex-wrap justify-content-around mb-1">
|
||||
<div className="col-md-5">
|
||||
<label htmlFor="input4" className="form-label">Recherche</label>
|
||||
<input type="text" className="form-control" id="input4" value={search} onChange={(e) => setSearch(e.target.value)}/>
|
||||
</div>
|
||||
|
||||
<div style={{width: "12em"}}>
|
||||
<label htmlFor="inputState0" className="form-label">Pays</label>
|
||||
<select id="inputState0" className="form-select" value={country_} onChange={(e) => setCountry_(e.target.value)}>
|
||||
<option value={""}>-- Tous --</option>
|
||||
{country && Object.keys(country).sort((a, b) => {
|
||||
if (a < b) return -1
|
||||
if (a > b) return 1
|
||||
return 0
|
||||
}).map((key, _) => {
|
||||
return (<option key={key} value={key}>{country[key]}</option>)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="inputState1" className="form-label">Club</label>
|
||||
<select id="inputState1" className="form-select" value={club} onChange={(e) => setClub(e.target.value)}>
|
||||
<option value={""}>-- Tous --</option>
|
||||
{clubList.sort((a, b) => a.localeCompare(b)).map((club) => (
|
||||
<option key={club} value={club}>{club}</option>))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-flex flex-wrap justify-content-around mb-1">
|
||||
<div>
|
||||
<label className="form-label">Genre</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck" checked={gender.H}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, H: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck">H</label>
|
||||
</div>
|
||||
<div className="form-check" style={{marginRight: '10px'}}>
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck2" checked={gender.F}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, F: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck2">F</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input className="form-check-input" type="checkbox" id="gridCheck3" checked={gender.NA}
|
||||
onChange={e => setGender((prev) => {
|
||||
return {...prev, NA: e.target.checked}
|
||||
})}/>
|
||||
<label className="form-check-label" htmlFor="gridCheck3">NA</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="inputState2" className="form-label">Catégorie</label>
|
||||
<select id="inputState2" className="form-select" value={cat} onChange={(e) => setCat(Number(e.target.value))}>
|
||||
<option value={-1}>-- Tous --</option>
|
||||
{CatList.map((cat, index) => {
|
||||
return (<option key={index} value={index}>{getCatName(cat)}</option>)
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="input5" className="form-label">Poids</label>
|
||||
<div className="row-cols-sm-auto d-flex align-items-center">
|
||||
<div style={{width: "4.25em"}}><input type="number" className="form-control" id="input5" value={weightMin} min="0"
|
||||
name="999"
|
||||
onChange={e => setWeightMin(Number(e.target.value))}/></div>
|
||||
<div><span>à</span></div>
|
||||
<div style={{width: "4.25em"}}><input type="number" className="form-control" value={weightMax} min="0" name="999"
|
||||
onChange={e => setWeightMax(Number(e.target.value))}/></div>
|
||||
<div><small>(0 = désactivé)</small></div>
|
||||
</div>
|
||||
</div>
|
||||
<button style={{display: "none"}} onClick={event => event.preventDefault()}></button>
|
||||
</div>
|
||||
<hr/>
|
||||
<div className="row g-3">
|
||||
<div className="col-md">
|
||||
<div style={{textAlign: "center"}}>Inscrit</div>
|
||||
<div className="list-group overflow-y-auto" style={{maxHeight: "50vh"}}>
|
||||
{dispoFiltered && Object.keys(dispoFiltered).length === 0 && <div>Aucun combattant disponible</div>}
|
||||
{Object.keys(dispoFiltered).map((id) => (
|
||||
<button key={id} type="button" className={"list-group-item list-group-item-action " + (dispoFiltered[id] ? "active" : "")}
|
||||
onClick={() => dispoReducer({type: 'TOGGLE_ID', payload: id})}>
|
||||
<CombName combId={id}/>
|
||||
</button>))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-auto" style={{margin: "0.5em auto"}}>
|
||||
<div className="d-flex flex-sm-column align-items-center h-100 justify-content-center">
|
||||
<button className="btn btn-secondary" style={{margin: "0.15em"}} onClick={moveComb}>>></button>
|
||||
<button className="btn btn-secondary" style={{margin: "0.15em"}} onClick={moveComb}><<</button>
|
||||
<button className="btn btn-secondary" style={{margin: "0.15em"}} onClick={moveComb}>></button>
|
||||
<button className="btn btn-secondary" style={{margin: "0.15em"}} onClick={moveComb}><</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md">
|
||||
<div style={{textAlign: "center"}}>Sélectionner</div>
|
||||
<div className="list-group overflow-y-auto" style={{maxHeight: "50vh"}}>
|
||||
{selectFiltered && Object.keys(selectFiltered).length === 0 && <div>Aucun combattant sélectionné</div>}
|
||||
{Object.keys(selectFiltered).map((id) => (
|
||||
<button key={id} type="button"
|
||||
className={"list-group-item list-group-item-action " + (selectFiltered[id] ? "active" : "")}
|
||||
onClick={() => selectReducer({type: 'TOGGLE_ID', payload: id})}>
|
||||
<CombName combId={id}/>
|
||||
</button>))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button type="button" className="btn btn-secondary" data-bs-dismiss="modal">Fermer</button>
|
||||
<div className="vr"></div>
|
||||
<label htmlFor="input6" className="form-label">Poule</label>
|
||||
<input type="text" className="form-control" id="input6" style={{width: "3em"}} maxLength={1} value={targetGroupe}
|
||||
onChange={(e) => setTargetGroupe(e.target.value)}/>
|
||||
<button type="submit" className="btn btn-primary" data-bs-dismiss="modal" onClick={handleSubmit}>Ajouter</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@ -3,8 +3,26 @@ import {useEffect, useRef} from "react";
|
||||
const max_x = 500;
|
||||
const size = 24;
|
||||
|
||||
export function DrawGraph({root = []}) {
|
||||
function getMousePos(canvas, evt) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: evt.clientX - rect.left,
|
||||
y: evt.clientY - rect.top
|
||||
};
|
||||
}
|
||||
|
||||
export function DrawGraph({
|
||||
root = [],
|
||||
scrollRef = null,
|
||||
onMatchClick = function (rect, match, comb) {
|
||||
},
|
||||
onClickVoid = function () {
|
||||
}
|
||||
}) {
|
||||
const canvasRef = useRef(null);
|
||||
const actionCanvasRef = useRef(null);
|
||||
const ctxARef = useRef(null);
|
||||
const actionMapRef = useRef({});
|
||||
|
||||
function getBounds(root) {
|
||||
let px = max_x;
|
||||
@ -126,8 +144,19 @@ export function DrawGraph({root = []}) {
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
const newColor = () => {
|
||||
const letters = '0123456789ABCDEF'
|
||||
let color
|
||||
do {
|
||||
color = '#'
|
||||
for (let i = 0; i < 6; i++)
|
||||
color += letters[Math.floor(Math.random() * 16)]
|
||||
} while (actionMapRef.current[color] !== undefined)
|
||||
return color;
|
||||
}
|
||||
|
||||
// Fonction pour dessiner un nœud
|
||||
const drawNode = (ctx, tree, px, py, max_y) => {
|
||||
const drawNode = (ctx, ctxA, tree, px, py, max_y) => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px, py);
|
||||
ctx.lineTo(px - size, py);
|
||||
@ -147,15 +176,20 @@ export function DrawGraph({root = []}) {
|
||||
ctx.stroke();
|
||||
|
||||
printScores(ctx, match.scores, px, py, 1);
|
||||
ctx.fillStyle = "#FF0000";
|
||||
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName,
|
||||
px - size * 2 - size * 8, py - size - (size * 1.5 / 2 | 0),
|
||||
size * 8, (size * 1.5 | 0), false, true);
|
||||
|
||||
ctx.fillStyle = "#0000FF";
|
||||
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName,
|
||||
px - size * 2 - size * 8, py + size - (size * 1.5 / 2 | 0),
|
||||
size * 8, (size * 1.5 | 0), false, true);
|
||||
const pos = {x: px - size * 2 - size * 8, y: py - size - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)}
|
||||
ctx.fillStyle = "#FF0000"
|
||||
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, false, true)
|
||||
ctxA.fillStyle = newColor()
|
||||
ctxA.fillRect(pos.x, pos.y, pos.width, pos.height)
|
||||
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos, match: match.id, comb: 1}
|
||||
|
||||
const pos2 = {x: px - size * 2 - size * 8, y: py + size - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)}
|
||||
ctx.fillStyle = "#0000FF"
|
||||
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, false, true)
|
||||
ctxA.fillStyle = newColor()
|
||||
ctxA.fillRect(pos2.x, pos2.y, pos2.width, pos2.height)
|
||||
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos2, match: match.id, comb: 2}
|
||||
|
||||
if (max_y.current < py + size + ((size * 1.5 / 2) | 0)) {
|
||||
max_y.current = py + size + (size * 1.5 / 2 | 0);
|
||||
@ -173,15 +207,20 @@ export function DrawGraph({root = []}) {
|
||||
ctx.stroke();
|
||||
|
||||
printScores(ctx, match.scores, px, py, 1.5);
|
||||
ctx.fillStyle = "#FF0000";
|
||||
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName,
|
||||
px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2 | 0),
|
||||
size * 8, (size * 1.5 | 0), true, true);
|
||||
|
||||
ctx.fillStyle = "#0000FF";
|
||||
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName,
|
||||
px - size * 2 - size * 8, py + size * 2 * death - (size * 1.5 / 2 | 0),
|
||||
size * 8, (size * 1.5 | 0), true, true);
|
||||
const pos = {x: px - size * 2 - size * 8, y: py - size * 2 * death - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)}
|
||||
ctx.fillStyle = "#FF0000"
|
||||
printText(ctx, (match.c1FullName == null) ? "" : match.c1FullName, pos.x, pos.y, pos.width, pos.height, true, true)
|
||||
ctxA.fillStyle = newColor()
|
||||
ctxA.fillRect(pos.x, pos.y, pos.width, pos.height)
|
||||
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos, match: match.id, comb: 1}
|
||||
|
||||
const pos2 = {x: px - size * 2 - size * 8, y: py + size * 2 * death - (size * 1.5 / 2 | 0), width: size * 8, height: (size * 1.5 | 0)}
|
||||
ctx.fillStyle = "#0000FF"
|
||||
printText(ctx, (match.c2FullName == null) ? "" : match.c2FullName, pos2.x, pos2.y, pos2.width, pos2.height, true, true)
|
||||
ctxA.fillStyle = newColor()
|
||||
ctxA.fillRect(pos2.x, pos2.y, pos2.width, pos2.height)
|
||||
actionMapRef.current[ctxA.fillStyle] = {type: 'match', rect: pos2, match: match.id, comb: 2}
|
||||
|
||||
if (max_y.current < py + size * 2 * death + ((size * 1.5 / 2) | 0)) {
|
||||
max_y.current = py + size * 2 * death + ((size * 1.5 / 2 | 0));
|
||||
@ -189,10 +228,10 @@ export function DrawGraph({root = []}) {
|
||||
}
|
||||
|
||||
if (tree.left != null) {
|
||||
drawNode(ctx, tree.left, px - size * 2 - size * 8, py - size * 2 * death, max_y);
|
||||
drawNode(ctx, ctxA, tree.left, px - size * 2 - size * 8, py - size * 2 * death, max_y);
|
||||
}
|
||||
if (tree.right != null) {
|
||||
drawNode(ctx, tree.right, px - size * 2 - size * 8, py + size * 2 * death, max_y);
|
||||
drawNode(ctx, ctxA, tree.right, px - size * 2 - size * 8, py + size * 2 * death, max_y);
|
||||
}
|
||||
};
|
||||
|
||||
@ -211,6 +250,7 @@ export function DrawGraph({root = []}) {
|
||||
useEffect(() => {
|
||||
if (root.length === 0) return;
|
||||
|
||||
// Dessiner sur le canvas principal
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const [minx, maxx, miny, maxy] = getBounds(root);
|
||||
@ -222,6 +262,20 @@ export function DrawGraph({root = []}) {
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = "#000000";
|
||||
|
||||
// Dessiner sur le canvas d'action
|
||||
const actionCanvas = actionCanvasRef.current;
|
||||
const ctxA = actionCanvas.getContext("2d", {willReadFrequently: true});
|
||||
ctxARef.current = ctxA;
|
||||
|
||||
actionCanvas.width = maxx - minx;
|
||||
actionCanvas.height = maxy - miny;
|
||||
ctxA.translate(-minx, -miny);
|
||||
ctxA.fillStyle = "#000000";
|
||||
ctxA.lineWidth = 2;
|
||||
ctxA.strokeStyle = "#000000";
|
||||
|
||||
actionMapRef.current = {};
|
||||
|
||||
let px = maxx;
|
||||
let py;
|
||||
const max_y = {current: 0};
|
||||
@ -242,11 +296,91 @@ export function DrawGraph({root = []}) {
|
||||
size * 8, (size * 1.5 | 0), true, false);
|
||||
|
||||
px = px - size * 2 - size * 8;
|
||||
drawNode(ctx, node, px, py, max_y);
|
||||
drawNode(ctx, ctxA, node, px, py, max_y);
|
||||
py = max_y.current + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0)));
|
||||
px = maxx;
|
||||
}
|
||||
|
||||
for (const color in actionMapRef.current) {
|
||||
const old = actionMapRef.current[color]
|
||||
if (old.rect == null) continue;
|
||||
actionMapRef.current[color] = {
|
||||
...old,
|
||||
rect: {x: old.rect.x - minx, y: old.rect.y - miny + 10, width: old.rect.width, height: old.rect.height}
|
||||
};
|
||||
}
|
||||
}, [root]);
|
||||
|
||||
return <canvas ref={canvasRef} style={{border: "1px solid grey", marginTop: "10px"}} id="myCanvas"></canvas>;
|
||||
useEffect(() => {
|
||||
let isDownScroll = false
|
||||
let downColor = undefined
|
||||
let startX
|
||||
let scrollLeft
|
||||
|
||||
const mousedown = (e) => {
|
||||
const pos = getMousePos(canvasRef.current, e)
|
||||
const pixel = ctxARef.current.getImageData(pos.x, pos.y, 1, 1).data
|
||||
|
||||
downColor = (pixel[3] === 0) ? undefined : `#${((1 << 24) + (pixel[0] << 16) + (pixel[1] << 8) + pixel[2]).toString(16).slice(1)}`
|
||||
|
||||
if (downColor === undefined)
|
||||
onClickVoid()
|
||||
|
||||
isDownScroll = downColor === undefined
|
||||
startX = e.pageX - scrollRef.current.offsetLeft
|
||||
scrollLeft = scrollRef.current.scrollLeft
|
||||
}
|
||||
|
||||
const mouseleave = () => {
|
||||
isDownScroll = false
|
||||
downColor = undefined
|
||||
}
|
||||
|
||||
const mouseup = (e) => {
|
||||
if (isDownScroll || downColor === undefined) {
|
||||
isDownScroll = false
|
||||
return
|
||||
}
|
||||
|
||||
const pos = getMousePos(canvasRef.current, e)
|
||||
const pixel = ctxARef.current.getImageData(pos.x, pos.y, 1, 1).data
|
||||
const upColor = `#${((1 << 24) + (pixel[0] << 16) + (pixel[1] << 8) + pixel[2]).toString(16).slice(1)}`
|
||||
|
||||
if (upColor === downColor) {
|
||||
const action = actionMapRef.current[downColor]
|
||||
if (action.type === 'match')
|
||||
onMatchClick(action.rect, action.match, action.comb);
|
||||
}
|
||||
}
|
||||
|
||||
const mousemove = (e) => {
|
||||
if (!isDownScroll) return
|
||||
e.preventDefault()
|
||||
const x = e.pageX - scrollRef.current.offsetLeft
|
||||
const walk = (x - startX) // Ajuste la vitesse de défilement
|
||||
scrollRef.current.scrollLeft = scrollLeft - walk
|
||||
}
|
||||
|
||||
if (scrollRef) {
|
||||
canvasRef.current.addEventListener("mousedown", mousedown)
|
||||
canvasRef.current.addEventListener("mouseleave", mouseleave)
|
||||
canvasRef.current.addEventListener("mouseup", mouseup)
|
||||
canvasRef.current.addEventListener("mousemove", mousemove)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (canvasRef && canvasRef.current) {
|
||||
canvasRef.current.removeEventListener("mousedown", mousedown)
|
||||
canvasRef.current.removeEventListener("mouseleave", mouseleave)
|
||||
canvasRef.current.removeEventListener("mouseup", mouseup)
|
||||
canvasRef.current.removeEventListener("mousemove", mousemove)
|
||||
}
|
||||
}
|
||||
}, [onMatchClick, onClickVoid]);
|
||||
|
||||
return <div style={{position: "relative"}}>
|
||||
<canvas ref={actionCanvasRef} style={{border: "1px solid grey", marginTop: "10px", position: "absolute", top: 0, left: 0, opacity: 0}}
|
||||
id="myCanvas2"></canvas>
|
||||
<canvas ref={canvasRef} style={{border: "1px solid grey", marginTop: "10px", position: "relative", opacity: 1}} id="myCanvas"></canvas>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import {AxiosError} from "../../components/AxiosError.jsx";
|
||||
import {ThreeDots} from "react-loader-spinner";
|
||||
import {useEffect, useState} from "react";
|
||||
import {DrawGraph} from "./DrawGraph.jsx";
|
||||
import {TreeNode} from "../../utils/TreeUtils.js";
|
||||
|
||||
function CupImg() {
|
||||
return <img decoding="async" loading="lazy" width="16" height="16" className="wp-image-1635"
|
||||
@ -143,27 +144,6 @@ function BuildRankArray({rankArray}) {
|
||||
}
|
||||
|
||||
function BuildTree({treeData}) {
|
||||
class TreeNode {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
this.left = null;
|
||||
this.right = null;
|
||||
}
|
||||
|
||||
death() {
|
||||
let dg = 0;
|
||||
let dd = 0;
|
||||
|
||||
if (this.right != null)
|
||||
dg = this.right.death();
|
||||
|
||||
if (this.left != null)
|
||||
dg = this.left.death();
|
||||
|
||||
return 1 + Math.max(dg, dd);
|
||||
}
|
||||
}
|
||||
|
||||
function parseTree(data_in) {
|
||||
if (data_in?.data == null)
|
||||
return null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user