Compare commits

...

9 Commits

Author SHA1 Message Date
401e2ce3c5 [FEAT] block reload on scoll 2025-03-22 12:12:08 +01:00
693d59ab68 [FEAT] review and normailise title of the topBar
Signed-off-by: Edouard DUPIN <yui.heero@gmail.com>
2025-03-22 12:11:51 +01:00
4eff2e55ef [FIX] session management 2025-03-22 12:11:19 +01:00
1e890f9524 [FIX] remove develop mode in production 2025-03-22 12:11:06 +01:00
dbb2527cb8 [FEAT] update dependecy 2025-03-22 12:10:45 +01:00
d65faa8810 [FIX] new file update and create 2025-03-22 12:10:32 +01:00
9c9476b052 [FEAT] increase feature of topBar 2025-03-22 12:09:37 +01:00
3e92c2b74a [FEAT] update new archidata 2025-03-22 12:09:18 +01:00
a7134c01ed [FIX] readme title 2025-03-22 12:06:05 +01:00
61 changed files with 752 additions and 365 deletions

View File

@ -1,4 +1,4 @@
Karideo
Karusic
=======
**K**angaroo **A**nd **R**abbit (m)usic is a simple framework to propose music streaming for personal network

View File

@ -20,7 +20,7 @@
<dependency>
<groupId>kangaroo-and-rabbit</groupId>
<artifactId>archidata</artifactId>
<version>0.23.6</version>
<version>0.25.4</version>
</dependency>
<!-- Loopback of logger JDK logging API to SLF4J -->
<dependency>

View File

@ -13,6 +13,7 @@ import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.validation.ValidationFeature;
import org.kar.archidata.UpdateJwtPublicKey;
import org.kar.archidata.api.DataResource;
import org.kar.archidata.api.ProxyResource;
@ -151,6 +152,8 @@ public class WebLauncher {
// add jackson to be discover when we are ins standalone server
rc.register(JacksonFeature.class);
// enable jersey specific validations (@Valid)
rc.register(ValidationFeature.class);
// enable this to show low level request
// rc.property(LoggingFeature.LOGGING_FEATURE_LOGGER_LEVEL_SERVER, Level.WARNING.getName());

View File

@ -32,7 +32,6 @@ public class WebLauncherLocal extends WebLauncher {
if (true) {
// for local test:
ConfigBaseVariable.apiAdress = "http://0.0.0.0:19080/karusic/api/";
// ConfigBaseVariable.ssoAdress = "https://atria-soft.org/karso/api/";
ConfigBaseVariable.dbPort = "3906";
ConfigBaseVariable.testMode = "true";
}

View File

@ -6,9 +6,9 @@ import java.util.UUID;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.archidata.annotation.AsyncType;
import org.kar.archidata.annotation.FormDataOptional;
import org.kar.archidata.annotation.TypeScriptProgress;
import org.kar.archidata.annotation.apiGenerator.ApiAsyncType;
import org.kar.archidata.annotation.apiGenerator.ApiInputOptional;
import org.kar.archidata.annotation.apiGenerator.ApiTypeScriptProgress;
import org.kar.archidata.dataAccess.DBAccess;
import org.kar.archidata.dataAccess.DataAccess;
import org.kar.archidata.dataAccess.addOnSQL.AddOnDataJson;
@ -74,7 +74,7 @@ public class AlbumResource {
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
@Operation(description = "Update a specific album")
public Album patch(@PathParam("id") final Long id, @AsyncType(Album.class) final String jsonRequest) throws Exception {
public Album patch(@PathParam("id") final Long id, @ApiAsyncType(Album.class) final String jsonRequest) throws Exception {
// final Query<Album> query = this.morphiaService.getDatastore().find(Album.class).filter(Filters.eq("id", id));
// final UpdateOperations<Album> ops = this.morphiaService.getDatastore().createUpdateOperations(Album.class)
// .set("name", master.getName());
@ -119,9 +119,9 @@ public class AlbumResource {
@RolesAllowed("ADMIN")
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@Operation(description = "Add a cover on a specific album")
@TypeScriptProgress
public Album uploadCover(@PathParam("id") final Long id, @FormDataOptional @FormDataParam("uri") final String uri, @FormDataOptional @FormDataParam("file") final InputStream fileInputStream,
@FormDataOptional @FormDataParam("file") final FormDataContentDisposition fileMetaData) throws Exception {
@ApiTypeScriptProgress
public Album uploadCover(@PathParam("id") final Long id, @ApiInputOptional @FormDataParam("uri") final String uri, @ApiInputOptional @FormDataParam("file") final InputStream fileInputStream,
@ApiInputOptional @FormDataParam("file") final FormDataContentDisposition fileMetaData) throws Exception {
try (DBAccess db = DBAccess.createInterface()) {
if (uri != null) {
DataTools.uploadCoverFromUri(db, Album.class, id, uri);

View File

@ -6,9 +6,8 @@ import java.util.List;
import org.bson.types.ObjectId;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.archidata.annotation.AsyncType;
import org.kar.archidata.annotation.FormDataOptional;
import org.kar.archidata.annotation.TypeScriptProgress;
import org.kar.archidata.annotation.apiGenerator.ApiInputOptional;
import org.kar.archidata.annotation.apiGenerator.ApiTypeScriptProgress;
import org.kar.archidata.dataAccess.DBAccess;
import org.kar.archidata.dataAccess.DataAccess;
import org.kar.archidata.dataAccess.addOnSQL.AddOnDataJson;
@ -20,11 +19,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.annotation.security.RolesAllowed;
import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@ -56,12 +56,13 @@ public class ArtistResource {
return DataAccess.insert(data, new CheckFunction(CHECKER));
}
@PATCH
@PUT
@Path("{id}")
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
public Artist patch(@PathParam("id") final Long id, @AsyncType(Artist.class) final String jsonRequest) throws Exception {
DataAccess.updateWithJson(Artist.class, id, jsonRequest, new CheckFunction(CHECKER));
public Artist patch(@PathParam("id") final Long id, @Valid final Artist jsonRequest) throws Exception {
// new CheckFunction(CHECKER)
DataAccess.update(id, jsonRequest);
return DataAccess.get(Artist.class, id);
}
@ -76,9 +77,9 @@ public class ArtistResource {
@Path("{id}/cover")
@RolesAllowed("ADMIN")
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@TypeScriptProgress
public Artist uploadCover(@PathParam("id") final Long id, @FormDataOptional @FormDataParam("uri") final String uri, @FormDataOptional @FormDataParam("file") final InputStream fileInputStream,
@FormDataOptional @FormDataParam("file") final FormDataContentDisposition fileMetaData) throws Exception {
@ApiTypeScriptProgress
public Artist uploadCover(@PathParam("id") final Long id, @ApiInputOptional @FormDataParam("uri") final String uri, @ApiInputOptional @FormDataParam("file") final InputStream fileInputStream,
@ApiInputOptional @FormDataParam("file") final FormDataContentDisposition fileMetaData) throws Exception {
try (DBAccess db = DBAccess.createInterface()) {
if (uri != null) {
DataTools.uploadCoverFromUri(db, Artist.class, id, uri);

View File

@ -6,9 +6,9 @@ import java.util.List;
import org.bson.types.ObjectId;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.archidata.annotation.AsyncType;
import org.kar.archidata.annotation.FormDataOptional;
import org.kar.archidata.annotation.TypeScriptProgress;
import org.kar.archidata.annotation.apiGenerator.ApiAsyncType;
import org.kar.archidata.annotation.apiGenerator.ApiInputOptional;
import org.kar.archidata.annotation.apiGenerator.ApiTypeScriptProgress;
import org.kar.archidata.dataAccess.DBAccess;
import org.kar.archidata.dataAccess.DataAccess;
import org.kar.archidata.dataAccess.addOnSQL.AddOnDataJson;
@ -60,7 +60,7 @@ public class GenderResource {
@Path("{id}")
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
public Gender patch(@PathParam("id") final Long id, @AsyncType(Gender.class) final String jsonRequest) throws Exception {
public Gender patch(@PathParam("id") final Long id, @ApiAsyncType(Gender.class) final String jsonRequest) throws Exception {
DataAccess.updateWithJson(Gender.class, id, jsonRequest, new CheckFunction(CHECKER));
return DataAccess.get(Gender.class, id);
}
@ -76,9 +76,9 @@ public class GenderResource {
@Path("{id}/cover")
@RolesAllowed("ADMIN")
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@TypeScriptProgress
public Gender uploadCover(@PathParam("id") final Long id, @FormDataOptional @FormDataParam("uri") final String uri, @FormDataOptional @FormDataParam("file") final InputStream fileInputStream,
@FormDataOptional @FormDataParam("file") final FormDataContentDisposition fileMetaData) throws Exception {
@ApiTypeScriptProgress
public Gender uploadCover(@PathParam("id") final Long id, @ApiInputOptional @FormDataParam("uri") final String uri, @ApiInputOptional @FormDataParam("file") final InputStream fileInputStream,
@ApiInputOptional @FormDataParam("file") final FormDataContentDisposition fileMetaData) throws Exception {
try (DBAccess db = DBAccess.createInterface()) {
if (uri != null) {
DataTools.uploadCoverFromUri(db, Gender.class, id, uri);

View File

@ -6,7 +6,7 @@ import java.util.List;
import org.bson.types.ObjectId;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.archidata.annotation.AsyncType;
import org.kar.archidata.annotation.apiGenerator.ApiAsyncType;
import org.kar.archidata.dataAccess.DBAccess;
import org.kar.archidata.dataAccess.DataAccess;
import org.kar.archidata.dataAccess.addOnSQL.AddOnDataJson;
@ -58,7 +58,7 @@ public class PlaylistResource {
@Path("{id}")
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
public Playlist patch(@PathParam("id") final Long id, @AsyncType(Playlist.class) final String jsonRequest) throws Exception {
public Playlist patch(@PathParam("id") final Long id, @ApiAsyncType(Playlist.class) final String jsonRequest) throws Exception {
DataAccess.updateWithJson(Playlist.class, id, jsonRequest, new CheckFunction(CHECKER));
return DataAccess.get(Playlist.class, id);
}
@ -95,7 +95,7 @@ public class PlaylistResource {
@Path("{id}/cover")
@RolesAllowed("ADMIN")
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@AsyncType(Playlist.class)
@ApiAsyncType(Playlist.class)
public void uploadCover(@PathParam("id") final Long id, @FormDataParam("file") final InputStream fileInputStream, @FormDataParam("file") final FormDataContentDisposition fileMetaData)
throws Exception {
try (DBAccess db = DBAccess.createInterface()) {

View File

@ -9,9 +9,9 @@ import java.util.List;
import org.bson.types.ObjectId;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.archidata.annotation.AsyncType;
import org.kar.archidata.annotation.FormDataOptional;
import org.kar.archidata.annotation.TypeScriptProgress;
import org.kar.archidata.annotation.apiGenerator.ApiAsyncType;
import org.kar.archidata.annotation.apiGenerator.ApiInputOptional;
import org.kar.archidata.annotation.apiGenerator.ApiTypeScriptProgress;
import org.kar.archidata.dataAccess.DBAccess;
import org.kar.archidata.dataAccess.DataAccess;
import org.kar.archidata.dataAccess.addOnSQL.AddOnDataJson;
@ -65,7 +65,7 @@ public class TrackResource {
@Path("{id}")
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
public Track patch(@PathParam("id") final Long id, @AsyncType(Track.class) final String jsonRequest) throws Exception {
public Track patch(@PathParam("id") final Long id, @ApiAsyncType(Track.class) final String jsonRequest) throws Exception {
DataAccess.updateWithJson(Track.class, id, jsonRequest, new CheckFunction(CHECKER));
return DataAccess.get(Track.class, id);
}
@ -102,7 +102,7 @@ public class TrackResource {
@Path("{id}/cover")
@RolesAllowed("ADMIN")
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@TypeScriptProgress
@ApiTypeScriptProgress
public Track uploadCover(@PathParam("id") final Long id, @FormDataParam("uri") final String uri, @FormDataParam("file") final InputStream fileInputStream,
@FormDataParam("file") final FormDataContentDisposition fileMetaData) throws Exception {
try (DBAccess db = DBAccess.createInterface()) {
@ -129,14 +129,14 @@ public class TrackResource {
@Path("upload/")
@RolesAllowed("ADMIN")
@Consumes({ MediaType.MULTIPART_FORM_DATA })
@AsyncType(Track.class)
@TypeScriptProgress
@ApiAsyncType(Track.class)
@ApiTypeScriptProgress
public Response uploadTrack( //
@FormDataParam("title") String title, //
@FormDataOptional @AsyncType(Long.class) @FormDataParam("genderId") String genderId, //
@FormDataOptional @AsyncType(Long.class) @FormDataParam("artistId") String artistId, //
@FormDataOptional @AsyncType(Long.class) @FormDataParam("albumId") String albumId, //
@FormDataOptional @AsyncType(Long.class) @FormDataParam("trackId") String trackId, //
@ApiInputOptional @ApiAsyncType(Long.class) @FormDataParam("genderId") String genderId, //
@ApiInputOptional @ApiAsyncType(Long.class) @FormDataParam("artistId") String artistId, //
@ApiInputOptional @ApiAsyncType(Long.class) @FormDataParam("albumId") String albumId, //
@ApiInputOptional @ApiAsyncType(Long.class) @FormDataParam("trackId") String trackId, //
@FormDataParam("file") final InputStream fileInputStream, //
@FormDataParam("file") final FormDataContentDisposition fileMetaData //
) {

View File

@ -2,9 +2,6 @@ package org.kar.karusic.api.UserResourceModel;
import java.util.HashMap;
import org.kar.archidata.annotation.NoWriteSpecificMode;
@NoWriteSpecificMode
public class ModuleAuthorizations extends HashMap<String, PartRight> {
private static final long serialVersionUID = 1L;

View File

@ -2,11 +2,8 @@ package org.kar.karusic.api.UserResourceModel;
import java.util.Map;
import org.kar.archidata.annotation.NoWriteSpecificMode;
import io.swagger.v3.oas.annotations.media.Schema;
@NoWriteSpecificMode
public class UserMe {
public long id;
public String login;

View File

@ -10,6 +10,7 @@ import org.kar.archidata.externalRestApi.TsGenerateApi;
import org.kar.archidata.migration.MigrationSqlStep;
import org.kar.archidata.model.Data;
import org.kar.archidata.model.User;
import org.kar.archidata.model.token.JwtToken;
import org.kar.karusic.api.AlbumResource;
import org.kar.karusic.api.ArtistResource;
import org.kar.karusic.api.Front;
@ -43,6 +44,7 @@ public class Initialization extends MigrationSqlStep {
TrackResource.class, DataResource.class, ProxyResource.class);
final AnalyzeApi api = new AnalyzeApi();
api.addAllApi(listOfResources);
api.addModel(JwtToken.class);
TsGenerateApi.generateApi(api, "../front/src/back-api/");
LOGGER.info("Generate APIs (DONE)");
}

View File

@ -6,6 +6,7 @@ import java.util.List;
import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete;
@ -22,6 +23,7 @@ import jakarta.persistence.Table;
@Table(name = "album")
@DataIfNotExists
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiGenerationMode(create = true, update = true)
public class Album extends GenericDataSoftDelete {
public static class AlbumChecker extends CheckJPA<Album> {
public AlbumChecker() {

View File

@ -6,6 +6,7 @@ import java.util.List;
import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete;
@ -22,6 +23,7 @@ import jakarta.persistence.Table;
@Table(name = "artist")
@DataIfNotExists
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiGenerationMode(create = true, update = true)
public class Artist extends GenericDataSoftDelete {
public static class ArtistChecker extends CheckJPA<Artist> {
public ArtistChecker() {

View File

@ -17,6 +17,7 @@ import java.util.List;
import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete;
@ -33,6 +34,7 @@ import jakarta.persistence.Table;
@Table(name = "gender")
@DataIfNotExists
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiGenerationMode(create = true, update = true)
public class Gender extends GenericDataSoftDelete {
public static class GenderChecker extends CheckJPA<Gender> {
public GenderChecker() {

View File

@ -17,6 +17,7 @@ import java.util.List;
import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete;
@ -35,6 +36,7 @@ import jakarta.persistence.Table;
@Table(name = "playlist")
@DataIfNotExists
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiGenerationMode(create = true, update = true)
public class Playlist extends GenericDataSoftDelete {
public static class PlaylistChecker extends CheckJPA<Playlist> {
public PlaylistChecker() {

View File

@ -17,6 +17,7 @@ import java.util.List;
import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.annotation.apiGenerator.ApiGenerationMode;
import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete;
@ -33,6 +34,7 @@ import jakarta.persistence.Table;
@Table(name = "track")
@DataIfNotExists
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiGenerationMode(create = true, update = true)
public class Track extends GenericDataSoftDelete {
public static class TrackChecker extends CheckJPA<Track> {

View File

@ -29,63 +29,65 @@
"*.{ts,tsx,js,jsx,json}": "prettier --write"
},
"dependencies": {
"react-speech-recognition": "4.0.0",
"regenerator-runtime": "0.14.1",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@chakra-ui/cli": "3.7.0",
"@chakra-ui/react": "3.7.0",
"@chakra-ui/cli": "3.13.0",
"@chakra-ui/react": "3.13.0",
"@emotion/react": "11.14.0",
"allotment": "1.20.2",
"allotment": "1.20.3",
"css-mediaquery": "0.1.2",
"dayjs": "1.11.13",
"history": "5.3.0",
"next-themes": "^0.4.4",
"react": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"next-themes": "^0.4.6",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-error-boundary": "5.0.0",
"react-icons": "5.4.0",
"react-router-dom": "7.1.5",
"react-select": "5.10.0",
"react-icons": "5.5.0",
"react-router-dom": "7.4.0",
"react-select": "5.10.1",
"react-use": "17.6.0",
"zod": "3.24.1",
"zod": "3.24.2",
"zustand": "5.0.3"
},
"devDependencies": {
"@chakra-ui/styled-system": "^2.12.0",
"@playwright/test": "1.50.1",
"@storybook/addon-actions": "8.5.4",
"@storybook/addon-essentials": "8.5.4",
"@storybook/addon-links": "8.5.4",
"@storybook/addon-mdx-gfm": "8.5.4",
"@storybook/react": "8.5.4",
"@storybook/react-vite": "8.5.4",
"@storybook/theming": "8.5.4",
"@playwright/test": "1.51.1",
"@storybook/addon-actions": "8.6.8",
"@storybook/addon-essentials": "8.6.8",
"@storybook/addon-links": "8.6.8",
"@storybook/addon-mdx-gfm": "8.6.8",
"@storybook/react": "8.6.8",
"@storybook/react-vite": "8.6.8",
"@storybook/theming": "8.6.8",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/jest": "29.5.14",
"@types/node": "22.13.1",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@typescript-eslint/eslint-plugin": "8.24.0",
"@typescript-eslint/parser": "8.24.0",
"@types/node": "22.13.11",
"@types/react": "19.0.12",
"@types/react-dom": "19.0.4",
"@typescript-eslint/eslint-plugin": "8.27.0",
"@typescript-eslint/parser": "8.27.0",
"@vitejs/plugin-react": "4.3.4",
"eslint": "9.20.1",
"eslint": "9.23.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "5.1.0",
"eslint-plugin-storybook": "0.11.2",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-storybook": "0.11.6",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"knip": "5.44.0",
"lint-staged": "15.4.3",
"npm-check-updates": "^17.1.14",
"prettier": "3.5.0",
"puppeteer": "24.2.0",
"knip": "5.46.0",
"lint-staged": "15.5.0",
"npm-check-updates": "^17.1.16",
"prettier": "3.5.3",
"puppeteer": "24.4.0",
"react-is": "19.0.0",
"storybook": "8.5.4",
"storybook": "8.6.8",
"ts-node": "10.9.2",
"typescript": "5.7.3",
"vite": "6.1.0",
"vitest": "3.0.5"
"typescript": "5.8.2",
"vite": "6.2.2",
"vitest": "3.0.9"
}
}

15
front/pnpm-lock.yaml generated
View File

@ -53,9 +53,15 @@ importers:
react-select:
specifier: 5.10.0
version: 5.10.0(@types/react@19.0.8)(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)
react-speech-recognition:
specifier: 3.10.0
version: 3.10.0(react@19.0.0-rc.1)
react-use:
specifier: 17.6.0
version: 17.6.0(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1)
regenerator-runtime:
specifier: 0.14.1
version: 0.14.1
zod:
specifier: 3.24.1
version: 3.24.1
@ -3992,6 +3998,11 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-speech-recognition@3.10.0:
resolution: {integrity: sha512-EVSr4Ik8l9urwdPiK2r0+ADrLyDDrjB0qBRdUWO+w2MfwEBrj6NuRmy1GD3x7BU/V6/hab0pl8Lupen0zwlJyw==}
peerDependencies:
react: '>=16.8.0'
react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@ -9741,6 +9752,10 @@ snapshots:
- '@types/react'
- supports-color
react-speech-recognition@3.10.0(react@19.0.0-rc.1):
dependencies:
react: 19.0.0-rc.1
react-transition-group@4.4.5(react-dom@19.0.0-rc.1(react@19.0.0-rc.1))(react@19.0.0-rc.1):
dependencies:
'@babel/runtime': 7.26.7

View File

@ -1,3 +1,5 @@
import { useEffect } from 'react';
import { ErrorBoundary } from '@/errors/ErrorBoundary';
import { AudioPlayer } from './components';
@ -6,6 +8,19 @@ import { AppRoutes } from './scene/AppRoutes';
import { ServiceContextProvider } from './service/ServiceContext';
export const App = () => {
// Prevent reload the page when scroll on mobile
useEffect(() => {
const preventRefresh = (event: { preventDefault: () => void }) => {
if (window.scrollY === 0) {
event.preventDefault();
}
};
document.addEventListener('touchmove', preventRefresh, { passive: false });
return () => {
document.removeEventListener('touchmove', preventRefresh);
};
}, []);
return (
<ServiceContextProvider>
<EnvDevelopment />

View File

@ -13,7 +13,8 @@ import {
import { z as zod } from "zod"
import {
Album,
AlbumWrite,
AlbumCreate,
AlbumUpdate,
Long,
UUID,
ZodAlbum,
@ -87,7 +88,7 @@ export namespace AlbumResource {
params: {
id: Long,
},
data: AlbumWrite,
data: Partial<AlbumUpdate>,
}): Promise<Album> {
return RESTRequestJson({
restModel: {
@ -109,7 +110,7 @@ export namespace AlbumResource {
data,
}: {
restConfig: RESTConfig,
data: AlbumWrite,
data: AlbumCreate,
}): Promise<Album> {
return RESTRequestJson({
restModel: {

View File

@ -13,7 +13,8 @@ import {
import { z as zod } from "zod"
import {
Artist,
ArtistWrite,
ArtistCreate,
ArtistUpdate,
Long,
ObjectId,
ZodArtist,
@ -78,12 +79,12 @@ export namespace ArtistResource {
params: {
id: Long,
},
data: ArtistWrite,
data: ArtistUpdate,
}): Promise<Artist> {
return RESTRequestJson({
restModel: {
endPoint: "/artist/{id}",
requestType: HTTPRequestModel.PATCH,
requestType: HTTPRequestModel.PUT,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
@ -97,7 +98,7 @@ export namespace ArtistResource {
data,
}: {
restConfig: RESTConfig,
data: ArtistWrite,
data: ArtistCreate,
}): Promise<Artist> {
return RESTRequestJson({
restModel: {

View File

@ -22,7 +22,7 @@ export namespace DataResource {
restConfig,
queries,
params,
data,
headers,
}: {
restConfig: RESTConfig,
queries: {
@ -32,7 +32,9 @@ export namespace DataResource {
name: string,
oid: ObjectId,
},
data: string,
headers?: {
Range?: string,
},
}): Promise<object> {
return RESTRequestJson({
restModel: {
@ -42,7 +44,7 @@ export namespace DataResource {
restConfig,
params,
queries,
data,
headers,
});
};
/**
@ -52,7 +54,7 @@ export namespace DataResource {
restConfig,
queries,
params,
data,
headers,
}: {
restConfig: RESTConfig,
queries: {
@ -61,7 +63,9 @@ export namespace DataResource {
params: {
oid: ObjectId,
},
data: string,
headers?: {
Range: string,
},
}): Promise<object> {
return RESTRequestJson({
restModel: {
@ -71,7 +75,7 @@ export namespace DataResource {
restConfig,
params,
queries,
data,
headers,
});
};
/**
@ -81,7 +85,7 @@ export namespace DataResource {
restConfig,
queries,
params,
data,
headers,
}: {
restConfig: RESTConfig,
queries: {
@ -90,7 +94,9 @@ export namespace DataResource {
params: {
oid: ObjectId,
},
data: string,
headers?: {
Range: string,
},
}): Promise<object> {
return RESTRequestJson({
restModel: {
@ -100,7 +106,7 @@ export namespace DataResource {
restConfig,
params,
queries,
data,
headers,
});
};
/**

View File

@ -13,7 +13,8 @@ import {
import { z as zod } from "zod"
import {
Gender,
GenderWrite,
GenderCreate,
GenderUpdate,
Long,
ObjectId,
ZodGender,
@ -78,7 +79,7 @@ export namespace GenderResource {
params: {
id: Long,
},
data: GenderWrite,
data: Partial<GenderUpdate>,
}): Promise<Gender> {
return RESTRequestJson({
restModel: {
@ -97,7 +98,7 @@ export namespace GenderResource {
data,
}: {
restConfig: RESTConfig,
data: GenderWrite,
data: GenderCreate,
}): Promise<Gender> {
return RESTRequestJson({
restModel: {

View File

@ -14,7 +14,8 @@ import {
Long,
ObjectId,
Playlist,
PlaylistWrite,
PlaylistCreate,
PlaylistUpdate,
ZodPlaylist,
isPlaylist,
} from "../model";
@ -98,7 +99,7 @@ export namespace PlaylistResource {
params: {
id: Long,
},
data: PlaylistWrite,
data: Partial<PlaylistUpdate>,
}): Promise<Playlist> {
return RESTRequestJson({
restModel: {
@ -117,7 +118,7 @@ export namespace PlaylistResource {
data,
}: {
restConfig: RESTConfig,
data: PlaylistWrite,
data: PlaylistCreate,
}): Promise<Playlist> {
return RESTRequestJson({
restModel: {

View File

@ -15,7 +15,8 @@ import {
Long,
ObjectId,
Track,
TrackWrite,
TrackCreate,
TrackUpdate,
ZodTrack,
isTrack,
} from "../model";
@ -99,7 +100,7 @@ export namespace TrackResource {
params: {
id: Long,
},
data: TrackWrite,
data: Partial<TrackUpdate>,
}): Promise<Track> {
return RESTRequestJson({
restModel: {
@ -118,7 +119,7 @@ export namespace TrackResource {
data,
}: {
restConfig: RESTConfig,
data: TrackWrite,
data: TrackCreate,
}): Promise<Track> {
return RESTRequestJson({
restModel: {

View File

@ -5,7 +5,7 @@ import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodLocalDate} from "./local-date";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteUpdate , ZodGenericDataSoftDeleteCreate } from "./generic-data-soft-delete";
export const ZodAlbum = ZodGenericDataSoftDelete.extend({
name: zod.string().optional(),
@ -29,7 +29,7 @@ export function isAlbum(data: any): data is Album {
return false;
}
}
export const ZodAlbumWrite = ZodGenericDataSoftDeleteWrite.extend({
export const ZodAlbumUpdate = ZodGenericDataSoftDeleteUpdate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
@ -40,14 +40,36 @@ export const ZodAlbumWrite = ZodGenericDataSoftDeleteWrite.extend({
});
export type AlbumWrite = zod.infer<typeof ZodAlbumWrite>;
export type AlbumUpdate = zod.infer<typeof ZodAlbumUpdate>;
export function isAlbumWrite(data: any): data is AlbumWrite {
export function isAlbumUpdate(data: any): data is AlbumUpdate {
try {
ZodAlbumWrite.parse(data);
ZodAlbumUpdate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodAlbumWrite' error=${e}`);
console.log(`Fail to parse data type='ZodAlbumUpdate' error=${e}`);
return false;
}
}
export const ZodAlbumCreate = ZodGenericDataSoftDeleteCreate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
publication: ZodLocalDate.nullable().optional(),
});
export type AlbumCreate = zod.infer<typeof ZodAlbumCreate>;
export function isAlbumCreate(data: any): data is AlbumCreate {
try {
ZodAlbumCreate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodAlbumCreate' error=${e}`);
return false;
}
}

View File

@ -5,7 +5,7 @@ import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodLocalDate} from "./local-date";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteUpdate , ZodGenericDataSoftDeleteCreate } from "./generic-data-soft-delete";
export const ZodArtist = ZodGenericDataSoftDelete.extend({
name: zod.string().optional(),
@ -32,7 +32,7 @@ export function isArtist(data: any): data is Artist {
return false;
}
}
export const ZodArtistWrite = ZodGenericDataSoftDeleteWrite.extend({
export const ZodArtistUpdate = ZodGenericDataSoftDeleteUpdate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
@ -46,14 +46,39 @@ export const ZodArtistWrite = ZodGenericDataSoftDeleteWrite.extend({
});
export type ArtistWrite = zod.infer<typeof ZodArtistWrite>;
export type ArtistUpdate = zod.infer<typeof ZodArtistUpdate>;
export function isArtistWrite(data: any): data is ArtistWrite {
export function isArtistUpdate(data: any): data is ArtistUpdate {
try {
ZodArtistWrite.parse(data);
ZodArtistUpdate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodArtistWrite' error=${e}`);
console.log(`Fail to parse data type='ZodArtistUpdate' error=${e}`);
return false;
}
}
export const ZodArtistCreate = ZodGenericDataSoftDeleteCreate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
firstName: zod.string().nullable().optional(),
surname: zod.string().nullable().optional(),
birth: ZodLocalDate.nullable().optional(),
death: ZodLocalDate.nullable().optional(),
});
export type ArtistCreate = zod.infer<typeof ZodArtistCreate>;
export function isArtistCreate(data: any): data is ArtistCreate {
try {
ZodArtistCreate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodArtistCreate' error=${e}`);
return false;
}
}

View File

@ -4,7 +4,7 @@
import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteUpdate , ZodGenericDataSoftDeleteCreate } from "./generic-data-soft-delete";
export const ZodGender = ZodGenericDataSoftDelete.extend({
name: zod.string().optional(),
@ -27,7 +27,7 @@ export function isGender(data: any): data is Gender {
return false;
}
}
export const ZodGenderWrite = ZodGenericDataSoftDeleteWrite.extend({
export const ZodGenderUpdate = ZodGenericDataSoftDeleteUpdate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
@ -37,14 +37,35 @@ export const ZodGenderWrite = ZodGenericDataSoftDeleteWrite.extend({
});
export type GenderWrite = zod.infer<typeof ZodGenderWrite>;
export type GenderUpdate = zod.infer<typeof ZodGenderUpdate>;
export function isGenderWrite(data: any): data is GenderWrite {
export function isGenderUpdate(data: any): data is GenderUpdate {
try {
ZodGenderWrite.parse(data);
ZodGenderUpdate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenderWrite' error=${e}`);
console.log(`Fail to parse data type='ZodGenderUpdate' error=${e}`);
return false;
}
}
export const ZodGenderCreate = ZodGenericDataSoftDeleteCreate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
});
export type GenderCreate = zod.infer<typeof ZodGenderCreate>;
export function isGenderCreate(data: any): data is GenderCreate {
try {
ZodGenderCreate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenderCreate' error=${e}`);
return false;
}
}

View File

@ -3,7 +3,7 @@
*/
import { z as zod } from "zod";
import {ZodGenericData, ZodGenericDataWrite } from "./generic-data";
import {ZodGenericData, ZodGenericDataUpdate , ZodGenericDataCreate } from "./generic-data";
export const ZodGenericDataSoftDelete = ZodGenericData.extend({
/**
@ -24,16 +24,29 @@ export function isGenericDataSoftDelete(data: any): data is GenericDataSoftDelet
return false;
}
}
export const ZodGenericDataSoftDeleteWrite = ZodGenericDataWrite;
export const ZodGenericDataSoftDeleteUpdate = ZodGenericDataUpdate;
export type GenericDataSoftDeleteWrite = zod.infer<typeof ZodGenericDataSoftDeleteWrite>;
export type GenericDataSoftDeleteUpdate = zod.infer<typeof ZodGenericDataSoftDeleteUpdate>;
export function isGenericDataSoftDeleteWrite(data: any): data is GenericDataSoftDeleteWrite {
export function isGenericDataSoftDeleteUpdate(data: any): data is GenericDataSoftDeleteUpdate {
try {
ZodGenericDataSoftDeleteWrite.parse(data);
ZodGenericDataSoftDeleteUpdate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataSoftDeleteWrite' error=${e}`);
console.log(`Fail to parse data type='ZodGenericDataSoftDeleteUpdate' error=${e}`);
return false;
}
}
export const ZodGenericDataSoftDeleteCreate = ZodGenericDataCreate;
export type GenericDataSoftDeleteCreate = zod.infer<typeof ZodGenericDataSoftDeleteCreate>;
export function isGenericDataSoftDeleteCreate(data: any): data is GenericDataSoftDeleteCreate {
try {
ZodGenericDataSoftDeleteCreate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataSoftDeleteCreate' error=${e}`);
return false;
}
}

View File

@ -4,7 +4,7 @@
import { z as zod } from "zod";
import {ZodLong} from "./long";
import {ZodGenericTiming, ZodGenericTimingWrite } from "./generic-timing";
import {ZodGenericTiming, ZodGenericTimingUpdate , ZodGenericTimingCreate } from "./generic-timing";
export const ZodGenericData = ZodGenericTiming.extend({
/**
@ -25,16 +25,29 @@ export function isGenericData(data: any): data is GenericData {
return false;
}
}
export const ZodGenericDataWrite = ZodGenericTimingWrite;
export const ZodGenericDataUpdate = ZodGenericTimingUpdate;
export type GenericDataWrite = zod.infer<typeof ZodGenericDataWrite>;
export type GenericDataUpdate = zod.infer<typeof ZodGenericDataUpdate>;
export function isGenericDataWrite(data: any): data is GenericDataWrite {
export function isGenericDataUpdate(data: any): data is GenericDataUpdate {
try {
ZodGenericDataWrite.parse(data);
ZodGenericDataUpdate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataWrite' error=${e}`);
console.log(`Fail to parse data type='ZodGenericDataUpdate' error=${e}`);
return false;
}
}
export const ZodGenericDataCreate = ZodGenericTimingCreate;
export type GenericDataCreate = zod.infer<typeof ZodGenericDataCreate>;
export function isGenericDataCreate(data: any): data is GenericDataCreate {
try {
ZodGenericDataCreate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataCreate' error=${e}`);
return false;
}
}

View File

@ -28,18 +28,33 @@ export function isGenericTiming(data: any): data is GenericTiming {
return false;
}
}
export const ZodGenericTimingWrite = zod.object({
export const ZodGenericTimingUpdate = zod.object({
});
export type GenericTimingWrite = zod.infer<typeof ZodGenericTimingWrite>;
export type GenericTimingUpdate = zod.infer<typeof ZodGenericTimingUpdate>;
export function isGenericTimingWrite(data: any): data is GenericTimingWrite {
export function isGenericTimingUpdate(data: any): data is GenericTimingUpdate {
try {
ZodGenericTimingWrite.parse(data);
ZodGenericTimingUpdate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericTimingWrite' error=${e}`);
console.log(`Fail to parse data type='ZodGenericTimingUpdate' error=${e}`);
return false;
}
}
export const ZodGenericTimingCreate = zod.object({
});
export type GenericTimingCreate = zod.infer<typeof ZodGenericTimingCreate>;
export function isGenericTimingCreate(data: any): data is GenericTimingCreate {
try {
ZodGenericTimingCreate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericTimingCreate' error=${e}`);
return false;
}
}

View File

@ -19,18 +19,3 @@ export function isHealthResult(data: any): data is HealthResult {
return false;
}
}
export const ZodHealthResultWrite = zod.object({
});
export type HealthResultWrite = zod.infer<typeof ZodHealthResultWrite>;
export function isHealthResultWrite(data: any): data is HealthResultWrite {
try {
ZodHealthResultWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodHealthResultWrite' error=${e}`);
return false;
}
}

View File

@ -10,12 +10,16 @@ export * from "./generic-timing"
export * from "./health-result"
export * from "./integer"
export * from "./iso-date"
export * from "./jwt-header"
export * from "./jwt-payload"
export * from "./jwt-token"
export * from "./local-date"
export * from "./long"
export * from "./object-id"
export * from "./part-right"
export * from "./playlist"
export * from "./rest-error-response"
export * from "./rest-input-error"
export * from "./timestamp"
export * from "./track"
export * from "./user"

View File

@ -0,0 +1,23 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodJwtHeader = zod.object({
typ: zod.string().max(128),
alg: zod.string().max(128),
});
export type JwtHeader = zod.infer<typeof ZodJwtHeader>;
export function isJwtHeader(data: any): data is JwtHeader {
try {
ZodJwtHeader.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodJwtHeader' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,29 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
export const ZodJwtPayload = zod.object({
sub: zod.string(),
application: zod.string(),
iss: zod.string(),
right: zod.record(zod.string(), zod.record(zod.string(), ZodLong)),
login: zod.string(),
exp: ZodLong,
iat: ZodLong,
});
export type JwtPayload = zod.infer<typeof ZodJwtPayload>;
export function isJwtPayload(data: any): data is JwtPayload {
try {
ZodJwtPayload.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodJwtPayload' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,26 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodJwtHeader} from "./jwt-header";
import {ZodJwtPayload} from "./jwt-payload";
export const ZodJwtToken = zod.object({
header: ZodJwtHeader,
payload: ZodJwtPayload,
signature: zod.string(),
});
export type JwtToken = zod.infer<typeof ZodJwtToken>;
export function isJwtToken(data: any): data is JwtToken {
try {
ZodJwtToken.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodJwtToken' error=${e}`);
return false;
}
}

View File

@ -4,7 +4,7 @@
import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteUpdate , ZodGenericDataSoftDeleteCreate } from "./generic-data-soft-delete";
export const ZodPlaylist = ZodGenericDataSoftDelete.extend({
name: zod.string().optional(),
@ -28,25 +28,47 @@ export function isPlaylist(data: any): data is Playlist {
return false;
}
}
export const ZodPlaylistWrite = ZodGenericDataSoftDeleteWrite.extend({
export const ZodPlaylistUpdate = ZodGenericDataSoftDeleteUpdate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
tracks: zod.array(ZodObjectId).optional(),
tracks: zod.array(ZodObjectId),
});
export type PlaylistWrite = zod.infer<typeof ZodPlaylistWrite>;
export type PlaylistUpdate = zod.infer<typeof ZodPlaylistUpdate>;
export function isPlaylistWrite(data: any): data is PlaylistWrite {
export function isPlaylistUpdate(data: any): data is PlaylistUpdate {
try {
ZodPlaylistWrite.parse(data);
ZodPlaylistUpdate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodPlaylistWrite' error=${e}`);
console.log(`Fail to parse data type='ZodPlaylistUpdate' error=${e}`);
return false;
}
}
export const ZodPlaylistCreate = ZodGenericDataSoftDeleteCreate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
tracks: zod.array(ZodObjectId),
});
export type PlaylistCreate = zod.infer<typeof ZodPlaylistCreate>;
export function isPlaylistCreate(data: any): data is PlaylistCreate {
try {
ZodPlaylistCreate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodPlaylistCreate' error=${e}`);
return false;
}
}

View File

@ -5,6 +5,7 @@ import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodInteger} from "./integer";
import {ZodRestInputError} from "./rest-input-error";
export const ZodRestErrorResponse = zod.object({
oid: ZodObjectId.optional(),
@ -13,6 +14,7 @@ export const ZodRestErrorResponse = zod.object({
time: zod.string(),
status: ZodInteger,
statusMessage: zod.string(),
inputError: zod.array(ZodRestInputError).optional(),
});

View File

@ -0,0 +1,24 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodRestInputError = zod.object({
argument: zod.string().optional(),
path: zod.string().optional(),
message: zod.string(),
});
export type RestInputError = zod.infer<typeof ZodRestInputError>;
export function isRestInputError(data: any): data is RestInputError {
try {
ZodRestInputError.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodRestInputError' error=${e}`);
return false;
}
}

View File

@ -5,7 +5,7 @@ import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodLong} from "./long";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteUpdate , ZodGenericDataSoftDeleteCreate } from "./generic-data-soft-delete";
export const ZodTrack = ZodGenericDataSoftDelete.extend({
name: zod.string().optional(),
@ -33,7 +33,7 @@ export function isTrack(data: any): data is Track {
return false;
}
}
export const ZodTrackWrite = ZodGenericDataSoftDeleteWrite.extend({
export const ZodTrackUpdate = ZodGenericDataSoftDeleteUpdate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
@ -44,18 +44,44 @@ export const ZodTrackWrite = ZodGenericDataSoftDeleteWrite.extend({
albumId: ZodLong.nullable().optional(),
track: ZodLong.nullable().optional(),
dataId: ZodObjectId.nullable().optional(),
artists: zod.array(ZodLong).optional(),
artists: zod.array(ZodLong),
});
export type TrackWrite = zod.infer<typeof ZodTrackWrite>;
export type TrackUpdate = zod.infer<typeof ZodTrackUpdate>;
export function isTrackWrite(data: any): data is TrackWrite {
export function isTrackUpdate(data: any): data is TrackUpdate {
try {
ZodTrackWrite.parse(data);
ZodTrackUpdate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodTrackWrite' error=${e}`);
console.log(`Fail to parse data type='ZodTrackUpdate' error=${e}`);
return false;
}
}
export const ZodTrackCreate = ZodGenericDataSoftDeleteCreate.extend({
name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
genderId: ZodLong.nullable().optional(),
albumId: ZodLong.nullable().optional(),
track: ZodLong.nullable().optional(),
dataId: ZodObjectId.nullable().optional(),
artists: zod.array(ZodLong),
});
export type TrackCreate = zod.infer<typeof ZodTrackCreate>;
export function isTrackCreate(data: any): data is TrackCreate {
try {
ZodTrackCreate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodTrackCreate' error=${e}`);
return false;
}
}

View File

@ -3,7 +3,7 @@
*/
import { z as zod } from "zod";
import {ZodUser, ZodUserWrite } from "./user";
import {ZodUser, ZodUserUpdate , ZodUserCreate } from "./user";
export const ZodUserKarusic = ZodUser;
@ -18,16 +18,3 @@ export function isUserKarusic(data: any): data is UserKarusic {
return false;
}
}
export const ZodUserKarusicWrite = ZodUserWrite;
export type UserKarusicWrite = zod.infer<typeof ZodUserKarusicWrite>;
export function isUserKarusicWrite(data: any): data is UserKarusicWrite {
try {
ZodUserKarusicWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserKarusicWrite' error=${e}`);
return false;
}
}

View File

@ -5,7 +5,7 @@ import { z as zod } from "zod";
import {ZodTimestamp} from "./timestamp";
import {ZodUUID} from "./uuid";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteUpdate , ZodGenericDataSoftDeleteCreate } from "./generic-data-soft-delete";
export const ZodUser = ZodGenericDataSoftDelete.extend({
login: zod.string().min(3).max(128),
@ -30,8 +30,8 @@ export function isUser(data: any): data is User {
return false;
}
}
export const ZodUserWrite = ZodGenericDataSoftDeleteWrite.extend({
login: zod.string().min(3).max(128).optional(),
export const ZodUserUpdate = ZodGenericDataSoftDeleteUpdate.extend({
login: zod.string().min(3).max(128),
lastConnection: ZodTimestamp.nullable().optional(),
blocked: zod.boolean().nullable().optional(),
blockedReason: zod.string().max(512).nullable().optional(),
@ -42,14 +42,37 @@ export const ZodUserWrite = ZodGenericDataSoftDeleteWrite.extend({
});
export type UserWrite = zod.infer<typeof ZodUserWrite>;
export type UserUpdate = zod.infer<typeof ZodUserUpdate>;
export function isUserWrite(data: any): data is UserWrite {
export function isUserUpdate(data: any): data is UserUpdate {
try {
ZodUserWrite.parse(data);
ZodUserUpdate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserWrite' error=${e}`);
console.log(`Fail to parse data type='ZodUserUpdate' error=${e}`);
return false;
}
}
export const ZodUserCreate = ZodGenericDataSoftDeleteCreate.extend({
login: zod.string().min(3).max(128),
lastConnection: ZodTimestamp.nullable().optional(),
blocked: zod.boolean().nullable().optional(),
blockedReason: zod.string().max(512).nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
});
export type UserCreate = zod.infer<typeof ZodUserCreate>;
export function isUserCreate(data: any): data is UserCreate {
try {
ZodUserCreate.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserCreate' error=${e}`);
return false;
}
}

View File

@ -3,30 +3,29 @@
* @copyright 2024, Edouard DUPIN, all right reserved
* @license MPL-2
*/
import { RestErrorResponse, isRestErrorResponse } from "./model";
import { RestErrorResponse, isRestErrorResponse } from './model';
export enum HTTPRequestModel {
ARCHIVE = "ARCHIVE",
DELETE = "DELETE",
HEAD = "HEAD",
GET = "GET",
OPTION = "OPTION",
PATCH = "PATCH",
POST = "POST",
PUT = "PUT",
RESTORE = "RESTORE",
ARCHIVE = 'ARCHIVE',
DELETE = 'DELETE',
HEAD = 'HEAD',
GET = 'GET',
OPTION = 'OPTION',
PATCH = 'PATCH',
POST = 'POST',
PUT = 'PUT',
RESTORE = 'RESTORE',
}
export enum HTTPMimeType {
ALL = "*/*",
CSV = "text/csv",
IMAGE = "image/*",
IMAGE_JPEG = "image/jpeg",
IMAGE_PNG = "image/png",
JSON = "application/json",
MULTIPART = "multipart/form-data",
OCTET_STREAM = "application/octet-stream",
TEXT_PLAIN = "text/plain",
ALL = '*/*',
CSV = 'text/csv',
IMAGE = 'image/*',
IMAGE_JPEG = 'image/jpeg',
IMAGE_PNG = 'image/png',
JSON = 'application/json',
MULTIPART = 'multipart/form-data',
OCTET_STREAM = 'application/octet-stream',
TEXT_PLAIN = 'text/plain',
}
export interface RESTConfig {
@ -54,6 +53,14 @@ export interface ModelResponseHttp {
data: any;
}
export type ErrorRestApiCallback = (response: Response) => void;
let errorApiGlobalCallback: ErrorRestApiCallback | undefined = undefined;
export const setErrorApiGlobalCallback = (callback: ErrorRestApiCallback) => {
errorApiGlobalCallback = callback;
};
function isNullOrUndefined(data: any): data is undefined | null {
return data === undefined || data === null;
}
@ -78,6 +85,7 @@ export interface RESTRequestType {
data?: any;
params?: object;
queries?: object;
headers?: any;
callbacks?: RESTCallbacks;
}
@ -87,15 +95,15 @@ function replaceAll(input, searchValue, replaceValue) {
function removeTrailingSlashes(input: string): string {
if (isNullOrUndefined(input)) {
return "undefined";
return 'undefined';
}
return input.replace(/\/+$/, "");
return input.replace(/\/+$/, '');
}
function removeLeadingSlashes(input: string): string {
if (isNullOrUndefined(input)) {
return "";
return '';
}
return input.replace(/^\/+/, "");
return input.replace(/^\/+/, '');
}
export function RESTUrl({
@ -133,9 +141,9 @@ export function RESTUrl({
}
}
if (restConfig.token !== undefined && restModel.tokenInUrl === true) {
searchParams.append("Authorization", `Bearer ${restConfig.token}`);
searchParams.append('Authorization', `Bearer ${restConfig.token}`);
}
return generateUrl + "?" + searchParams.toString();
return generateUrl + '?' + searchParams.toString();
}
export function fetchProgress(
@ -159,7 +167,7 @@ export function fetchProgress(
return new Promise((resolve, reject) => {
// Stream the upload progress
if (progressUpload) {
xhr.io?.upload.addEventListener("progress", (dataEvent) => {
xhr.io?.upload.addEventListener('progress', (dataEvent) => {
if (dataEvent.lengthComputable) {
progressUpload(dataEvent.loaded, dataEvent.total);
}
@ -167,7 +175,7 @@ export function fetchProgress(
}
// Stream the download progress
if (progressDownload) {
xhr.io?.addEventListener("progress", (dataEvent) => {
xhr.io?.addEventListener('progress', (dataEvent) => {
if (dataEvent.lengthComputable) {
progressDownload(dataEvent.loaded, dataEvent.total);
}
@ -187,19 +195,19 @@ export function fetchProgress(
};
}
// Check if we have an internal Fail:
xhr.io?.addEventListener("error", () => {
xhr.io?.addEventListener('error', () => {
xhr.io = undefined;
reject(new TypeError("Failed to fetch"));
reject(new TypeError('Failed to fetch'));
});
// Capture the end of the stream
xhr.io?.addEventListener("loadend", () => {
xhr.io?.addEventListener('loadend', () => {
if (xhr.io?.readyState !== XMLHttpRequest.DONE) {
return;
}
if (xhr.io?.status === 0) {
//the stream has been aborted
reject(new TypeError("Fetch has been aborted"));
reject(new TypeError('Fetch has been aborted'));
return;
}
// Stream is ended, transform in a generic response:
@ -209,17 +217,17 @@ export function fetchProgress(
});
const headersArray = replaceAll(
xhr.io.getAllResponseHeaders().trim(),
"\r\n",
"\n"
).split("\n");
'\r\n',
'\n'
).split('\n');
headersArray.forEach(function (header) {
const firstColonIndex = header.indexOf(":");
const firstColonIndex = header.indexOf(':');
if (firstColonIndex !== -1) {
const key = header.substring(0, firstColonIndex).trim();
const value = header.substring(firstColonIndex + 1).trim();
response.headers.set(key, value);
} else {
response.headers.set(header, "");
response.headers.set(header, '');
}
});
xhr.io = undefined;
@ -241,27 +249,29 @@ export function RESTRequest({
data,
params,
queries,
headers = {},
callbacks,
}: RESTRequestType): Promise<ModelResponseHttp> {
// Create the URL PATH:
let generateUrl = RESTUrl({ restModel, restConfig, data, params, queries });
let headers: any = {};
if (restConfig.token !== undefined && restModel.tokenInUrl !== true) {
headers["Authorization"] = `Bearer ${restConfig.token}`;
headers['Authorization'] = `Bearer ${restConfig.token}`;
}
if (restModel.accept !== undefined) {
headers["Accept"] = restModel.accept;
headers['Accept'] = restModel.accept;
}
if (restModel.requestType !== HTTPRequestModel.GET &&
restModel.requestType !== HTTPRequestModel.ARCHIVE &&
restModel.requestType !== HTTPRequestModel.RESTORE
if (
restModel.requestType !== HTTPRequestModel.GET &&
restModel.requestType !== HTTPRequestModel.ARCHIVE &&
restModel.requestType !== HTTPRequestModel.RESTORE
) {
// if Get we have not a content type, the body is empty
if (restModel.contentType !== HTTPMimeType.MULTIPART &&
restModel.contentType !== undefined
) {
if (
restModel.contentType !== HTTPMimeType.MULTIPART &&
restModel.contentType !== undefined
) {
// special case of multi-part ==> no content type otherwise the browser does not set the ";bundary=--****"
headers["Content-Type"] = restModel.contentType;
headers['Content-Type'] = restModel.contentType;
}
}
let body = data;
@ -302,19 +312,27 @@ export function RESTRequest({
}
action
.then((response: Response) => {
if (
errorApiGlobalCallback &&
400 <= response.status &&
response.status <= 499
) {
// Detect an error and trigger the generic error callback:
errorApiGlobalCallback(response);
}
if (response.status >= 200 && response.status <= 299) {
const contentType = response.headers.get("Content-Type");
const contentType = response.headers.get('Content-Type');
if (
!isNullOrUndefined(restModel.accept) &&
restModel.accept !== contentType
) {
reject({
name: "Model accept type incompatible",
name: 'Model accept type incompatible',
time: Date().toString(),
status: 901,
message: `REST Content type are not compatible: ${restModel.accept} != ${contentType}`,
statusMessage: "Fetch error",
error: "rest-tools.ts Wrong type in the message return type",
statusMessage: 'Fetch error',
error: 'rest-tools.ts Wrong type in the message return type',
} as RestErrorResponse);
} else if (contentType === HTTPMimeType.JSON) {
response
@ -324,12 +342,12 @@ export function RESTRequest({
})
.catch((reason: Error) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: 902,
message: `REST parse json fail: ${reason}`,
statusMessage: "Fetch parse error",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch parse error',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
});
} else {
@ -349,22 +367,22 @@ export function RESTRequest({
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: 903,
message: `REST parse error json with wrong type fail. ${dataError}`,
statusMessage: "Fetch parse error",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch parse error',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR parse error",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch ERROR parse error',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
});
}
@ -374,22 +392,22 @@ export function RESTRequest({
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ${dataError} with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT parse error",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch ERROR TEXT parse error',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
name: 'API serialization error',
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT FAIL",
error: "rest-tools.ts Wrong message model to parse",
statusMessage: 'Fetch ERROR TEXT FAIL',
error: 'rest-tools.ts Wrong message model to parse',
} as RestErrorResponse);
});
});
@ -400,12 +418,12 @@ export function RESTRequest({
reject(error);
} else {
reject({
name: "Request fail",
name: 'Request fail',
time: Date(),
status: 999,
message: error,
statusMessage: "Fetch catch error",
error: "rest-tools.ts detect an error in the fetch request",
statusMessage: 'Fetch catch error',
error: 'rest-tools.ts detect an error in the fetch request',
});
}
});
@ -426,12 +444,12 @@ export function RESTRequestJson<TYPE>(
resolve(value.data);
} else {
reject({
name: "Model check fail",
name: 'Model check fail',
time: Date().toString(),
status: 950,
error: "REST Fail to verify the data",
statusMessage: "API cast ERROR",
message: "api.ts Check type as fail",
error: 'REST Fail to verify the data',
statusMessage: 'API cast ERROR',
message: 'api.ts Check type as fail',
} as RestErrorResponse);
}
})

View File

@ -1,26 +1,61 @@
import { ReactNode } from 'react';
import { Box, Button, ConditionalValue, Flex, HStack, IconButton, Span, Text, useDisclosure } from '@chakra-ui/react';
import { LuAlignJustify, LuArrowBigLeft, LuCircleUserRound, LuKeySquare, LuLogIn, LuLogOut, LuMoon, LuSettings, LuSun } from 'react-icons/lu';
import { MdHelp, MdHome, MdMore, MdOutlinePlaylistPlay, MdOutlineUploadFile, MdSupervisedUserCircle } from 'react-icons/md';
import {
Box,
Button,
ConditionalValue,
Flex,
HStack,
IconButton,
Span,
Text,
useBreakpointValue,
useDisclosure,
} from '@chakra-ui/react';
import {
LuAlignJustify,
LuArrowBigLeft,
LuCircleUserRound,
LuKeySquare,
LuLogIn,
LuLogOut,
LuMoon,
LuSettings,
LuSun,
} from 'react-icons/lu';
import {
MdHelp,
MdHome,
MdMore,
MdOutlinePlaylistPlay,
MdOutlineUploadFile,
MdSupervisedUserCircle,
} from 'react-icons/md';
import { useNavigate } from 'react-router-dom';
import { useColorMode, useColorModeValue } from '@/components/ui/color-mode';
import { DrawerBody, DrawerContent, DrawerHeader, DrawerRoot } from '@/components/ui/drawer';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '@/components/ui/menu';
import {
DrawerBody,
DrawerContent,
DrawerHeader,
DrawerRoot,
} from '@/components/ui/drawer';
import {
MenuContent,
MenuItem,
MenuRoot,
MenuTrigger,
} from '@/components/ui/menu';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionState } from '@/service/SessionState';
import { useSessionService } from '@/service/session';
import { colors } from '@/theme/colors';
import { requestOpenSite, requestSignIn, requestSignOut, requestSignUp } from '@/utils/sso';
import {
requestOpenSite,
requestSignIn,
requestSignOut,
requestSignUp,
} from '@/utils/sso';
export const TOP_BAR_HEIGHT = '50px';
@ -37,6 +72,8 @@ export const BUTTON_TOP_BAR_PROPERTY = {
export type TopBarProps = {
children?: ReactNode;
title?: string;
titleLink?: string;
titleIcon?: ReactNode;
};
const ButtonMenuLeft = ({
@ -74,7 +111,12 @@ const ButtonMenuLeft = ({
</>
);
};
export const TopBar = ({ title, children }: TopBarProps) => {
export const TopBar = ({
title,
titleLink,
titleIcon,
children,
}: TopBarProps) => {
const navigate = useNavigate();
const { colorMode, toggleColorMode } = useColorMode();
const { session } = useServiceContext();
@ -99,8 +141,10 @@ export const TopBar = ({ title, children }: TopBarProps) => {
const onKarso = (): void => {
requestOpenSite();
};
const isVisible = useBreakpointValue({ base: false, md: true });
return (
<Flex
minWidth="320px"
position="absolute"
top={0}
left={0}
@ -117,21 +161,28 @@ export const TopBar = ({ title, children }: TopBarProps) => {
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onChangeTheme}>
<HStack>
<LuAlignJustify />
<Text paddingLeft="3px" fontWeight="bold">
Karusic
</Text>
{isVisible && (
<Text paddingLeft="3px" fontWeight="bold">
Karusic
</Text>
)}
</HStack>
</Button>
{title && (
<Text
truncate
fontSize="20px"
fontWeight="bold"
textTransform="uppercase"
marginRight="auto"
userSelect="none"
color="brand.500"
onClick={titleLink ? () => navigate(titleLink) : undefined}
>
{title}
<Flex gap="4px">
{titleIcon}
{title}
</Flex>
</Text>
)}
{children}
@ -264,4 +315,4 @@ export const TopBar = ({ title, children }: TopBarProps) => {
</DrawerRoot>
</Flex>
);
};
};

View File

@ -9,7 +9,7 @@ import {
} from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom';
import { AlbumResource, AlbumWrite } from '@/back-api';
import { AlbumResource, AlbumUpdate } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers';
import { FormGroupShow } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput';
@ -64,11 +64,11 @@ export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => {
};
const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<AlbumWrite>({
const form = useFormidable<Partial<AlbumUpdate>>({
initialValues: dataAlbum,
deltaConfig: { omit: ['covers'] },
});
const onSave = async (deltaData: AlbumWrite) => {
const onSave = async (deltaData: Partial<AlbumUpdate>) => {
if (isNullOrUndefined(albumIdInt)) {
return;
}

View File

@ -9,7 +9,7 @@ import {
} from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom';
import { ArtistResource, ArtistWrite } from '@/back-api';
import { ArtistResource, ArtistUpdate } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers';
import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea';
@ -63,11 +63,11 @@ export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => {
};
const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<ArtistWrite>({
const form = useFormidable<Partial<ArtistUpdate>>({
initialValues: dataArtist,
deltaConfig: { omit: ['covers'] },
});
const onSave = async (dataDelta: ArtistWrite) => {
const onSave = async (dataDelta: Partial<ArtistUpdate>) => {
if (isNullOrUndefined(artistIdInt)) {
return;
}

View File

@ -9,7 +9,7 @@ import {
} from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom';
import { GenderResource, GenderWrite } from '@/back-api';
import { GenderResource, GenderUpdate } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers';
import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea';
@ -63,11 +63,11 @@ export const GenderEditPopUp = ({}: GenderEditPopUpProps) => {
};
const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<GenderWrite>({
const form = useFormidable<Partial<GenderUpdate>>({
initialValues: dataGender,
deltaConfig: { omit: ['covers'] },
});
const onSave = async (dataDelta: GenderWrite) => {
const onSave = async (dataDelta: Partial<GenderUpdate>) => {
if (isNullOrUndefined(genderIdInt)) {
return;
}

View File

@ -4,7 +4,7 @@ import { Button, Text, useDisclosure } from '@chakra-ui/react';
import { MdAdminPanelSettings, MdDeleteForever, MdEdit } from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom';
import { TrackResource, TrackWrite } from '@/back-api';
import { TrackResource, TrackUpdate } from '@/back-api';
import { FormGroupShow } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput';
import { FormNumber } from '@/components/form/FormNumber';
@ -64,11 +64,11 @@ export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
};
const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<TrackWrite>({
const form = useFormidable<Partial<TrackUpdate>>({
initialValues: dataTrack,
deltaConfig: { omit: ['covers'] },
});
const onSave = async (dataDelta: TrackWrite) => {
const onSave = async (dataDelta: Partial<TrackUpdate>) => {
if (isNullOrUndefined(trackIdInt)) {
return;
}

View File

@ -45,7 +45,7 @@ export const AlbumDetailPage = () => {
if (!dataAlbum) {
return (
<>
<TopBar title="Album detail" />
<TopBar title="Album detail" titleLink="/album" />
<PageLayoutInfoCenter>
Fail to load artist id: {albumId}
</PageLayoutInfoCenter>
@ -54,7 +54,7 @@ export const AlbumDetailPage = () => {
}
return (
<>
<TopBar title="Album detail">
<TopBar title="Album detail" titleLink="/album">
<Button
{...BUTTON_TOP_BAR_PROPERTY}
onClick={() =>

View File

@ -28,7 +28,7 @@ export const AlbumsPage = () => {
if (isLoading) {
return (
<>
<TopBar title="All Albums" />
<TopBar title="All Albums" titleLink="/home" />
<PageLayoutInfoCenter>No Album available</PageLayoutInfoCenter>
</>
);
@ -36,7 +36,7 @@ export const AlbumsPage = () => {
return (
<>
<TopBar title="All Albums">
<TopBar title="All Albums" titleLink="/home">
<SearchInput onChange={setFilterTitle} />
</TopBar>
<PageLayout>

View File

@ -55,33 +55,27 @@ export const ArtistAlbumDetailPage = () => {
}
return (
<>
<TopBar title={dataArtist ? undefined : 'Album detail'}>
<TopBar
title={dataArtist ? dataArtist?.name : 'Album detail'}
titleLink={dataArtist ? `/artist/${dataArtist.id}` : undefined}
titleIcon={
<Covers
data={dataArtist?.covers}
size="35px"
borderRadius="full"
iconEmpty={<MdPerson />}
/>
}
>
{dataArtist && (
<>
<Button
{...BUTTON_TOP_BAR_PROPERTY}
marginRight="auto"
onClick={() => navigate(`/artist/${dataArtist.id}`)}
>
<Covers
data={dataArtist?.covers}
size="35px"
borderRadius="full"
iconEmpty={<MdPerson />}
/>
<Text fontSize="24px" fontWeight="bold">
{dataArtist?.name}
</Text>
</Button>
<Button
{...BUTTON_TOP_BAR_PROPERTY}
onClick={() =>
navigate(`/album/${albumId}/edit-album/${dataAlbum.id}`)
}
>
<MdEdit />
</Button>
</>
<Button
{...BUTTON_TOP_BAR_PROPERTY}
onClick={() =>
navigate(`/album/${albumId}/edit-album/${dataAlbum.id}`)
}
>
<MdEdit />
</Button>
)}
</TopBar>
<PageLayout>

View File

@ -32,7 +32,7 @@ export const ArtistDetailPage = () => {
if (!dataArtist) {
return (
<>
<TopBar title="Artist detail" />
<TopBar title="Artists" titleLink="/artist" />
<PageLayoutInfoCenter>
Fail to load artist id: {artistId}
</PageLayoutInfoCenter>
@ -41,19 +41,12 @@ export const ArtistDetailPage = () => {
}
return (
<>
<TopBar>
<TopBar
title="Artists"
titleLink="/artist/all"
titleIcon={<MdGroup height="full" />}
>
<>
<Button
{...BUTTON_TOP_BAR_PROPERTY}
marginRight="auto"
onClick={() => navigate(`/artist/all`)}
>
<MdGroup height="full" />
<Text fontSize="24px" fontWeight="bold">
Artists
</Text>
</Button>
<Button
{...BUTTON_TOP_BAR_PROPERTY}
onClick={() =>

View File

@ -63,7 +63,7 @@ export const ArtistsPage = () => {
return (
<>
<TopBar title="All artists">
<TopBar title="All artists" titleLink="/home">
<SearchInput onChange={setFilterName} />
<Tooltip.Root aria-label="Random play">
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onRandomPlay}>

View File

@ -45,7 +45,7 @@ export const GenderDetailPage = () => {
if (!dataGender) {
return (
<>
<TopBar title="Gender detail" />
<TopBar title="Gender detail" titleLink="/gender" />
<PageLayoutInfoCenter>
Fail to load artist id: {genderId}
</PageLayoutInfoCenter>
@ -54,7 +54,7 @@ export const GenderDetailPage = () => {
}
return (
<>
<TopBar title="Gender detail">
<TopBar title="Gender detail" titleLink="/gender">
<Button
{...BUTTON_TOP_BAR_PROPERTY}
onClick={() =>

View File

@ -23,7 +23,7 @@ export const GendersPage = () => {
if (isLoading) {
return (
<>
<TopBar title="All Genders" />
<TopBar title="All Genders" titleLink="/home" />
<PageLayoutInfoCenter>No Gender available</PageLayoutInfoCenter>
</>
);
@ -31,7 +31,7 @@ export const GendersPage = () => {
return (
<>
<TopBar title="All Genders">
<TopBar title="All Genders" titleLink="/home">
<SearchInput onChange={setFilterTitle} />
</TopBar>
<PageLayout>

View File

@ -97,7 +97,7 @@ export const OnAirPage = () => {
if (!playTrackList || playTrackList.length == 0) {
return (
<>
<TopBar title="Album detail" />
<TopBar title="On Air ..." titleLink="/home" />
<PageLayoutInfoCenter>
No data is currently playing...
</PageLayoutInfoCenter>
@ -106,7 +106,7 @@ export const OnAirPage = () => {
}
return (
<>
<TopBar title="On Air ...">
<TopBar title="On Air ..." titleLink="/home">
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={clean}>
<LuTrash2 />
</Button>

View File

@ -42,7 +42,7 @@ export const TrackSelectionPage = () => {
};
return (
<>
<TopBar title="All Tracks" />
<TopBar title="All Tracks" titleLink="/home" />
<PageLayout>
<HStack
wrap="wrap"

View File

@ -1,21 +1,33 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { PartRight, RESTConfig, UserMe, UserResource } from '@/back-api';
import { createListCollection } from '@chakra-ui/react';
import { useEffectOnce } from 'react-use';
import {
PartRight,
RESTConfig,
UserMe,
UserResource,
setErrorApiGlobalCallback,
} from '@/back-api';
import { environment, getApiUrl } from '@/environment';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionState } from '@/service/SessionState';
import { isBrowser } from '@/utils/layout';
import { parseToken } from '@/utils/sso';
import { createListCollection } from '@chakra-ui/react';
const TOKEN_KEY = 'karusic-token-key-storage';
export const USERS_COLLECTION = createListCollection({
items: [
{ label: "admin", value: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIiwiYXBwbGljYXRpb24iOiJrYXJ1c2ljIiwiaXNzIjoiS2FyQXV0aCIsInJpZ2h0Ijp7ImthcnVzaWMiOnsiQURNSU4iOnRydWUsIlVTRVIiOnRydWV9fSwibG9naW4iOiJIZWVyb1l1aSIsImV4cCI6MTcyNDIwNjc5NCwiaWF0IjoxNzI0MTY2ODM0fQ.TEST_SIGNATURE_FOR_LOCAL_TEST_AND_TEST_E2E" },
{ label: "NO_USER", value: "svelte" },
{
label: 'admin',
value:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIiwiYXBwbGljYXRpb24iOiJrYXJ1c2ljIiwiaXNzIjoiS2FyQXV0aCIsInJpZ2h0Ijp7ImthcnVzaWMiOnsiQURNSU4iOnRydWUsIlVTRVIiOnRydWV9fSwibG9naW4iOiJIZWVyb1l1aSIsImV4cCI6MTcyNDIwNjc5NCwiaWF0IjoxNzI0MTY2ODM0fQ.TEST_SIGNATURE_FOR_LOCAL_TEST_AND_TEST_E2E',
},
{ label: 'NO_USER', value: 'svelte' },
],
})
});
export const USERS = {
admin:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIiwiYXBwbGljYXRpb24iOiJrYXJ1c2ljIiwiaXNzIjoiS2FyQXV0aCIsInJpZ2h0Ijp7ImthcnVzaWMiOnsiQURNSU4iOnRydWUsIlVTRVIiOnRydWV9fSwibG9naW4iOiJIZWVyb1l1aSIsImV4cCI6MTcyNDIwNjc5NCwiaWF0IjoxNzI0MTY2ODM0fQ.TEST_SIGNATURE_FOR_LOCAL_TEST_AND_TEST_E2E',
@ -57,20 +69,26 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
const [state, setState] = useState<SessionState>(SessionState.NO_USER);
const [config, setConfig] = useState<UserMe | undefined>(undefined);
useEffectOnce(() => {
setErrorApiGlobalCallback((response: Response) => {
if (response.status == 401) {
console.error('Detect 401 error ==> remove token');
clearToken();
}
});
});
const updateRight = useCallback(() => {
console.log('update right...');
if (isBrowser) {
console.log('Detect a new token...');
setState(SessionState.NO_USER);
setConfig(undefined);
if (token === undefined) {
console.log(` ==> No User`);
setState(SessionState.NO_USER);
setConfig(undefined);
localStorage.removeItem(TOKEN_KEY);
} else if (token === '__LOGOUT__') {
console.log(` ==> disconnection: ${token}`);
setState(SessionState.DISCONNECT);
localStorage.removeItem(TOKEN_KEY);
} else if (!['__LOGOUT__', '__FAIL__', '__CANCEL__'].includes(token)) {
} else {
console.log(' ==> Login ... (try to keep right)');
setState(SessionState.CONNECTING);
localStorage.setItem(TOKEN_KEY, token);
@ -84,6 +102,7 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
})
.catch((error) => {
setState(SessionState.CONNECTION_FAIL);
setConfig(undefined);
//console.log(` ==> Fail to get right: '${error}'`);
localStorage.removeItem(TOKEN_KEY);
});
@ -91,7 +110,10 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
}
}, [localStorage, parseToken, token]);
const setTokenLocal = useCallback(
(token: string) => {
(token?: string) => {
if (token ? token.startsWith('__') : false) {
token = undefined;
}
setToken(token);
updateRight();
},

File diff suppressed because one or more lines are too long

View File

@ -5,9 +5,6 @@ import { defineConfig } from 'vite';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
define: {
'process.env.NODE_ENV': '"development"', // Forcer le mode dev pour éviter l'erreur
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),