Compare commits
9 Commits
09a51edd5f
...
dev-comp
| Author | SHA1 | Date | |
|---|---|---|---|
| 73f026210c | |||
| 4b969e6d69 | |||
| 4706af27f8 | |||
| 3e8c19534b | |||
| a1b5ca2694 | |||
| c5f7b81ac3 | |||
| 7f999733dc | |||
| 0ac92fcda3 | |||
| 8e01fb46f8 |
@@ -66,15 +66,19 @@ public class MatchModel {
|
|||||||
List<CardboardModel> cardboard = new ArrayList<>();
|
List<CardboardModel> cardboard = new ArrayList<>();
|
||||||
|
|
||||||
public String getC1Name() {
|
public String getC1Name() {
|
||||||
if (c1_id == null)
|
if (c1_id != null)
|
||||||
|
return c1_id.fname + " " + c1_id.lname;
|
||||||
|
if (c1_guest != null)
|
||||||
return c1_guest.fname + " " + c1_guest.lname;
|
return c1_guest.fname + " " + c1_guest.lname;
|
||||||
return c1_id.fname + " " + c1_id.lname;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getC2Name() {
|
public String getC2Name() {
|
||||||
if (c2_id == null)
|
if (c2_id != null)
|
||||||
|
return c2_id.fname + " " + c2_id.lname;
|
||||||
|
if (c2_guest != null)
|
||||||
return c2_guest.fname + " " + c2_guest.lname;
|
return c2_guest.fname + " " + c2_guest.lname;
|
||||||
return c2_id.fname + " " + c2_id.lname;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public int win() {
|
public int win() {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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> {
|
||||||
|
}
|
||||||
@@ -17,7 +17,10 @@ public class CardboardEntity {
|
|||||||
int yellow;
|
int yellow;
|
||||||
|
|
||||||
public static CardboardEntity fromModel(CardboardModel model) {
|
public static CardboardEntity fromModel(CardboardModel model) {
|
||||||
return new CardboardEntity(model.getComb().getId(), model.getMatch().getId(), model.getCompet().getId(),
|
return new CardboardEntity(
|
||||||
|
model.getComb() != null ? model.getComb().getId() : model.getGuestComb().getId() * -1,
|
||||||
|
model.getMatch().getId(),
|
||||||
|
model.getCompet().getId(),
|
||||||
model.getRed(), model.getYellow());
|
model.getRed(), model.getYellow());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class CombEntity {
|
|||||||
private String lname;
|
private String lname;
|
||||||
private String fname;
|
private String fname;
|
||||||
Categorie categorie;
|
Categorie categorie;
|
||||||
Long club;
|
String club_uuid;
|
||||||
String club_str;
|
String club_str;
|
||||||
Genre genre;
|
Genre genre;
|
||||||
String country;
|
String country;
|
||||||
@@ -29,7 +29,7 @@ public class CombEntity {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
|
return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
|
||||||
model.getClub() == null ? null : model.getClub().getId(),
|
model.getClub() == null ? null : model.getClub().getClubId(),
|
||||||
model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(),
|
model.getClub() == null ? "Sans club" : model.getClub().getName(), model.getGenre(), model.getCountry(),
|
||||||
0, null);
|
0, null);
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ public class CombEntity {
|
|||||||
MembreModel model = registerModel.getMembre();
|
MembreModel model = registerModel.getMembre();
|
||||||
|
|
||||||
return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(),
|
return new CombEntity(model.getId(), model.getLname(), model.getFname(), registerModel.getCategorie(),
|
||||||
registerModel.getClub2() == null ? null : registerModel.getClub2().getId(),
|
registerModel.getClub2() == null ? null : registerModel.getClub2().getClubId(),
|
||||||
registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(),
|
registerModel.getClub2() == null ? "Sans club" : registerModel.getClub2().getName(), model.getGenre(),
|
||||||
model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight());
|
model.getCountry(), registerModel.getOverCategory(), registerModel.getWeight());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public class CompetPermService {
|
|||||||
CompletableFuture<SimpleCompet> f = new CompletableFuture<>();
|
CompletableFuture<SimpleCompet> f = new CompletableFuture<>();
|
||||||
SReqCompet.getConfig(serverCustom.clients, id, f);
|
SReqCompet.getConfig(serverCustom.clients, id, f);
|
||||||
try {
|
try {
|
||||||
return f.get(1500, TimeUnit.MILLISECONDS);
|
return f.get(500, TimeUnit.MILLISECONDS);
|
||||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,8 @@ public class CompetPermService {
|
|||||||
.chain(competitionModels -> {
|
.chain(competitionModels -> {
|
||||||
CompletableFuture<HashMap<String, String>> f = new CompletableFuture<>();
|
CompletableFuture<HashMap<String, String>> f = new CompletableFuture<>();
|
||||||
SReqCompet.getAllHaveAccess(serverCustom.clients, securityCtx.getSubject(), f);
|
SReqCompet.getAllHaveAccess(serverCustom.clients, securityCtx.getSubject(), f);
|
||||||
return Uni.createFrom().future(f, Duration.ofMillis(1500))
|
return Uni.createFrom().future(f, Duration.ofMillis(500))
|
||||||
|
.onFailure().recoverWithItem(new HashMap<>())
|
||||||
.map(map_ -> {
|
.map(map_ -> {
|
||||||
HashMap<Long, String> map = new HashMap<>();
|
HashMap<Long, String> map = new HashMap<>();
|
||||||
map_.forEach((key, value) -> map.put(Long.parseLong(key), value));
|
map_.forEach((key, value) -> map.put(Long.parseLong(key), value));
|
||||||
|
|||||||
@@ -162,6 +162,14 @@ public class CompetitionService {
|
|||||||
.map(pouleModels -> pouleModels.stream().map(CompetitionData::fromModel).toList());
|
.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) {
|
public Uni<CompetitionData> addOrUpdate(SecurityCtx securityCtx, CompetitionData data) {
|
||||||
if (data.getId() == null) {
|
if (data.getId() == null) {
|
||||||
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
|
return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult()
|
||||||
@@ -496,6 +504,10 @@ public class CompetitionService {
|
|||||||
.andCollectFailures()))
|
.andCollectFailures()))
|
||||||
.call(competitionModel -> Panache.withTransaction(
|
.call(competitionModel -> Panache.withTransaction(
|
||||||
() -> categoryRepository.delete("compet = ?1", competitionModel)))
|
() -> 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())))
|
.chain(model -> Panache.withTransaction(() -> repository.delete("id", model.getId())))
|
||||||
.invoke(o -> SReqCompet.rmCompet(serverCustom.clients, id))
|
.invoke(o -> SReqCompet.rmCompet(serverCustom.clients, id))
|
||||||
.call(__ -> cache.invalidate(id));
|
.call(__ -> cache.invalidate(id));
|
||||||
|
|||||||
@@ -186,6 +186,6 @@ public class AffiliationRequestEndpoints {
|
|||||||
public Uni<Response> getStatus(
|
public Uni<Response> getStatus(
|
||||||
@Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id) throws URISyntaxException {
|
@Parameter(description = "L'identifiant de la demande d'affiliation") @PathParam("id") long id) throws URISyntaxException {
|
||||||
return Utils.getMediaFile(id, media, "aff_request/status", "affiliation_request_" + id,
|
return Utils.getMediaFile(id, media, "aff_request/status", "affiliation_request_" + id,
|
||||||
Uni.createFrom().nullItem());
|
Uni.createFrom().nullItem(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ public class ClubEndpoints {
|
|||||||
@Parameter(description = "Identifiant long (clubId) de club") @PathParam("clubId") String clubId) {
|
@Parameter(description = "Identifiant long (clubId) de club") @PathParam("clubId") String clubId) {
|
||||||
return clubService.getByClubId(clubId).chain(Unchecked.function(clubModel -> {
|
return clubService.getByClubId(clubId).chain(Unchecked.function(clubModel -> {
|
||||||
try {
|
try {
|
||||||
return Utils.getMediaFile((clubModel != null) ? clubModel.getId() : -1, media, "ppClub",
|
return Utils.getMediaFileNoDefault((clubModel != null) ? clubModel.getId() : -1, media, "ppClub",
|
||||||
Uni.createFrom().nullItem());
|
Uni.createFrom().nullItem());
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
throw new InternalError();
|
throw new InternalError();
|
||||||
@@ -358,7 +358,7 @@ public class ClubEndpoints {
|
|||||||
return clubService.getById(id).onItem().invoke(checkPerm).chain(Unchecked.function(clubModel -> {
|
return clubService.getById(id).onItem().invoke(checkPerm).chain(Unchecked.function(clubModel -> {
|
||||||
try {
|
try {
|
||||||
return Utils.getMediaFile(clubModel.getId(), media, "clubStatus",
|
return Utils.getMediaFile(clubModel.getId(), media, "clubStatus",
|
||||||
"statue-" + clubModel.getName(), Uni.createFrom().nullItem());
|
"statue-" + clubModel.getName(), Uni.createFrom().nullItem(), false);
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
throw new InternalError();
|
throw new InternalError();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,4 +47,12 @@ public class CompetitionAdminEndpoints {
|
|||||||
public Uni<List<CompetitionData>> getAllSystemAdmin(@PathParam("system") CompetitionSystem system) {
|
public Uni<List<CompetitionData>> getAllSystemAdmin(@PathParam("system") CompetitionSystem system) {
|
||||||
return service.getAllSystemAdmin(securityCtx, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,11 +150,16 @@ public class Utils {
|
|||||||
|
|
||||||
public static Uni<Response> getMediaFile(long id, String media, String dirname,
|
public static Uni<Response> getMediaFile(long id, String media, String dirname,
|
||||||
Uni<?> uniBase) throws URISyntaxException {
|
Uni<?> uniBase) throws URISyntaxException {
|
||||||
return getMediaFile(id, media, dirname, null, uniBase);
|
return getMediaFile(id, media, dirname, null, uniBase, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uni<Response> getMediaFileNoDefault(long id, String media, String dirname,
|
||||||
|
Uni<?> uniBase) throws URISyntaxException {
|
||||||
|
return getMediaFile(id, media, dirname, null, uniBase, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uni<Response> getMediaFile(long id, String media, String dirname, String out_filename,
|
public static Uni<Response> getMediaFile(long id, String media, String dirname, String out_filename,
|
||||||
Uni<?> uniBase) throws URISyntaxException {
|
Uni<?> uniBase, boolean default_) throws URISyntaxException {
|
||||||
Future<Pair<File, byte[]>> future = CompletableFuture.supplyAsync(() -> {
|
Future<Pair<File, byte[]>> future = CompletableFuture.supplyAsync(() -> {
|
||||||
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
|
FilenameFilter filter = (directory, filename) -> filename.startsWith(id + ".");
|
||||||
File[] files = new File(media, dirname).listFiles(filter);
|
File[] files = new File(media, dirname).listFiles(filter);
|
||||||
@@ -182,19 +187,25 @@ public class Utils {
|
|||||||
return uniBase.chain(__ -> Uni.createFrom().future(future)
|
return uniBase.chain(__ -> Uni.createFrom().future(future)
|
||||||
.chain(filePair -> {
|
.chain(filePair -> {
|
||||||
if (filePair == null) {
|
if (filePair == null) {
|
||||||
return Uni.createFrom().future(future2).map(data -> {
|
if (default_) {
|
||||||
if (data == null)
|
return Uni.createFrom().future(future2).map(data -> {
|
||||||
return Response.noContent().build();
|
if (data == null)
|
||||||
|
return Response.noContent().build();
|
||||||
|
|
||||||
String mimeType = "image/apng";
|
String mimeType = "image/apng";
|
||||||
Response.ResponseBuilder resp = Response.ok(data);
|
Response.ResponseBuilder resp = Response.ok(data);
|
||||||
resp.type(MediaType.APPLICATION_OCTET_STREAM);
|
resp.type(MediaType.APPLICATION_OCTET_STREAM);
|
||||||
resp.header(HttpHeaders.CONTENT_LENGTH, data.length);
|
resp.header(HttpHeaders.CONTENT_LENGTH, data.length);
|
||||||
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
|
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
|
||||||
resp.header(HttpHeaders.CONTENT_DISPOSITION,
|
resp.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
"inline; " + ((out_filename == null) ? "" : "filename=\"" + out_filename + "\""));
|
"inline; " + ((out_filename == null) ? "" : "filename=\"" + out_filename + "\""));
|
||||||
return resp.build();
|
return resp.build();
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
Response.ResponseBuilder resp = Response.status(404);
|
||||||
|
resp.header(HttpHeaders.CACHE_CONTROL, "max-age=600");
|
||||||
|
return Uni.createFrom().item(resp.build());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Uni.createFrom().item(() -> {
|
return Uni.createFrom().item(() -> {
|
||||||
String mimeType = URLConnection.guessContentTypeFromName(filePair.getKey().getName());
|
String mimeType = URLConnection.guessContentTypeFromName(filePair.getKey().getName());
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import fr.titionfire.ffsaf.domain.service.CompetPermService;
|
|||||||
import fr.titionfire.ffsaf.net2.MessageType;
|
import fr.titionfire.ffsaf.net2.MessageType;
|
||||||
import fr.titionfire.ffsaf.utils.SecurityCtx;
|
import fr.titionfire.ffsaf.utils.SecurityCtx;
|
||||||
import fr.titionfire.ffsaf.ws.data.WelcomeInfo;
|
import fr.titionfire.ffsaf.ws.data.WelcomeInfo;
|
||||||
import fr.titionfire.ffsaf.ws.recv.RCategorie;
|
import fr.titionfire.ffsaf.ws.recv.*;
|
||||||
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 fr.titionfire.ffsaf.ws.send.JsonUni;
|
||||||
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
import io.quarkus.hibernate.reactive.panache.common.WithSession;
|
||||||
import io.quarkus.security.Authenticated;
|
import io.quarkus.security.Authenticated;
|
||||||
@@ -44,6 +41,9 @@ public class CompetitionWS {
|
|||||||
@Inject
|
@Inject
|
||||||
RRegister rRegister;
|
RRegister rRegister;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
RCardboard rCardboard;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
SecurityCtx securityCtx;
|
SecurityCtx securityCtx;
|
||||||
|
|
||||||
@@ -77,6 +77,7 @@ public class CompetitionWS {
|
|||||||
getWSReceiverMethods(RMatch.class, rMatch);
|
getWSReceiverMethods(RMatch.class, rMatch);
|
||||||
getWSReceiverMethods(RCategorie.class, rCategorie);
|
getWSReceiverMethods(RCategorie.class, rCategorie);
|
||||||
getWSReceiverMethods(RRegister.class, rRegister);
|
getWSReceiverMethods(RRegister.class, rRegister);
|
||||||
|
getWSReceiverMethods(RCardboard.class, rCardboard);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnOpen
|
@OnOpen
|
||||||
|
|||||||
128
src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java
Normal file
128
src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ public class RRegister {
|
|||||||
@Inject
|
@Inject
|
||||||
CompetitionRepository competitionRepository;
|
CompetitionRepository competitionRepository;
|
||||||
|
|
||||||
@WSReceiver(code = "getRegister", permission = PermLevel.ADMIN)
|
@WSReceiver(code = "getRegister", permission = PermLevel.TABLE)
|
||||||
public Uni<List<CombEntity>> getRegister(WebSocketConnection connection, Object o) {
|
public Uni<List<CombEntity>> getRegister(WebSocketConnection connection, Object o) {
|
||||||
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
return competitionRepository.find("uuid", connection.pathParam("uuid")).firstResult()
|
||||||
.call(cm -> Mutiny.fetch(cm.getInsc()))
|
.call(cm -> Mutiny.fetch(cm.getInsc()))
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class SRegister {
|
|||||||
public Uni<Void> send(String uuid, String code, Object data) {
|
public Uni<Void> send(String uuid, String code, Object data) {
|
||||||
List<Uni<Void>> queue = connections.findByEndpointId(CompetitionWS.class.getCanonicalName()).stream()
|
List<Uni<Void>> queue = connections.findByEndpointId(CompetitionWS.class.getCanonicalName()).stream()
|
||||||
.filter(c -> c.pathParam("uuid").equals(uuid) && PermLevel.valueOf(
|
.filter(c -> c.pathParam("uuid").equals(uuid) && PermLevel.valueOf(
|
||||||
c.userData().get(UserData.TypedKey.forString("prem"))).ordinal() >= PermLevel.ADMIN.ordinal())
|
c.userData().get(UserData.TypedKey.forString("prem"))).ordinal() >= PermLevel.TABLE.ordinal())
|
||||||
.map(c -> c.sendText(
|
.map(c -> c.sendText(
|
||||||
new MessageOut(UUID.randomUUID(), code, MessageType.NOTIFY, data)))
|
new MessageOut(UUID.randomUUID(), code, MessageType.NOTIFY, data)))
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
13
src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java
Normal file
13
src/main/java/fr/titionfire/ffsaf/ws/send/SSCardboard.java
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main/webapp/src/assets/SimpleIconsScore.ts
Normal file
26
src/main/webapp/src/assets/SimpleIconsScore.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
IconDefinition,
|
||||||
|
IconName,
|
||||||
|
IconPrefix
|
||||||
|
} from "@fortawesome/fontawesome-svg-core";
|
||||||
|
|
||||||
|
export const SimpleIconsScore: IconDefinition = {
|
||||||
|
icon: [
|
||||||
|
// SVG viewbox width (in pixels)
|
||||||
|
122.88,
|
||||||
|
|
||||||
|
// SVG viewbox height (in pixels)
|
||||||
|
100.08,
|
||||||
|
|
||||||
|
// Aliases (not needed)
|
||||||
|
[],
|
||||||
|
|
||||||
|
// Unicode as hex value (not needed)
|
||||||
|
"",
|
||||||
|
|
||||||
|
// SVG path data
|
||||||
|
"M5.49,0h55.95h55.95c1.51,0,2.89,0.62,3.88,1.61c0.99,0.99,1.61,2.37,1.61,3.88v75.79c0,1.51-0.62,2.89-1.61,3.88 c-0.99,0.99-2.37,1.61-3.88,1.61h-25v12.15c0,0.64-0.52,1.16-1.16,1.16H31.66c-0.65,0-1.17-0.53-1.17-1.17V86.77h-25 c-1.51,0-2.89-0.62-3.88-1.61C0.62,84.17,0,82.8,0,81.28V5.49C0,3.98,0.62,2.6,1.61,1.61C2.6,0.62,3.98,0,5.49,0L5.49,0z M45.45,37.11v13.88c0,3.16-0.18,5.45-0.52,6.89c-0.34,1.45-1.05,2.79-2.13,4.05c-1.08,1.25-2.38,2.15-3.9,2.69 c-1.52,0.55-3.22,0.82-5.11,0.82c-2.48,0-4.54-0.29-6.19-0.86c-1.64-0.58-2.95-1.47-3.93-2.68c-0.97-1.22-1.67-2.5-2.08-3.84 c-0.41-1.35-0.61-3.49-0.61-6.42V37.11c0-3.83,0.33-6.69,0.99-8.59c0.66-1.9,1.97-3.43,3.93-4.58c1.96-1.15,4.33-1.72,7.12-1.72 c2.28,0,4.32,0.39,6.12,1.19c1.8,0.8,3.14,1.77,4.03,2.92c0.89,1.15,1.5,2.44,1.82,3.88C45.29,31.66,45.45,33.95,45.45,37.11 L45.45,37.11z M35.08,33.63c0-2.21-0.11-3.59-0.32-4.15c-0.21-0.55-0.71-0.83-1.51-0.83c-0.77,0-1.28,0.3-1.53,0.89 c-0.25,0.59-0.38,1.96-0.38,4.1v20.29c0,2.41,0.11,3.87,0.35,4.36c0.23,0.5,0.73,0.75,1.51,0.75c0.77,0,1.27-0.29,1.52-0.88 c0.24-0.58,0.36-1.89,0.36-3.92V33.63L35.08,33.63z M98.87,23.01v41.64H88.49V42.3c0-3.23-0.07-5.18-0.23-5.83 c-0.15-0.65-0.58-1.15-1.27-1.48c-0.69-0.33-2.24-0.5-4.63-0.5h-1.03v-4.83c5.02-1.07,8.83-3.29,11.42-6.64H98.87L98.87,23.01z M64.96,7.05v72.68h50.87V7.05H64.96L64.96,7.05z M57.92,79.73V7.05H7.05v72.68H57.92L57.92,79.73z"
|
||||||
|
],
|
||||||
|
iconName: "simple-icons-score" as IconName,
|
||||||
|
prefix: "simple-icons" as IconPrefix
|
||||||
|
};
|
||||||
@@ -43,9 +43,9 @@ function AffiliationMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CompMenu() {
|
function CompMenu() {
|
||||||
const {is_authenticated} = useAuth()
|
const {is_authenticated, userinfo} = useAuth()
|
||||||
|
|
||||||
if (!is_authenticated)
|
if (!is_authenticated || !userinfo?.roles?.includes("federation_admin"))
|
||||||
return <></>
|
return <></>
|
||||||
|
|
||||||
return <li className="nav-item dropdown">
|
return <li className="nav-item dropdown">
|
||||||
|
|||||||
119
src/main/webapp/src/components/SmartLogoBackground.jsx
Normal file
119
src/main/webapp/src/components/SmartLogoBackground.jsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import {useEffect, useRef, useState, memo} from 'react';
|
||||||
|
|
||||||
|
const cache = {};
|
||||||
|
|
||||||
|
export function SmartLogoBackground({
|
||||||
|
src,
|
||||||
|
darkBackground = '#333333',
|
||||||
|
lightBackground = '#f0f0f0',
|
||||||
|
defaultBackground = 'transparent',
|
||||||
|
tolerance = 55,
|
||||||
|
minPixels = 10, // Seuil minimal de pixels détectés pour appliquer un fond
|
||||||
|
alt = 'Logo',
|
||||||
|
style = {},
|
||||||
|
imgClassName = '',
|
||||||
|
}) {
|
||||||
|
const canvasRef = useRef(null);
|
||||||
|
const [background, setBackground] = useState(defaultBackground);
|
||||||
|
const [load, setLoad] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cache[src]) {
|
||||||
|
setBackground(cache[src]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!load)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.crossOrigin = 'Anonymous';
|
||||||
|
img.src = src;
|
||||||
|
|
||||||
|
// Prevent error logging
|
||||||
|
img.onerror = function () {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const pixels = imageData.data;
|
||||||
|
let lightBorderCount = 0;
|
||||||
|
let darkBorderCount = 0;
|
||||||
|
|
||||||
|
const isTransparent = (i) => pixels[i + 3] === 0;
|
||||||
|
const isLightColor = (i) => {
|
||||||
|
const r = pixels[i];
|
||||||
|
const g = pixels[i + 1];
|
||||||
|
const b = pixels[i + 2];
|
||||||
|
const a = pixels[i + 3];
|
||||||
|
return r > 255 - tolerance && g > 255 - tolerance && b > 255 - tolerance && a > 128;
|
||||||
|
};
|
||||||
|
const isDarkColor = (i) => {
|
||||||
|
const r = pixels[i];
|
||||||
|
const g = pixels[i + 1];
|
||||||
|
const b = pixels[i + 2];
|
||||||
|
const a = pixels[i + 3];
|
||||||
|
return r < tolerance && g < tolerance && b < tolerance && a > 128;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parcourir une ligne/colonne sur 3
|
||||||
|
for (let y = 0; y < canvas.height; y += Math.max(canvas.height / 500, 1)) {
|
||||||
|
for (let x = 0; x < canvas.width; x += Math.max(canvas.width / 500, 1)) {
|
||||||
|
const i = Math.round(y * canvas.width + x) * 4;
|
||||||
|
if (isTransparent(i)) {
|
||||||
|
const neighbors = [
|
||||||
|
i - 4, // Haut
|
||||||
|
i + 4, // Bas
|
||||||
|
i - 4 * canvas.width, // Gauche
|
||||||
|
i + 4 * canvas.width, // Droite
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const neighbor of neighbors) {
|
||||||
|
if (neighbor >= 0 && neighbor < pixels.length) {
|
||||||
|
if (isLightColor(neighbor)) lightBorderCount++;
|
||||||
|
if (isDarkColor(neighbor)) darkBorderCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (lightBorderCount >= 25 || darkBorderCount >= 25)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) {
|
||||||
|
cache[src] = darkBackground;
|
||||||
|
setBackground(darkBackground)
|
||||||
|
} else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) {
|
||||||
|
cache[src] = lightBackground;
|
||||||
|
setBackground(lightBackground)
|
||||||
|
} else {
|
||||||
|
cache[src] = defaultBackground;
|
||||||
|
setBackground(defaultBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [src, darkBackground, lightBackground, defaultBackground, tolerance, minPixels, load]);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<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"
|
||||||
|
setLoad(true)
|
||||||
|
}}/>
|
||||||
|
<canvas ref={canvasRef} style={{display: 'none'}}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SmartLogoBackgroundMemo = memo(SmartLogoBackground);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {createContext, useContext, useReducer} from "react";
|
import {createContext, useContext, useEffect, useReducer} from "react";
|
||||||
|
import {useWS} from "./useWS.jsx";
|
||||||
|
|
||||||
const CombsContext = createContext({});
|
const CombsContext = createContext({});
|
||||||
const CombsDispatchContext = createContext(() => {
|
const CombsDispatchContext = createContext(() => {
|
||||||
@@ -16,11 +17,18 @@ function compareCombs(a, b) {
|
|||||||
function reducer(state, action) {
|
function reducer(state, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_COMB':
|
case 'SET_COMB':
|
||||||
if (state[action.payload.id] === undefined || !compareCombs(state[action.payload.id], action.payload)) {
|
const comb = (action.payload.source === "register") ? action.payload.data : ({
|
||||||
console.debug("Updating comb", action.payload);
|
id: action.payload.data.id,
|
||||||
|
fname: action.payload.data.fname,
|
||||||
|
lname: action.payload.data.lname,
|
||||||
|
genre: action.payload.data.genre,
|
||||||
|
country: action.payload.data.country,
|
||||||
|
})
|
||||||
|
if (state[comb.id] === undefined || !compareCombs(comb, state[comb.id])) {
|
||||||
|
console.debug("Updating comb", comb);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[action.payload.id]: action.payload.value
|
[comb.id]: comb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
@@ -58,12 +66,30 @@ function reducer(state, action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function WSListener({dispatch}) {
|
||||||
|
const {dispatch: dispatchWS} = useWS()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sendRegister = ({data}) => {
|
||||||
|
dispatch({type: 'SET_ALL', payload: {source: "register", data: data}});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchWS({type: 'addListener', payload: {callback: sendRegister, code: 'sendRegister'}})
|
||||||
|
return () => {
|
||||||
|
dispatchWS({type: 'removeListener', payload: {callback: sendRegister, code: 'sendRegister'}})
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
export function CombsProvider({children}) {
|
export function CombsProvider({children}) {
|
||||||
const [combs, dispatch] = useReducer(reducer, {})
|
const [combs, dispatch] = useReducer(reducer, {})
|
||||||
|
|
||||||
return <CombsContext.Provider value={combs}>
|
return <CombsContext.Provider value={combs}>
|
||||||
<CombsDispatchContext.Provider value={dispatch}>
|
<CombsDispatchContext.Provider value={dispatch}>
|
||||||
{children}
|
{children}
|
||||||
|
<WSListener dispatch={dispatch}/>
|
||||||
</CombsDispatchContext.Provider>
|
</CombsDispatchContext.Provider>
|
||||||
</CombsContext.Provider>
|
</CombsContext.Provider>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {createContext, useContext, useReducer} from "react";
|
import {createContext, useContext, useReducer} from "react";
|
||||||
|
|
||||||
const PubAffContext = createContext({next: [], c1: undefined, c2: undefined});
|
const PubAffContext = createContext({next: [], c1: undefined, c2: undefined, showScore: true, timeCb: undefined, scoreRouge: 0, scoreBleu: 0});
|
||||||
const PubAffDispatchContext = createContext(() => {
|
const PubAffDispatchContext = createContext(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -8,6 +8,12 @@ function reducer(state, action) {
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_DATA':
|
case 'SET_DATA':
|
||||||
return {...state, ...action.payload}
|
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:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {ContactEditor} from "../../../components/Club/ContactEditor.jsx";
|
|||||||
import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
|
import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {SmartLogoBackground} from "../../../components/SmartLogoBackground.jsx";
|
||||||
|
|
||||||
const vite_url = import.meta.env.VITE_URL;
|
const vite_url = import.meta.env.VITE_URL;
|
||||||
|
|
||||||
@@ -109,10 +110,10 @@ function InformationForm({data}) {
|
|||||||
<TextField name="name" text="Nom" value={data.name}/>
|
<TextField name="name" text="Nom" value={data.name}/>
|
||||||
<CountryList name="country" text="Pays" value={data.country}/>
|
<CountryList name="country" text="Pays" value={data.country}/>
|
||||||
|
|
||||||
<img
|
<SmartLogoBackground
|
||||||
src={`${vite_url}/api/club/${data.clubId}/logo`}
|
src={`${vite_url}/api/club/${data.clubId}/logo`}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
className="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/>
|
imgClassName="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<label className="input-group-text" htmlFor="logo">Blason</label>
|
<label className="input-group-text" htmlFor="logo">Blason</label>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {ContactEditor} from "../../../components/Club/ContactEditor.jsx";
|
|||||||
import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
|
import {HoraireEditor} from "../../../components/Club/HoraireEditor.jsx";
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
import {faFilePdf} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {SmartLogoBackground} from "../../../components/SmartLogoBackground.jsx";
|
||||||
|
|
||||||
const vite_url = import.meta.env.VITE_URL;
|
const vite_url = import.meta.env.VITE_URL;
|
||||||
|
|
||||||
@@ -80,10 +81,10 @@ function InformationForm({data}) {
|
|||||||
|
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<img
|
<SmartLogoBackground
|
||||||
src={`${vite_url}/api/club/${data.clubId}/logo`}
|
src={`${vite_url}/api/club/${data.clubId}/logo`}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
className="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/>
|
ingClassName="img-fluid" style={{object_fit: 'contain', maxHeight: '15em'}}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<a href={`${vite_url}/api/club/${data.id}/status`} target='_blank'>
|
<a href={`${vite_url}/api/club/${data.id}/status`} target='_blank'>
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export function CompetitionEdit() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh(`/competition/${id}?light=false`)
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<button type="button" className="btn btn-link" onClick={() => navigate("/competition")}>
|
<button type="button" className="btn btn-link" onClick={() => navigate("/competition")}>
|
||||||
« retour
|
« retour
|
||||||
@@ -284,17 +288,18 @@ function Content({data}) {
|
|||||||
toast.promise(
|
toast.promise(
|
||||||
apiAxios.post(`/competition`, out),
|
apiAxios.post(`/competition`, out),
|
||||||
{
|
{
|
||||||
pending: "Enregistrement du club en cours",
|
pending: "Enregistrement de la competition en cours",
|
||||||
success: "Club enregistrée avec succès 🎉",
|
success: "Competition enregistrée avec succès 🎉",
|
||||||
error: {
|
error: {
|
||||||
render({data}) {
|
render({data}) {
|
||||||
return errFormater(data, "Échec de l'enregistrement du club")
|
return errFormater(data, "Échec de l'enregistrement de la competition")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
).then(data => {
|
).then(data => {
|
||||||
if (data.id !== undefined)
|
console.log(data.data)
|
||||||
navigate("/competition/" + data.id)
|
if (data.data.id !== undefined)
|
||||||
|
navigate("/competition/" + data.data.id)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +320,7 @@ function Content({data}) {
|
|||||||
<div id="collapseOne" className="accordion-collapse collapse" data-bs-parent="#accordionExample">
|
<div id="collapseOne" className="accordion-collapse collapse" data-bs-parent="#accordionExample">
|
||||||
<div className="accordion-body">
|
<div className="accordion-body">
|
||||||
<TextField name="uuid" text="UUID" value={data.uuid} disabled={true}/>
|
<TextField name="uuid" text="UUID" value={data.uuid} disabled={true}/>
|
||||||
<OptionField name="system" text="System" value={data.system} values={{SAFCA: 'SAFCA', NONE: "intranet"}}
|
<OptionField name="system" text="System" value={data.system} values={{SAFCA: 'SAFCA', INTERNAL: "Intranet"}}
|
||||||
disabled={data.id !== null}/>
|
disabled={data.id !== null}/>
|
||||||
{data.id !== null &&
|
{data.id !== null &&
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@@ -383,11 +388,11 @@ function Content({data}) {
|
|||||||
<span className="input-group-text" id="startRegister">Du</span>
|
<span className="input-group-text" id="startRegister">Du</span>
|
||||||
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="date"
|
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="date"
|
||||||
name="startRegister" aria-describedby="startRegister"
|
name="startRegister" aria-describedby="startRegister"
|
||||||
defaultValue={data.startRegister ? data.startRegister.split('+')[0] : ''}/>
|
defaultValue={data.startRegister ? data.startRegister.substring(0, 16) : ''}/>
|
||||||
<span className="input-group-text" id="endRegister">Au</span>
|
<span className="input-group-text" id="endRegister">Au</span>
|
||||||
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="endRegister"
|
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="endRegister"
|
||||||
name="endRegister" aria-describedby="endRegister"
|
name="endRegister" aria-describedby="endRegister"
|
||||||
defaultValue={data.endRegister ? data.endRegister.split('+')[0] : ''}/>
|
defaultValue={data.endRegister ? data.endRegister.substring(0, 16) : ''}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{display: registerMode === "HELLOASSO" ? "initial" : "none"}}>
|
<div style={{display: registerMode === "HELLOASSO" ? "initial" : "none"}}>
|
||||||
|
|||||||
183
src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx
Normal file
183
src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.btn-xs {
|
||||||
|
--bs-btn-padding-y: .05rem;
|
||||||
|
--bs-btn-padding-x: .6rem;
|
||||||
|
--bs-btn-font-size: .75rem;
|
||||||
|
}
|
||||||
626
src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx
Normal file
626
src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx
Normal file
@@ -0,0 +1,626 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
51
src/main/webapp/src/pages/competition/editor/CMTPoint.jsx
Normal file
51
src/main/webapp/src/pages/competition/editor/CMTPoint.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
@@ -1,46 +1,52 @@
|
|||||||
import React, {useEffect, useReducer, useRef, useState} from "react";
|
import React, {useEffect, useRef, useState} from "react";
|
||||||
import {useRequestWS, useWS} from "../../../hooks/useWS.jsx";
|
import {useRequestWS} from "../../../hooks/useWS.jsx";
|
||||||
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
import {useCombsDispatch} from "../../../hooks/useComb.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 {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 {createPortal} from "react-dom";
|
||||||
import {copyStyles} from "../../../utils/copyStyles.js";
|
import {copyStyles} from "../../../utils/copyStyles.js";
|
||||||
import {PubAffProvider, usePubAffDispatch, usePubAffState} from "../../../hooks/useExternalWindow.jsx";
|
import {PubAffProvider, usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||||
import {faDisplay} from "@fortawesome/free-solid-svg-icons";
|
import {faArrowRightArrowLeft, faDisplay} from "@fortawesome/free-solid-svg-icons";
|
||||||
import {PubAffWindow} from "./PubAffWindow.jsx";
|
import {PubAffWindow} from "./PubAffWindow.jsx";
|
||||||
|
import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts";
|
||||||
function CupImg() {
|
import {ChronoPanel} from "./CMTChronoPanel.jsx";
|
||||||
return <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
|
import {CategorieSelect} from "./CMTMatchPanel.jsx";
|
||||||
style={{width: "16px"}} src="/img/171891.png"
|
import {PointPanel} from "./CMTPoint.jsx";
|
||||||
alt=""/>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CMTable() {
|
export function CMTable() {
|
||||||
|
const combDispatch = useCombsDispatch()
|
||||||
const [catId, setCatId] = useState(-1);
|
const [catId, setCatId] = useState(-1);
|
||||||
const menuAction = useRef({});
|
const menuActions = useRef({});
|
||||||
|
const {data} = useRequestWS("getRegister", null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data === null)
|
||||||
|
return;
|
||||||
|
combDispatch({type: 'SET_ALL', payload: {source: "register", data: data}});
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return <PubAffProvider>
|
return <PubAffProvider>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-12 col-lg">
|
<div className="col-md-12 col-lg">
|
||||||
<div style={{backgroundColor: "#00c700"}}>
|
<div className="card mb-3">
|
||||||
A
|
<div className="card-header">Chronomètre</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<ChronoPanel/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{backgroundColor: "#0099c7"}}>
|
|
||||||
B
|
<div className="card mb-3">
|
||||||
|
<div className="card-header">Score</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<PointPanel menuActions={menuActions}/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-12 col-xl-6 col-xxl-5">
|
<div className="col-md-12 col-xl-6 col-xxl-5">
|
||||||
<div className="card">
|
<div className="card mb-3">
|
||||||
<div className="card-header">Matches</div>
|
<div className="card-header">Matches</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<CategorieSelect catId={catId} setCatId={setCatId} menuAction={menuAction}/>
|
<CategorieSelect catId={catId} setCatId={setCatId} menuActions={menuActions}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{backgroundColor: "#c70000"}}>
|
<div style={{backgroundColor: "#c70000"}}>
|
||||||
@@ -48,16 +54,20 @@ export function CMTable() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Menu menuAction={menuAction}/>
|
<Menu menuActions={menuActions}/>
|
||||||
</div>
|
</div>
|
||||||
</PubAffProvider>
|
</PubAffProvider>
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowName = "FFSAFScorePublicWindow";
|
const windowName = "FFSAFScorePublicWindow";
|
||||||
|
|
||||||
function Menu({menuAction}) {
|
let tto = [];
|
||||||
|
|
||||||
|
function Menu({menuActions}) {
|
||||||
const e = document.getElementById("actionMenu")
|
const e = document.getElementById("actionMenu")
|
||||||
|
const publicAffDispatch = usePubAffDispatch()
|
||||||
const [showPubAff, setShowPubAff] = useState(false)
|
const [showPubAff, setShowPubAff] = useState(false)
|
||||||
|
const [showScore, setShowScore] = useState(true)
|
||||||
|
|
||||||
const externalWindow = useRef(null)
|
const externalWindow = useRef(null)
|
||||||
const containerEl = useRef(document.createElement("div"))
|
const containerEl = useRef(document.createElement("div"))
|
||||||
@@ -84,6 +94,7 @@ function Menu({menuAction}) {
|
|||||||
setShowPubAff(false);
|
setShowPubAff(false);
|
||||||
externalWindow.current.close();
|
externalWindow.current.close();
|
||||||
externalWindow.current = null;
|
externalWindow.current = null;
|
||||||
|
publicAffDispatch({type: 'CLEAR_CB_TIME', payload: null});
|
||||||
sessionStorage.removeItem(windowName + "_open");
|
sessionStorage.removeItem(windowName + "_open");
|
||||||
});
|
});
|
||||||
setShowPubAff(true);
|
setShowPubAff(true);
|
||||||
@@ -93,483 +104,38 @@ function Menu({menuAction}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
if (!e)
|
||||||
return <></>;
|
return <></>;
|
||||||
return <>
|
return <>
|
||||||
{createPortal(
|
{createPortal(
|
||||||
<>
|
<>
|
||||||
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
||||||
<FontAwesomeIcon icon={faDisplay} size="xl" style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer"}}
|
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer"}} onClick={handleSwitchScore}
|
||||||
onClick={handlePubAff}/>
|
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"/>
|
||||||
|
<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"/>
|
||||||
</>, document.getElementById("actionMenu"))}
|
</>, document.getElementById("actionMenu"))}
|
||||||
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
|
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
function CategorieSelect({catId, setCatId, menuAction}) {
|
|
||||||
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} menuAction={menuAction}/>}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
function MatchPanel({catId, cat, menuAction}) {
|
|
||||||
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} menuAction={menuAction}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListMatch({cat, matches, trees, menuAction}) {
|
|
||||||
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 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.id !== activeMatch).map(m => ({c1: m.c1, c2: m.c2}))}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [match]);
|
|
||||||
//useEffect(() => {
|
|
||||||
// if (activeMatch !== null)
|
|
||||||
// setActiveMatch(null);
|
|
||||||
//}, [cat])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (marches2.length === 0)
|
|
||||||
return;
|
|
||||||
if (marches2.some(m => m.id === activeMatch))
|
|
||||||
return;
|
|
||||||
|
|
||||||
setActiveMatch(marches2.find(m => !m.end)?.id);
|
|
||||||
}, [matches])
|
|
||||||
|
|
||||||
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
|
|
||||||
return <>
|
|
||||||
<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" : ""} 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>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import {Route, Routes, useNavigate, useParams} from "react-router-dom";
|
import {Route, Routes, useNavigate, useParams} from "react-router-dom";
|
||||||
import {LoadingProvider} from "../../../hooks/useLoading.jsx";
|
import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {useWS, WSProvider} from "../../../hooks/useWS.jsx";
|
import {useWS, WSProvider} from "../../../hooks/useWS.jsx";
|
||||||
import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
|
import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
|
||||||
import {CMAdmin} from "./CMAdmin.jsx";
|
import {CMAdmin} from "./CMAdmin.jsx";
|
||||||
import {CombsProvider} from "../../../hooks/useComb.jsx";
|
import {CombsProvider} from "../../../hooks/useComb.jsx";
|
||||||
import {CMTable} from "./CMTable.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;
|
const vite_url = import.meta.env.VITE_URL;
|
||||||
|
|
||||||
@@ -22,13 +25,33 @@ export default function CompetitionManagerRoot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const nav = useNavigate();
|
const navigate = useNavigate();
|
||||||
return <div>
|
const setLoading = useLoadingSwitcher()
|
||||||
<h2>Home</h2>
|
const {data, error} = useFetch(`/competition/admin/all/INTERNAL/table`, setLoading, 1)
|
||||||
<button onClick={() => nav("d3dc76a6-2058-423a-b34b-6d15d7ae5848")}>Go comp</button>
|
|
||||||
|
return <div className="row">
|
||||||
|
{data
|
||||||
|
? <MakeCentralPanel data={data} navigate={navigate}/>
|
||||||
|
: error
|
||||||
|
? <AxiosError error={error}/>
|
||||||
|
: <Def/>
|
||||||
|
}
|
||||||
</div>
|
</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() {
|
function HomeComp() {
|
||||||
let {compUuid} = useParams();
|
let {compUuid} = useParams();
|
||||||
const [perm, setPerm] = useState("")
|
const [perm, setPerm] = useState("")
|
||||||
@@ -98,11 +121,12 @@ function Home2({perm}) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function Test2() {
|
function Def() {
|
||||||
let {compUuid} = useParams();
|
return <div className="list-group">
|
||||||
const nav = useNavigate();
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
return <div>
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
<h2>Product ID: {compUuid}</h2>
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
<button onClick={() => nav(-1)}>Go Back</button>
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
|
<li className="list-group-item"><ThreeDots/></li>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import {useCombs} from "../../../hooks/useComb.jsx";
|
import {useCombs} from "../../../hooks/useComb.jsx";
|
||||||
import {usePubAffState} from "../../../hooks/useExternalWindow.jsx";
|
import {usePubAffState} from "../../../hooks/useExternalWindow.jsx";
|
||||||
|
import {SmartLogoBackgroundMemo} from "../../../components/SmartLogoBackground.jsx";
|
||||||
|
import {useMemo, useRef} from 'react';
|
||||||
|
|
||||||
|
const vite_url = import.meta.env.VITE_URL;
|
||||||
|
|
||||||
const noMP = {margin: 0, padding: 0};
|
const noMP = {margin: 0, padding: 0};
|
||||||
const redBackground = "radial-gradient(circle, #C80000FF 0%, #000000FF 100%)"
|
const redBackground = "radial-gradient(circle, #C80000FF 0%, #000000FF 100%)"
|
||||||
@@ -9,27 +13,36 @@ const text1Style = {fontSize: "min(2.25vw, 8vh)", fontWeight: "bold", marginLeft
|
|||||||
const text2Style = {fontSize: "min(1.7vw, 7vh)", fontWeight: "bold"};
|
const text2Style = {fontSize: "min(1.7vw, 7vh)", fontWeight: "bold"};
|
||||||
|
|
||||||
export function PubAffWindow({document}) {
|
export function PubAffWindow({document}) {
|
||||||
|
const chronoText = useRef(null)
|
||||||
|
const state2 = useRef({lastColor: "white", lastTimeStr: "--:--"})
|
||||||
const state = usePubAffState();
|
const state = usePubAffState();
|
||||||
|
|
||||||
document.title = "A React portal window"
|
document.title = "A React portal window"
|
||||||
document.body.className = "bg-dark text-white overflow-hidden";
|
document.body.className = "bg-dark text-white overflow-hidden";
|
||||||
|
|
||||||
const showScore = false;
|
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 <>
|
return <>
|
||||||
<div className="row text-center"
|
<div className="row text-center"
|
||||||
style={{background: "linear-gradient(to bottom, #000000, #323232)", height: `calc(100vh - ${combHeight} * 2)`, ...noMP}}>
|
style={{background: "linear-gradient(to bottom, #000000, #323232)", height: `calc(100vh - ${combHeight} * 2)`, ...noMP}}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{fontSize: "30vh", lineHeight: "30vh"}}>01:30</div>
|
<div ref={chronoText}
|
||||||
|
style={{fontSize: "30vh", lineHeight: "30vh", color: state2.current.lastColor}}>{state2.current.lastTimeStr}</div>
|
||||||
{showScore &&
|
{showScore &&
|
||||||
<div className="row" style={noMP}>
|
<div className="row" style={noMP}>
|
||||||
<div className="col-4" style={noMP}>
|
<div className="col-4" style={noMP}>
|
||||||
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#ff1414"}}>0</div>
|
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#ff1414"}}>{state.scoreRouge}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-4" style={noMP}>
|
<div className="col-4" style={noMP}>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-4" style={noMP}>
|
<div className="col-4" style={noMP}>
|
||||||
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#14adff"}}>0</div>
|
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#14adff"}}>{state.scoreBleu}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
@@ -73,8 +86,6 @@ function MatchDisplay({state}) {
|
|||||||
const {getComb} = useCombs();
|
const {getComb} = useCombs();
|
||||||
const combs = state?.next?.slice(1, 6) || [];
|
const combs = state?.next?.slice(1, 6) || [];
|
||||||
|
|
||||||
console.log("Rendering MatchDisplay for", combs);
|
|
||||||
|
|
||||||
return <div className="col-12 position-relative" style={{height: `calc(${combHeight} * 2)`}}>
|
return <div className="col-12 position-relative" style={{height: `calc(${combHeight} * 2)`}}>
|
||||||
<div className="position-absolute bottom-0 start-0" style={{height: "100%", background: redBackground, width: "50vw"}}/>
|
<div className="position-absolute bottom-0 start-0" style={{height: "100%", background: redBackground, width: "50vw"}}/>
|
||||||
<div className="position-absolute bottom-0 start-50" style={{height: "100%", background: blueBackground, width: "50vw"}}/>
|
<div className="position-absolute bottom-0 start-50" style={{height: "100%", background: blueBackground, width: "50vw"}}/>
|
||||||
@@ -99,7 +110,8 @@ function MatchDisplay({state}) {
|
|||||||
<div className="col" style={{fontSize: `3vh`}}>
|
<div className="col" style={{fontSize: `3vh`}}>
|
||||||
{c2.fname} {c2.lname}
|
{c2.fname} {c2.lname}
|
||||||
</div>
|
</div>
|
||||||
{index !== combs.length - 1 && <div className="w-75" style={{...noMP, height: "0.2vh", margin: "0 12.5vw", backgroundColor: "#646464AA"}}/>}
|
{index !== combs.length - 1 &&
|
||||||
|
<div className="w-75" style={{...noMP, height: "0.2vh", margin: "0 12.5vw", backgroundColor: "#646464AA"}}/>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
})}
|
})}
|
||||||
@@ -107,11 +119,18 @@ function MatchDisplay({state}) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoStyle = {width: "6vw", height: "min(11vh, 6vw)", objectFit: "contain", margin: "0 .5vw"};
|
||||||
|
|
||||||
function CombDisplay({combId, background, children}) {
|
function CombDisplay({combId, background, children}) {
|
||||||
const {getComb} = useCombs();
|
const {getComb} = useCombs();
|
||||||
const comb = getComb(combId, "");
|
const comb = getComb(combId, "");
|
||||||
|
|
||||||
//console.log("Rendering CombDisplay for", combId, comb);
|
const logoAlt = useMemo(() => {
|
||||||
|
return comb?.club_str
|
||||||
|
}, [comb]);
|
||||||
|
const logoSrc = useMemo(() => {
|
||||||
|
return `${vite_url}/api/club/${comb?.club_uuid}/logo`
|
||||||
|
}, [comb]);
|
||||||
|
|
||||||
return <div className="col position-relative"
|
return <div className="col position-relative"
|
||||||
style={{
|
style={{
|
||||||
@@ -123,8 +142,8 @@ function CombDisplay({combId, background, children}) {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}>
|
}}>
|
||||||
{comb !== "" && <>
|
{comb !== "" && <>
|
||||||
<img src={`/flags/svg/ad.svg`} alt={"fr"} style={{width: "6vw", height: "min(9vh, 6vw)", objectFit: "contain", margin: "0 .5vw"}}/>
|
<SmartLogoBackgroundMemo src={logoSrc} alt={logoAlt} style={logoStyle}/>
|
||||||
<div style={{fontSize: "min(3.5vw, 10vh)"}}>{comb.fname} {comb.lname}</div>
|
<div style={{fontSize: "min(3.5vw, 6.5vh)"}}>{comb.fname} {comb.lname}</div>
|
||||||
<img src={`/flags/svg/${comb.country.toLowerCase()}.svg`} alt={comb.country}
|
<img src={`/flags/svg/${comb.country.toLowerCase()}.svg`} alt={comb.country}
|
||||||
style={{width: "4vw", height: "8vh", objectFit: "contain", margin: "0 1.25vw"}}/>
|
style={{width: "4vw", height: "8vh", objectFit: "contain", margin: "0 1.25vw"}}/>
|
||||||
</>}
|
</>}
|
||||||
|
|||||||
@@ -36,6 +36,22 @@ export function MarchReducer(datas, action) {
|
|||||||
datas[index] = action.payload
|
datas[index] = action.payload
|
||||||
return [...datas]
|
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':
|
case 'SORT':
|
||||||
return datas.sort(action.payload)
|
return datas.sort(action.payload)
|
||||||
case 'REORDER':
|
case 'REORDER':
|
||||||
|
|||||||
@@ -130,3 +130,26 @@ export function scorePrint(s1) {
|
|||||||
return String(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')
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user