add list user and part of user page

This commit is contained in:
Thibaut Valentin 2024-01-22 22:52:23 +01:00
parent 21cb673d33
commit 6f590cdaf7
44 changed files with 1469 additions and 186 deletions

16
pom.xml
View File

@ -72,12 +72,28 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>3.0.0-BETA</version>
</dependency>
</dependencies>
<build>
<plugins>

View File

@ -1,14 +1,15 @@
package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Contact;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
import java.util.Map;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@ -25,4 +26,25 @@ public class ClubModel {
String country;
String shieldURL;
//@Enumerated(EnumType.STRING)
@ElementCollection
@CollectionTable(name = "club_contact_mapping",
joinColumns = {@JoinColumn(name = "club_id", referencedColumnName = "id")})
@MapKeyColumn(name = "contact_type")
Map<Contact, String> contact;
String training_location;
String training_day_time;
String contact_intern;
String RNA;
String SIRET;
String no_affiliation;
boolean international;
}

View File

@ -2,29 +2,31 @@ package fr.titionfire.ffsaf.data.model;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.RoleAsso;
import io.quarkus.runtime.annotations.RegisterForReflection;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
import java.util.Date;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
@Entity
@Table(name = "comb")
public class CombModel {
@Table(name = "membre")
public class MembreModel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String lname = "";
String fname = "";
String lname;
String fname;
Categorie categorie;
@ -34,7 +36,17 @@ public class CombModel {
Genre genre;
int licence = 0;
int licence;
String country = "fr";
String country;
Date birth_date;
String email;
RoleAsso role;
GradeArbitrage grade_arbitrage;
String url_photo;
}

View File

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

View File

@ -1,11 +1,16 @@
package fr.titionfire.ffsaf.domain.entity;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.utils.Contact;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
@Data
@Builder
@AllArgsConstructor
@RegisterForReflection
public class ClubEntity {
@ -13,12 +18,39 @@ public class ClubEntity {
private String name;
private String country;
private String shieldURL;
private Map<Contact, String> contact;
private String training_location;
private String training_day_time;
private String contact_intern;
private String RNA;
private String SIRET;
private String no_affiliation;
private boolean international;
public static ClubEntity fromModel (ClubModel model) {
return new ClubEntity(model.getId(), model.getName(), model.getCountry(), model.getShieldURL());
if (model == null) {
return null;
}
return ClubEntity.builder()
.id(model.getId())
.name(model.getName())
.country(model.getCountry())
.shieldURL(model.getShieldURL())
.contact(model.getContact())
.training_location(model.getTraining_location())
.training_day_time(model.getTraining_day_time())
.contact_intern(model.getContact_intern())
.RNA(model.getRNA())
.SIRET(model.getSIRET())
.no_affiliation(model.getNo_affiliation())
.international(model.isInternational())
.build();
}
public ClubModel toModel () {
return new ClubModel(this.id, this.name, this.country, this.shieldURL);
return new ClubModel(this.id, this.name, this.country, this.shieldURL, this.contact, this.training_location,
this.training_day_time, this.contact_intern, this.RNA, this.SIRET, this.no_affiliation,
this.international);
}
}

View File

@ -1,38 +0,0 @@
package fr.titionfire.ffsaf.domain.entity;
import fr.titionfire.ffsaf.data.model.CombModel;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
@RegisterForReflection
public class CombEntity {
private long id;
private String lname = "";
private String fname = "";
private Categorie categorie;
private ClubEntity club;
private Genre genre;
private int licence;
private String country;
public static CombEntity fromModel(CombModel model) {
if (model == null)
return null;
return new CombEntity(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
(model.getClub() == null) ? null : ClubEntity.fromModel(model.getClub()),
model.getGenre(), model.getLicence(), model.getCountry());
}
public static String getFullName(CombModel model) {
return model.getFname() + " " + model.getLname();
}
public String getFullName() {
return this.fname + " " + this.lname;
}
}

View File

@ -0,0 +1,64 @@
package fr.titionfire.ffsaf.domain.entity;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.RoleAsso;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@RegisterForReflection
public class MembreEntity {
private long id;
private String lname = "";
private String fname = "";
private Categorie categorie;
private ClubEntity club;
private Genre genre;
private int licence;
private String country;
private Date birth_date;
private String email;
private RoleAsso role;
private GradeArbitrage grade_arbitrage;
private String url_photo;
public static MembreEntity fromModel(MembreModel model) {
if (model == null)
return null;
return new MembreEntityBuilder()
.id(model.getId())
.lname(model.getLname())
.fname(model.getFname())
.categorie(model.getCategorie())
.club(ClubEntity.fromModel(model.getClub()))
.genre(model.getGenre())
.licence(model.getLicence())
.country(model.getCountry())
.birth_date(model.getBirth_date())
.email(model.getEmail())
.role(model.getRole())
.grade_arbitrage(model.getGrade_arbitrage())
.url_photo(model.getUrl_photo())
.build();
}
public static String getFullName(MembreModel model) {
return model.getFname() + " " + model.getLname();
}
public String getFullName() {
return this.fname + " " + this.lname;
}
}

View File

@ -2,13 +2,16 @@ package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.vertx.VertxContextSupport;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.Collection;
import java.util.List;
@WithSession
@ApplicationScoped
@ -17,11 +20,16 @@ public class ClubService {
@Inject
ClubRepository repository;
public ClubModel findByIdOptionalClub (long id) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id)));
public SimpleClubModel findByIdOptionalClub(long id) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel)));
}
public Collection<ClubModel> findAllClub () throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findAll().list()));
public Collection<SimpleClubModel> findAllClub() throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(
() -> repository.findAll().list().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList())));
}
public Uni<List<ClubModel>> getAll() {
return repository.listAll();
}
}

View File

@ -1,27 +0,0 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.CombModel;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.vertx.VertxContextSupport;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@WithSession
@ApplicationScoped
public class CombService {
@Inject
CombRepository repository;
public CombModel find(int licence, String np) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() ->
repository.find("licence = ?1 AND (lname = ?2 OR fname = ?2)", licence, np).firstResult()));
}
public CombModel findByIdOptionalComb (long id) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id)));
}
}

View File

@ -0,0 +1,64 @@
package fr.titionfire.ffsaf.domain.service;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.data.repository.ClubRepository;
import fr.titionfire.ffsaf.data.repository.CombRepository;
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.utils.Pair;
import io.quarkus.hibernate.reactive.panache.Panache;
import io.quarkus.hibernate.reactive.panache.common.WithSession;
import io.quarkus.panache.common.Sort;
import io.quarkus.vertx.VertxContextSupport;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.util.List;
@WithSession
@ApplicationScoped
public class MembreService {
@Inject
CombRepository repository;
@Inject
ClubRepository clubRepository;
public SimpleCombModel find(int licence, String np) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() ->
repository.find("licence = ?1 AND (lname = ?2 OR fname = ?2)", licence, np).firstResult().map(SimpleCombModel::fromModel)));
}
public SimpleCombModel findByIdOptionalComb(long id) throws Throwable {
return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.findById(id).map(SimpleCombModel::fromModel)));
}
public Uni<List<MembreModel>> getAll() {
return repository.listAll(Sort.ascending("fname", "lname"));
}
public Uni<MembreModel> getById(long id) {
return repository.findById(id);
}
public Uni<String> update(long id, FullMemberForm membre) {
return repository.findById(id)
.chain(membreModel -> clubRepository.findById(membre.getClub()).map(club -> new Pair<>(membreModel, club)))
.onItem().transformToUni(pair -> {
MembreModel m = pair.getKey();
m.setFname(membre.getFname());
m.setLname(membre.getLname());
m.setClub(pair.getValue());
m.setCountry(membre.getCountry());
m.setBirth_date(membre.getBirth_date());
m.setGenre(membre.getGenre());
m.setCategorie(membre.getCategorie());
m.setRole(membre.getRole());
m.setGrade_arbitrage(membre.getGrade_arbitrage());
m.setEmail(membre.getEmail());
return Panache.withTransaction(() -> repository.persist(m));
}).map(__ -> "OK");
}
}

View File

@ -2,7 +2,7 @@ package fr.titionfire.ffsaf.net2;
import com.fasterxml.jackson.databind.JsonNode;
import fr.titionfire.ffsaf.domain.service.ClubService;
import fr.titionfire.ffsaf.domain.service.CombService;
import fr.titionfire.ffsaf.domain.service.MembreService;
import fr.titionfire.ffsaf.net2.packet.IAction;
import fr.titionfire.ffsaf.net2.packet.RegisterAction;
import fr.titionfire.ffsaf.utils.Pair;
@ -54,7 +54,7 @@ public class ServerCustom extends Thread {
protected Scheduler quartz;
@Inject
public CombService combService;
public MembreService membreService;
@Inject
public ClubService clubService;

View File

@ -0,0 +1,27 @@
package fr.titionfire.ffsaf.net2.data;
import fr.titionfire.ffsaf.data.model.ClubModel;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
public class SimpleClubModel {
Long id;
String name;
String country;
String shieldURL;
public static SimpleClubModel fromModel(ClubModel model) {
if (model == null)
return null;
return new SimpleClubModel(model.getId(), model.getName(), model.getCountry(), model.getShieldURL());
}
}

View File

@ -0,0 +1,35 @@
package fr.titionfire.ffsaf.net2.data;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@RegisterForReflection
public class SimpleCombModel {
Long id;
String lname = "";
String fname = "";
Categorie categorie;
SimpleClubModel club;
Genre genre;
int licence = 0;
String country = "fr";
public static SimpleCombModel fromModel(MembreModel model) {
if (model == null)
return null;
return new SimpleCombModel(model.getId(), model.getLname(), model.getFname(), model.getCategorie(),
(model.getClub() == null) ? null : SimpleClubModel.fromModel(model.getClub()),
model.getGenre(), model.getLicence(), model.getCountry());
}
}

View File

@ -1,7 +1,7 @@
package fr.titionfire.ffsaf.net2.packet;
import fr.titionfire.ffsaf.data.model.ClubModel;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import org.jboss.logging.Logger;
import java.util.Collection;
@ -12,7 +12,7 @@ public class RClub {
final CIA<Long> findByIdOptionalClub = new CIA<>(Long.class, (client_Thread, message) -> {
try {
ClubModel clubModel = ServerCustom.getInstance().clubService.findByIdOptionalClub(message.data());
SimpleClubModel clubModel = ServerCustom.getInstance().clubService.findByIdOptionalClub(message.data());
client_Thread.sendRepTo(clubModel, message);
} catch (Throwable e) {
LOGGER.error(e.getMessage(), e);
@ -22,7 +22,7 @@ public class RClub {
final IAction findAllClub = (client_Thread, message) -> {
try {
Collection<ClubModel> clubModels = ServerCustom.getInstance().clubService.findAllClub();
Collection<SimpleClubModel> clubModels = ServerCustom.getInstance().clubService.findAllClub();
client_Thread.sendRepTo(clubModels, message);
} catch (Throwable e) {
LOGGER.error(e.getMessage(), e);

View File

@ -1,7 +1,7 @@
package fr.titionfire.ffsaf.net2.packet;
import fr.titionfire.ffsaf.data.model.CombModel;
import fr.titionfire.ffsaf.net2.ServerCustom;
import fr.titionfire.ffsaf.net2.data.SimpleCombModel;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
@ -13,7 +13,7 @@ public class RComb {
final IAction findComb = (client_Thread, message) -> {
try {
CombModel combModel = ServerCustom.getInstance().combService.find(message.data().get("licence").asInt(), message.data().get("np").asText());
SimpleCombModel combModel = ServerCustom.getInstance().membreService.find(message.data().get("licence").asInt(), message.data().get("np").asText());
client_Thread.sendRepTo(combModel, message);
} catch (Throwable e) {
LOGGER.error(e.getMessage(), e);
@ -23,7 +23,7 @@ public class RComb {
final CIA<Long> findByIdOptionalComb = new CIA<>(Long.class, (client_Thread, message) -> {
try {
CombModel combModel = ServerCustom.getInstance().combService.findByIdOptionalComb(message.data());
SimpleCombModel combModel = ServerCustom.getInstance().membreService.findByIdOptionalComb(message.data());
client_Thread.sendRepTo(combModel, message);
} catch (Throwable e) {
LOGGER.error(e.getMessage(), e);

View File

@ -0,0 +1,28 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.ClubService;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import io.smallrye.mutiny.Uni;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Path("/club")
public class ClubEndpoints {
@Inject
ClubService clubService;
@GET
@Path("/no_detail")
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<SimpleClubModel>> getAll() {
return clubService.getAll().map(clubModels -> clubModels.stream().map(SimpleClubModel::fromModel).toList());
}
}

View File

@ -0,0 +1,124 @@
package fr.titionfire.ffsaf.rest;
import fr.titionfire.ffsaf.domain.service.MembreService;
import fr.titionfire.ffsaf.rest.data.SimpleMembre;
import fr.titionfire.ffsaf.rest.from.FullMemberForm;
import fr.titionfire.ffsaf.utils.Pair;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.unchecked.Unchecked;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.commons.io.FileUtils;
import org.apache.tika.mime.MimeTypeException;
import org.apache.tika.mime.MimeTypes;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.nio.file.Files;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
@Path("/member")
public class CombEndpoints {
@Inject
MembreService membreService;
@ConfigProperty(name = "upload_dir")
String media;
@GET
@Path("/all")
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
public Uni<List<SimpleMembre>> getAll() {
return membreService.getAll().map(membreModels -> membreModels.stream().map(SimpleMembre::fromModel).toList());
}
@GET
@Path("{id}")
@RolesAllowed("federation_admin")
@Produces(MediaType.APPLICATION_JSON)
public Uni<SimpleMembre> getById(@PathParam("id") long id) {
return membreService.getById(id).map(SimpleMembre::fromModel);
}
@POST
@Path("{id}")
@RolesAllowed("federation_admin")
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Uni<String> setAdminMembre(@PathParam("id") long id, FullMemberForm input) {
Future<String> future = CompletableFuture.supplyAsync(() -> {
try (InputStream is = new BufferedInputStream(new ByteArrayInputStream(input.getPhoto_data()))) {
String mimeType = URLConnection.guessContentTypeFromStream(is);
String extension = MimeTypes.getDefaultMimeTypes().forName(mimeType).getExtension();
FileUtils.writeByteArrayToFile(new File(media, "ppMembre/" + input.getId() + extension), input.getPhoto_data());
return "OK";
} catch (IOException | MimeTypeException e) {
return e.getMessage();
}
});
if (input.getPhoto_data().length > 0) {
return membreService.update(id, input)
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
}))
.chain(() -> Uni.createFrom().future(future))
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to get MimeType " + out);
}));
} else {
return membreService.update(id, input)
.invoke(Unchecked.consumer(out -> {
if (!out.equals("OK")) throw new InternalError("Fail to update data: " + out);
}));
}
}
@GET
@Path("{id}/photo")
public Uni<Response> getPhoto(@PathParam("id") long id) throws URISyntaxException {
Future<Pair<File, byte[]>> future = CompletableFuture.supplyAsync(() -> {
FilenameFilter filter = (directory, filename) -> filename.startsWith(String.valueOf(id));
File[] files = new File(media, "ppMembre").listFiles(filter);
if (files != null && files.length > 0) {
File file = files[0];
try {
byte[] data = Files.readAllBytes(file.toPath());
return new Pair<>(file, data);
} catch (IOException ignored) {
}
}
return null;
});
URI uri = new URI("https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava2.webp");
return Uni.createFrom().future(future)
.map(filePair -> {
if (filePair == null)
return Response.temporaryRedirect(uri).build();
String mimeType = URLConnection.guessContentTypeFromName(filePair.getKey().getName());
Response.ResponseBuilder resp = Response.ok(filePair.getValue());
resp.type(MediaType.APPLICATION_OCTET_STREAM);
resp.header(HttpHeaders.CONTENT_LENGTH, filePair.getValue().length);
resp.header(HttpHeaders.CONTENT_TYPE, mimeType);
resp.header(HttpHeaders.CONTENT_DISPOSITION, "inline; ");
return resp.build();
});
}
}

View File

@ -0,0 +1,55 @@
package fr.titionfire.ffsaf.rest.data;
import fr.titionfire.ffsaf.data.model.MembreModel;
import fr.titionfire.ffsaf.net2.data.SimpleClubModel;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.RoleAsso;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@RegisterForReflection
public class SimpleMembre {
private long id;
private String lname = "";
private String fname = "";
private Categorie categorie;
private SimpleClubModel club;
private Genre genre;
private int licence;
private String country;
private Date birth_date;
private String email;
private RoleAsso role;
private GradeArbitrage grade_arbitrage;
private String url_photo;
public static SimpleMembre fromModel(MembreModel model) {
if (model == null)
return null;
return new SimpleMembreBuilder()
.id(model.getId())
.lname(model.getLname())
.fname(model.getFname())
.categorie(model.getCategorie())
.club(SimpleClubModel.fromModel(model.getClub()))
.genre(model.getGenre())
.licence(model.getLicence())
.country(model.getCountry())
.birth_date(model.getBirth_date())
.email(model.getEmail())
.role(model.getRole())
.grade_arbitrage(model.getGrade_arbitrage())
.url_photo(model.getUrl_photo())
.build();
}
}

View File

@ -0,0 +1,74 @@
package fr.titionfire.ffsaf.rest.from;
import fr.titionfire.ffsaf.utils.Categorie;
import fr.titionfire.ffsaf.utils.Genre;
import fr.titionfire.ffsaf.utils.GradeArbitrage;
import fr.titionfire.ffsaf.utils.RoleAsso;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.core.MediaType;
import lombok.Getter;
import org.jboss.resteasy.reactive.PartType;
import java.util.Date;
@Getter
public class FullMemberForm {
@FormParam("id")
private String id = null;
@FormParam("lname")
private String lname = null;
@FormParam("fname")
private String fname = null;
@FormParam("categorie")
private Categorie categorie = null;
@FormParam("club")
private Long club = null;
@FormParam("genre")
private Genre genre;
@FormParam("licence")
private int licence;
@FormParam("country")
private String country;
@FormParam("birth_date")
private Date birth_date;
@FormParam("email")
private String email;
@FormParam("role")
private RoleAsso role;
@FormParam("grade_arbitrage")
private GradeArbitrage grade_arbitrage;
@FormParam("photo_data")
@PartType(MediaType.APPLICATION_OCTET_STREAM)
private byte[] photo_data = new byte[0];
@Override
public String toString() {
return "FullMemberForm{" +
"id='" + id + '\'' +
", lname='" + lname + '\'' +
", fname='" + fname + '\'' +
", categorie=" + categorie +
", club=" + club +
", genre=" + genre +
", licence=" + licence +
", country='" + country + '\'' +
", birth_date=" + birth_date +
", email='" + email + '\'' +
", role=" + role +
", grade_arbitrage=" + grade_arbitrage +
", url_photo=" + photo_data.length +
'}';
}
}

View File

@ -0,0 +1,20 @@
package fr.titionfire.ffsaf.utils;
public enum Contact {
COURRIEL("Courriel"),
TELEPHONE("Téléphone"),
SITE("Site web"),
FACEBOOK("Facebook"),
INSTAGRAM("Instagram");
public final String name;
Contact(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,18 @@
package fr.titionfire.ffsaf.utils;
public enum GradeArbitrage {
NA("N/A"),
ASSESSEUR("Assesseur"),
ARBITRE("Arbitre");
public final String name;
GradeArbitrage(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}

View File

@ -0,0 +1,20 @@
package fr.titionfire.ffsaf.utils;
public enum RoleAsso {
MEMBRE("Membre"),
PRESIDENT("Président"),
TRESORIER("Trésorier"),
SECRETAIRE("Secrétaire");
public final String name;
RoleAsso(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}

View File

@ -17,6 +17,14 @@ quarkus.quartz.start-mode=forced
%dev.quarkus.log.min-level=ALL
%dev.quarkus.log.category."fr.titionfire.ffsaf".level=ALL
quarkus.oidc.auth-server-url=https://auth.safca.fr/auth/realms/safca
quarkus.oidc.client-id=backend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.tls.verification=required
#quarkus.oidc.tls.verification=none
quarkus.http.limits.max-body-size=10M
database.prefix = test2_
database.database=ffsaf
database.hostname=localhost

View File

@ -6,3 +6,6 @@ Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
https://mhnpd.github.io/react-loader-spinner/docs/intro

View File

@ -6,6 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Vite + React</title>
<link href="/index.css" rel="stylesheet">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
@ -15,12 +17,17 @@
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="root">
<div class="loader-container">
<h1 style="color: #00aff2">Chargement de l'application FFSAF...</h1>
<div class="spinner"></div>
</div>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"
></script>
<script type="module" src="/src/main.jsx"></script>
<script async type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@ -14,8 +14,10 @@
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5",
"browser-image-compression": "^2.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.21.2"
},
"devDependencies": {
@ -378,6 +380,24 @@
"node": ">=6.9.0"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz",
"integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==",
"dependencies": {
"@emotion/memoize": "^0.8.1"
}
},
"node_modules/@emotion/memoize": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
},
"node_modules/@emotion/unitless": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
"integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.19.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz",
@ -1258,6 +1278,11 @@
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"dev": true
},
"node_modules/@types/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw=="
},
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@ -1501,6 +1526,14 @@
"concat-map": "0.0.1"
}
},
"node_modules/browser-image-compression": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz",
"integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==",
"dependencies": {
"uzip": "0.20201231.0"
}
},
"node_modules/browserslist": {
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
@ -1556,6 +1589,14 @@
"node": ">=6"
}
},
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001576",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz",
@ -1642,6 +1683,24 @@
"node": ">= 8"
}
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
"engines": {
"node": ">=4"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -3140,7 +3199,6 @@
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
@ -3375,8 +3433,7 @@
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
"dev": true
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/postcss": {
"version": "8.4.33",
@ -3406,6 +3463,11 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -3487,6 +3549,27 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-loader-spinner": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-6.1.6.tgz",
"integrity": "sha512-x5h1Jcit7Qn03MuKlrWcMG9o12cp9SNDVHVJTNRi9TgtGPKcjKiXkou4NRfLAtXaFB3+Z8yZsVzONmPzhv2ErA==",
"dependencies": {
"react-is": "^18.2.0",
"styled-components": "^6.1.2"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-loader-spinner/node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/react-refresh": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz",
@ -3750,6 +3833,11 @@
"node": ">= 0.4"
}
},
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -3789,7 +3877,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -3883,6 +3970,70 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/styled-components": {
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz",
"integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==",
"dependencies": {
"@emotion/is-prop-valid": "1.2.1",
"@emotion/unitless": "0.8.0",
"@types/stylis": "4.2.0",
"css-to-react-native": "3.2.0",
"csstype": "3.1.2",
"postcss": "8.4.31",
"shallowequal": "1.1.0",
"stylis": "4.3.1",
"tslib": "2.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/styled-components"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
}
},
"node_modules/styled-components/node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
"node_modules/styled-components/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/stylis": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz",
"integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ=="
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -3922,6 +4073,11 @@
"node": ">=4"
}
},
"node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -4065,6 +4221,11 @@
"punycode": "^2.1.0"
}
},
"node_modules/uzip": {
"version": "0.20201231.0",
"resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz",
"integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng=="
},
"node_modules/vite": {
"version": "5.0.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.11.tgz",

View File

@ -16,8 +16,10 @@
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"axios": "^1.6.5",
"browser-image-compression": "^2.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.21.2"
},
"devDependencies": {

View File

@ -0,0 +1,33 @@
:root {
}
.loader-container {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: fixed;
background: rgba(0, 0, 0, 0.834);
z-index: 1;
}
.spinner {
width: 64px;
height: 64px;
border: 8px solid;
border-color: #3d5af1 transparent #3d5af1 transparent;
border-radius: 50%;
animation: spin-anim 1.2s linear infinite;
}
@keyframes spin-anim {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -3,10 +3,11 @@ import './App.css'
import {Nav} from "./components/Nav.jsx";
import {createBrowserRouter, Outlet, RouterProvider, useRouteError} from "react-router-dom";
import {Home} from "./pages/Homepage.jsx";
import {AdminRoot} from "./pages/admin/AdminRoot.jsx";
import {AdminRoot, getAdminChildren} from "./pages/admin/AdminRoot.jsx";
import {AuthCallback} from "./components/auhCallback.jsx";
import {useAuthDispatch} from "./hooks/useAuth.jsx";
import {KeycloakContextProvider, useAuthDispatch} from "./hooks/useAuth.jsx";
import {check_validity} from "./utils/auth.js";
import {MemberList} from "./pages/admin/MemberList.jsx";
const router = createBrowserRouter([
{
@ -21,16 +22,7 @@ const router = createBrowserRouter([
{
path: 'admin',
element: <AdminRoot/>,
children: [
{
path: '',
element: <div>Admin</div>
},
{
path: 'b',
element: <div>Admin B</div>
}
]
children: getAdminChildren()
}
]
},
@ -51,6 +43,16 @@ function PageError() {
}
function Root() {
const dispatch = useAuthDispatch()
const isInit = useRef(false)
useEffect(() => {
if (isInit.current)
return;
isInit.current = true
check_validity(b => dispatch({type: 'init', val: b}))
}, []);
return <>
<header>
<Nav/>
@ -64,19 +66,9 @@ function Root() {
function App() {
console.log('render')
const dispatch = useAuthDispatch()
const isInit = useRef(false)
useEffect(() => {
if (isInit.current)
return;
isInit.current = true
check_validity(b => dispatch({type: 'init', val: b}))
}, []);
return <RouterProvider router={router}/>;
return <KeycloakContextProvider>
<RouterProvider router={router}/>
</KeycloakContextProvider>;
}
export default App

View File

@ -0,0 +1,17 @@
import axios from "axios";
const api_url = import.meta.env.VITE_API_URL;
export function getAllMembre() {
return axios.get(`${api_url}/membre/all`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem("access_token")}`
}
}).then(data => {
console.log(data.data)
return data.data;
}).catch((e) => {
console.log(e);
return null;
})
}

View File

@ -0,0 +1,3 @@
export function AxiosError(data) {
return <span>{data.error.status} - {data.error.statusText}</span>
}

View File

@ -0,0 +1,44 @@
import {LoadingContextProvider, useLoadingSwitcher} from "../hooks/useLoading.jsx";
import {useFetch} from "../hooks/useFetch.js";
import {AxiosError} from "./AxiosError.jsx";
const api_url = import.meta.env.VITE_API_URL;
export function ClubSelect({defaultValue, name}) {
return <LoadingContextProvider>
<div className="input-group mb-3">
<ClubSelect_ defaultValue={defaultValue} name={name}/>
</div>
</LoadingContextProvider>
}
function ClubSelect_({defaultValue, name}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`${api_url}/club/no_detail`, setLoading, 1)
return <>
{data
? <div className="input-group mb-3">
<label className="input-group-text" id="inputGroupSelect02">Club</label>
<select className="form-select" id="inputGroupSelect02"
defaultValue={defaultValue} name={name}>
<option>Sélectionner...</option>
{data.map(club => (<option key={club.id} value={club.id}>{club.name}</option>))}
</select>
</div>
: error
? <AxiosError error={error}/>
: <Def/>
}
</>
}
function Def() {
return <div className="input-group mb-3">
<label className="input-group-text" id="inputGroupSelect02">Club</label>
<select className="form-select" id="inputGroupSelect02"
defaultValue="Chargement...">
<option>Chargement...</option>
</select>
</div>;
}

View File

@ -0,0 +1,11 @@
export function Input({placeholder, value, onChange}) {
return <div>
<input
type="text"
className="form-control"
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
/>
</div>
}

View File

@ -6,8 +6,6 @@ import {logout, redirect_auth_page} from "../utils/auth.js";
export function Nav() {
const {token, refresh, is_authenticated} = useAuth()
const dispatch = useAuthDispatch()
return <nav className="navbar navbar-light navbar-expand-md bg-body-tertiary " id="main-navbar">
<div className="container-fluid">
@ -25,20 +23,43 @@ export function Nav() {
<div className="collapse-item">
<ul className="navbar-nav">
<li className="nav-item"><NavLink className="nav-link" to="/">Accueil</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/admin">Administration</NavLink></li>
<li className="nav-item">
{!is_authenticated ? (
<div className="nav-link" onClick={() => redirect_auth_page()}>Login</div>
) : (
<div className="nav-link" onClick={() => {
logout(token, refresh).then(() => dispatch({type: 'invalidate'}))
}}>Logout</div>
)}
</li>
<AdminMenu/>
<LoginMenu/>
</ul>
</div>
</div>
</div>
</nav>
}
function AdminMenu() {
const {is_authenticated, data} = useAuth()
if (!is_authenticated || !data?.realm_access?.roles?.includes("federation_admin"))
return <></>
return <li className="nav-item dropdown">
<div className="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Administration
</div>
<ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/admin/member">Member</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/admin/b">B</NavLink></li>
</ul>
</li>
}
function LoginMenu() {
const {token, refresh, is_authenticated} = useAuth()
const dispatch = useAuthDispatch()
return <li className="nav-item">
{!is_authenticated ? (
<div className="nav-link" onClick={() => redirect_auth_page()}>Connexion</div>
) : (
<div className="nav-link" onClick={() => {
logout(token, refresh).then(() => dispatch({type: 'invalidate'}))
}}>Déconnexion</div>
)}
</li>
}

View File

@ -0,0 +1,56 @@
import {useEffect, useState} from "react";
import axios from "axios";
function stdAction(promise, setData, setErrors, setLoading = null, loadingLevel = 1) {
if (setLoading)
setLoading(loadingLevel)
promise.then(data => {
setData(data.data)
}).catch(e => {
setErrors(e.response)
}).finally(() => {
if (setLoading)
setLoading(0)
})
}
export function useFetch(url, setLoading = null, loadingLevel = 1, options = {}) {
const [data, setData] = useState(null)
const [error, setErrors] = useState(null)
useEffect(() => {
stdAction(axios.get(url, {
...options,
headers: {
'Authorization': `Bearer ${localStorage.getItem("access_token")}`,
'Accept': 'application/json; charset=UTF-8',
...options?.headers
}
}), setData, setErrors, setLoading, loadingLevel)
}, []);
return {
data, error
}
}
export function useFetchPut(url, dataRequest, setLoading = null, loadingLevel = 1, options = {}) {
const [data, setData] = useState(null)
const [error, setErrors] = useState(null)
useEffect(() => {
stdAction(axios.put(url, dataRequest, {
...options,
headers: {
'Authorization': `Bearer ${localStorage.getItem("access_token")}`,
'Accept': 'application/json; charset=UTF-8',
...options?.headers
}
}), setData, setErrors, setLoading, loadingLevel)
}, []);
return {
data, error
}
}

View File

@ -0,0 +1,22 @@
.overlayBG {
display: block;
top: 0;
left: 0;
right: 0;
bottom: 0;
position: fixed;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5) !important;
cursor: pointer;
}
.overlayContent {
position: absolute;
box-sizing: border-box;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
cursor: default;
z-index: 100;
}

View File

@ -0,0 +1,43 @@
import {createContext, useContext, useState} from "react";
import './useLoading.css'
import {FallingLines} from "react-loader-spinner";
const LoadingContext = createContext(0);
const LoadingSwitcherContext = createContext(undefined);
export function useLoading() {
return useContext(LoadingContext);
}
export function useLoadingSwitcher() {
return useContext(LoadingSwitcherContext);
}
export function LoadingContextProvider({children}) {
const [showOverlay, setOverlay] = useState(0);
return <LoadingContext.Provider value={showOverlay}>
<LoadingSwitcherContext.Provider value={v => setOverlay(v)}>
<div style={{position: 'relative'}}>
{children}
<LoadingOverLay/>
</div>
</LoadingSwitcherContext.Provider>
</LoadingContext.Provider>
}
function LoadingOverLay() {
const showOverlay = useLoading()
if (showOverlay) {
return <div className="overlayBG" style={{position: showOverlay === 1 ? 'absolute' : 'fixed'}}>
<div className="overlayContent" onClick={(e) => {
e.stopPropagation()
}}>
<FallingLines/>
</div>
</div>
} else {
return <></>
}
}

View File

@ -1,3 +0,0 @@
:root {
}

View File

@ -1,13 +1,9 @@
import React from 'react'
import React, {lazy, Suspense} from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import {KeycloakContextProvider} from "./hooks/useAuth.jsx";
import App from "./App.jsx";
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<KeycloakContextProvider>
<App/>
</KeycloakContextProvider>
<App/>
</React.StrictMode>,
)
)

View File

@ -1,16 +0,0 @@
#admin-navbar {
background: #eeeeee;
.nav-link{
color: #00aff2;
}
.nav-link.active{
color: #004969;
}
.navbar-brand {
display: inline-flex;
align-items: center;
color: #00aff2;
}
}

View File

@ -1,29 +1,31 @@
import {NavLink, Outlet} from "react-router-dom";
import './AdminRoot.css'
import {LoadingContextProvider} from "../../hooks/useLoading.jsx";
import {MemberList} from "./MemberList.jsx";
import {MemberPage} from "./MemberPage.jsx";
export function AdminRoot() {
return <>
<NavAdmin/>
<Outlet/>
<h1>Espace administration</h1>
<LoadingContextProvider>
<Outlet/>
</LoadingContextProvider>
</>
}
function NavAdmin() {
return <nav className="navbar navbar-expand-md bg-body-tertiary " id="admin-navbar">
<div className="container-fluid">
<button className="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#adminNavbarNavDropdown" aria-controls="adminNavbarNavDropdown"
aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="adminNavbarNavDropdown">
<div className="collapse-item">
<ul className="navbar-nav">
<li className="nav-item"><NavLink className="nav-link" to="/admin">A</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/admin/b">B</NavLink></li>
</ul>
</div>
</div>
</div>
</nav>
export function getAdminChildren () {
return [
{
path: 'member',
element: <MemberList/>
},
{
path: 'member/:id',
element: <MemberPage/>
},
{
path: 'b',
element: <div>Admin B</div>
}
]
}

View File

@ -0,0 +1,62 @@
import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner";
import {useState} from "react";
import {Input} from "../../components/Input.jsx";
import {useNavigate} from "react-router-dom";
const api_url = import.meta.env.VITE_API_URL;
const removeDiacritics = str => {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
}
export function MemberList() {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`${api_url}/member/all`, setLoading, 1)
const [searchInput, setSearchInput] = useState("");
const navigate = useNavigate();
const visibleMember = data ? data.filter(member => {
const lo = removeDiacritics(searchInput).toLowerCase()
return !searchInput
|| (removeDiacritics(member.fname).toLowerCase().startsWith(lo)
|| removeDiacritics(member.lname).toLowerCase().startsWith(lo));
}) : [];
return <>
<SearchBar searchInput={searchInput} onSearchInputChange={setSearchInput}/>
{data
? <div className="list-group">
{visibleMember.map(member => (
<span key={member.id}
onClick={() => navigate("/admin/member/" + member.id)}
className="list-group-item list-group-item-action">{member.fname} {member.lname}</span>))}
</div>
: error
? <AxiosError error={error}/>
: <Def/>
}
</>
}
function SearchBar({searchInput, onSearchInputChange}) {
return <div>
<div className="mb-3">
<Input value={searchInput} onChange={onSearchInputChange} placeholder="Rechercher..."/>
</div>
</div>
}
function Def() {
return <div className="list-group">
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
<li className="list-group-item"><ThreeDots/></li>
</div>
}

View File

@ -0,0 +1,228 @@
import {useNavigate, useParams} from "react-router-dom";
import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
import {useFetch, useFetchPut} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx";
import {ClubSelect} from "../../components/ClubSelect.jsx";
import {useEffect, useState} from "react";
import {getCategoryFormBirthDate, getSaison} from "../../utils/Tools.js";
import axios from "axios";
import imageCompression from "browser-image-compression";
const api_url = import.meta.env.VITE_API_URL;
export function MemberPage() {
const {id} = useParams()
const navigate = useNavigate();
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`${api_url}/member/${id}`, setLoading, 1)
const handleSubmit = (event) => {
event.preventDefault();
setLoading(1)
const formData = new FormData();
formData.append("id", data.id);
formData.append("lname", event.target.lname?.value);
formData.append("fname", event.target.fname?.value);
formData.append("categorie", event.target.category?.value);
formData.append("club", event.target.club?.value);
formData.append("genre", event.target.genre?.value);
formData.append("country", event.target.country?.value);
formData.append("birth_date", new Date(event.target.birth_date?.value).toUTCString());
formData.append("email", event.target.email?.value);
formData.append("role", event.target.role?.value);
formData.append("grade_arbitrage", event.target.grade_arbitrage?.value);
const send = (formData_) => {
axios.post(`${api_url}/member/${id}`, formData_, {
headers: {
'Authorization': `Bearer ${localStorage.getItem("access_token")}`,
'Accept': '*/*',
'Content-Type': 'multipart/form-data',
}
}).then(data => {
console.log(data.data)
}).catch(e => {
console.log(e.response)
}).finally(() => {
if (setLoading)
setLoading(0)
})
}
const imageFile = event.target.url_photo.files[0];
if (imageFile) {
console.log(`originalFile size ${imageFile.size / 1024 / 1024} MB`);
const options = {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
useWebWorker: true,
}
imageCompression(imageFile, options).then(compressedFile => {
console.log(`compressedFile size ${compressedFile.size / 1024 / 1024} MB`); // smaller than maxSizeMB
formData.append("photo_data", compressedFile)
send(formData)
});
} else {
send(formData)
}
}
return <>
<h2>Page membre</h2>
<button type="button" className="btn btn-link" onClick={() => navigate("/admin/member")}>
&lt;&lt; retour
</button>
{data
? <MemberForm data={data} handleSubmit={handleSubmit}/>
: error && <AxiosError error={error}/>
}
</>
}
function MemberForm({data, handleSubmit}) {
return <div>
<div className="row">
<div className="col-lg-4">
<div className="card mb-4">
<div className="card-header">Licence n°{data.licence}</div>
<div className="card-body text-center">
<div className="input-group mb-3">
<img
src={"http://localhost:8080/member/"+data.id+"/photo"}
alt="avatar"
className="rounded-circle img-fluid" style={{object_fit: 'contain'}}/>
</div>
</div>
</div>
</div>
<div className="col-lg-8">
<form onSubmit={handleSubmit}>
<div className="card mb-4">
<div className="card-header">Information</div>
<div className="card-body">
<TextField name="lname" text="Nom" value={data.lname}/>
<TextField name="fname" text="Prénom" value={data.fname}/>
<TextField name="email" text="Email" value={data.email} placeholder="name@example.com"
type="email"/>
<OptionField name="genre" text="Genre" value={data.genre}
values={{NA: 'N/A', H: 'H', F: 'F'}}/>
<OptionField name="country" text="Pays" value={data.country}
values={{NA: 'Sélectionner...', fr: 'FR', es: 'ES', be: 'BE'}}/>
<BirthDayField inti_date={data.birth_date ? data.birth_date.split('T')[0] : ''}
inti_category={data.categorie}/>
<div className="row">
<ClubSelect defaultValue={data?.club?.id} name="club"/>
</div>
<OptionField name="grade_arbitrage" text="Rôle" value={data.role}
values={{NA: 'N/A', ASSESSEUR: 'Assesseur', ARBITRE: 'Arbitre'}}/>
<OptionField name="role" text="Grade d'arbitrage" value={data.grade_arbitrage}
values={{
MEMBRE: 'Membre',
PRESIDENT: 'Président',
TRESORIER: 'Trésorier',
SECRETAIRE: 'Secrétaire'
}}/>
<div className="row">
<div className="input-group mb-3">
<label className="input-group-text" htmlFor="url_photo">Photos
(optionnelle)</label>
<input type="file" className="form-control" id="url_photo" name="url_photo"
accept=".jpg,.jpeg,.gif,.png,.svg"/>
</div>
</div>
<div className="row">
<div className="col-md-12 text-right">
<button type="submit" className="btn btn-primary">Enregistrer</button>
</div>
</div>
</div>
</div>
</form>
<div className="row">
<div className="col-md-6">
<div className="card mb-4 mb-md-0">
<div className="card-header">Licence</div>
<div className="card-body">
<p className="mb-1">Web Design</p>
</div>
</div>
</div>
<div className="col-md-6">
<div className="card mb-4 mb-md-0">
<div className="card-header">Sélection en équipe de France</div>
<div className="card-body">
<p className="mb-1">Web Design</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
function BirthDayField({inti_date, inti_category}) {
const [date, setDate] = useState(inti_date)
const [category, setCategory] = useState(inti_category)
const [canUpdate, setCanUpdate] = useState(false)
useEffect(() => {
const b = category !== getCategoryFormBirthDate(new Date(date), new Date('2023-09-01'))
if (b !== canUpdate)
setCanUpdate(b)
}, [date, category])
const updateCat = (e) => {
console.log(date)
setCategory(getCategoryFormBirthDate(new Date(date), new Date('2023-09-01')))
}
return <>
<div className="input-group mb-3">
<span className="input-group-text" id="birth_date">Date de naissance</span>
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="birth_date"
name="birth_date" aria-describedby="birth_date" defaultValue={date} required
onChange={(e) => setDate(e.target.value)}/>
</div>
<div className="row">
<div className="input-group mb-3">
<span className="input-group-text" id="category">Catégorie</span>
<input type="text" className="form-control" placeholder="" name="category"
aria-label="category" value={category} aria-describedby="category"
disabled/>
{canUpdate && <button className="btn btn-outline-secondary" type="button" id="button-addon1"
onClick={updateCat}>Mettre à jours</button>}
</div>
</div>
</>
}
function OptionField({name, text, values, value}) {
return <div className="row">
<div className="input-group mb-3">
<label className="input-group-text" id={name}>{text}</label>
<select className="form-select" id={name} name={name} defaultValue={value} required>
{Object.keys(values).map((key, _) => {
return (<option key={key} value={key}>{values[key]}</option>)
})}
</select>
</div>
</div>
}
function TextField({name, text, value, placeholder, type = "text"}) {
return <div className="row">
<div className="input-group mb-3">
<span className="input-group-text" id={name}>{text}</span>
<input type={type} className="form-control" placeholder={placeholder ? placeholder : text} aria-label={name}
name={name} aria-describedby={name} defaultValue={value} required/>
</div>
</div>
}

View File

@ -0,0 +1,37 @@
export function getCategoryFormBirthDate(birth_date, currentDate = new Date()) {
const currentSaison = getSaison(currentDate)
const birthYear = birth_date.getFullYear()
const diff = currentSaison - birthYear;
if (diff < 6) {
return "SUPER_MINI";
} else if (diff < 8) {
return "MINI_POUSSIN";
} else if (diff < 10) {
return "POUSSIN";
} else if (diff < 12) {
return "BENJAMIN";
} else if (diff < 14) {
return "MINIME";
} else if (diff < 16) {
return "CADET";
} else if (diff < 18) {
return "JUNIOR";
} else if (diff < 24) {
return "SENIOR1";
} else if (diff < 34) {
return "SENIOR2";
} else if (diff < 44) {
return "VETERAN1";
} else {
return "VETERAN2";
}
}
export function getSaison(currentDate = new Date()) {
if (currentDate.getMonth() >= 8) { //septembre et plus
return currentDate.getFullYear()
} else {
return currentDate.getFullYear() - 1
}
}