Compare commits

..

31 Commits

Author SHA1 Message Date
88b27e5f39 [FIX] deploy 2025-02-11 22:23:55 +01:00
eaf0f5688e [FIX] add end correction 2025-02-11 22:06:57 +01:00
4ebfa4e2ca [FIX] form basic models 2025-02-11 21:35:42 +01:00
c65e7d5e25 [DEV] continue refacto 2025-02-11 00:10:38 +01:00
de61cc156f [DEV] update from karso 2025-02-10 21:50:04 +01:00
4d18438914 [FEAT] update new archidata 2025-02-10 19:14:12 +01:00
80dfabcf48 [FEAT] update new archidata 2025-02-10 00:46:26 +01:00
83bfeda4ca [FEAT] Chakra V3 full operational 2025-01-25 01:34:11 +01:00
c489fabb77 [theme and reciepice 2025-01-24 21:57:04 +01:00
d52052de90 [DEV] test step between receipice and direct managemnt 2025-01-20 18:45:19 +01:00
91defa42c2 [FIX] chakra ui component is not full 2025-01-14 18:56:03 +01:00
2812d21782 [FEAT] doen not work: regacto of the Chakra-ui 3.3 ==> very bad port 2025-01-13 21:59:06 +01:00
eb5a366a12 [FEAT] add on air 2025-01-11 20:48:22 +01:00
6b801d250f [FIX] remove a stupid log... 2025-01-11 19:31:51 +01:00
01fad1b9d4 [FEAT] add a shuffle in the artist list.
This feature take a random on the artist and keep the 25 first and ge a single track for each artist.
2025-01-11 19:30:01 +01:00
371bea79f9 [FEAT] add a tool to manage a shuffle of an array 2025-01-11 19:28:51 +01:00
35725e1320 [FIX] sign-in in production mode 2025-01-11 19:28:30 +01:00
d6a8c7d23f [FIX] build version 2025-01-11 18:12:27 +01:00
3c604e9593 [VERSION] update dev tag version 2025-01-11 17:33:31 +01:00
43d8108270 [RELEASE] Release v1.1.0 2025-01-11 17:33:21 +01:00
1a883193d0 [DEPENDENCY] update release of archidata 2025-01-11 17:32:43 +01:00
78b1970ba9 [FEAT] (front) Update versions of dependency 2025-01-11 17:31:01 +01:00
746d5dff96 [FIX] test will not able to connect on port 80 2025-01-11 17:31:01 +01:00
8780ea8e63 [FIX] initialization error 2025-01-11 17:31:01 +01:00
8911eed0fb [FEAT] update NCU to only update vertion but not the major 2025-01-11 17:31:01 +01:00
1a3652472e [FIX] update the error response with OID instead of UUID 2025-01-11 17:31:01 +01:00
2e62577103 [FIX] (front) fix the environment semection in production mode 2025-01-11 17:31:01 +01:00
653e77160b [FEAT] update logger to well support the backend 2025-01-11 17:31:01 +01:00
3898a6bf4f [DEV] remove old logger system 2025-01-11 17:31:01 +01:00
448cf1569b [FIX] remove old loger system 2025-01-11 17:31:01 +01:00
d3e2b3e601 [VERSION] update dev tag version 2025-01-06 23:49:35 +01:00
168 changed files with 7437 additions and 7552 deletions

1
.gitignore vendored
View File

@ -65,3 +65,4 @@ __pycache__
.design/ .design/
.vscode/ .vscode/
front/storybook-static front/storybook-static
back/bin

View File

@ -6,7 +6,7 @@
FROM archlinux:base-devel AS common FROM archlinux:base-devel AS common
# update system # update system
RUN pacman -Syu --noconfirm && pacman-db-upgrade \ RUN pacman -Syu --noconfirm && pacman-db-upgrade \
&& pacman -S --noconfirm jdk-openjdk \ && pacman -S --noconfirm jdk-openjdk wget\
&& pacman -Scc --noconfirm && pacman -Scc --noconfirm
WORKDIR /tmp WORKDIR /tmp
@ -53,13 +53,13 @@ RUN pnpm install --prod=false
FROM dependency_front AS load_sources_front FROM dependency_front AS load_sources_front
# JUST to get the vertion of the application and his sha... # JUST to get the vertion of the application and his sha...
COPY front/build.js \ COPY \
front/version.txt \
front/tsconfig.json \ front/tsconfig.json \
front/tsconfig.node.json \ front/tsconfig.node.json \
front/vite.config.mts \ front/vite.config.mts \
front/index.html \ front/index.html \
./ ./
COPY front/public ./public COPY front/public ./public
COPY front/src ./src COPY front/src ./src
@ -103,4 +103,8 @@ WORKDIR /application/
EXPOSE 80 EXPOSE 80
# To verify health-check: docker inspect --format "{{json .State.Health }}" YOUR_SERVICE_NAME | jq
HEALTHCHECK --start-period=10s --start-interval=2s --interval=30s --timeout=5s --retries=10 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/api/health_check || exit 1
CMD ["java", "-Xms64M", "-Xmx1G", "-cp", "/application/application.jar", "org.kar.karusic.WebLauncher"] CMD ["java", "-Xms64M", "-Xmx1G", "-cp", "/application/application.jar", "org.kar.karusic.WebLauncher"]

View File

@ -6,40 +6,90 @@ Karideo
Run in local: Run in local:
============= =============
so simple... Start tools
-----------
Start the server basic interfaces: (DB(mySQL), Adminer)
```{.bash} ```{.bash}
# start the Bdd interface (no big data > 50Mo) # start the Bdd interface (no big data > 50Mo)
cd bdd docker compose -f env_dev/docker-compose.yaml up -d
docker-compose up -d
# start the REST API
cd back
docker-compose up -d
# start the front API
cd ../front
docker-compose up -d
``` ```
Start the Back-end:
-------------------
convert in an angular application: backend is developed in JAVA
https://betterprogramming.pub/how-to-convert-your-angular-application-to-a-native-mobile-app-android-and-ios-c212b38976df
The first step is configuring your JAVA version (or select the JVM with the OS)
```bash
export PATH=$(ls -d --color=never /usr/lib/jvm/java-2*-openjdk)/bin:$PATH
```
Link with the external sub-library (front) Install the dependency:
------------------------------------------ ```bash
mvn install
```
Link: Run the test
```bash
mvn test
```
Install it for external use
```bash
mvn install
```
Execute the local server:
```bash
mvn exec:java@dev-mode
```
Start the Front-end:
--------------------
backend is developed in JAVA
```bash ```bash
cd front cd front
pnpm run link_kar_cw pnpm install
pnpm dev
``` ```
un-link: Display the result:
-------------------
[show the webpage: http://localhost:4203](http://localhost:4203)
Some other dev tools:
=====================
Format code:
------------
```bash ```bash
cd front export PATH=$(ls -d --color=never /usr/lib/jvm/java-2*-openjdk)/bin:$PATH
pnpm run unlink_kar_cw mvn formatter:format
mvn test
``` ```
Tools in production mode
========================
Changing the Log Level
----------------------
In a production environment, you can adjust the log level to help diagnose bugs more effectively.
The available log levels are:
| **Log Level Tag** | **org.kar.karusic** | **org.kar.archidata** | **other** |
| ----------------- | ------------------- | --------------------- | --------- |
| `prod` | INFO | INFO | INFO |
| `prod-debug` | DEBUG | INFO | INFO |
| `prod-trace` | TRACE | DEBUG | INFO |
| `prod-trace-full` | TRACE | TRACE | INFO |
| `dev` | TRACE | DEBUG | INFO |
Manual set in production: Manual set in production:
========================= =========================

View File

@ -1,7 +1,8 @@
FROM maven:3-openjdk-18 AS build FROM maven:3-openjdk-23 AS build
COPY pom.xml /tmp/ COPY pom.xml /tmp/
COPY src /tmp/src/ COPY src /tmp/src/
COPY Formatter.xml /tmp/
WORKDIR /tmp/ WORKDIR /tmp/
RUN mvn clean compile assembly:single RUN mvn clean compile assembly:single

View File

@ -3,11 +3,11 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.kar</groupId> <groupId>org.kar</groupId>
<artifactId>karusic</artifactId> <artifactId>karusic</artifactId>
<version>1.0.4</version> <version>1.1.1-SNAPSHOT</version>
<properties> <properties>
<maven.compiler.version>3.1</maven.compiler.version> <maven.compiler.version>3.13.0</maven.compiler.version>
<maven.compiler.source>21</maven.compiler.source> <maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target> <maven.compiler.target>23</maven.compiler.target>
<maven.dependency.version>3.1.1</maven.dependency.version> <maven.dependency.version>3.1.1</maven.dependency.version>
</properties> </properties>
<repositories> <repositories>
@ -20,7 +20,7 @@
<dependency> <dependency>
<groupId>kangaroo-and-rabbit</groupId> <groupId>kangaroo-and-rabbit</groupId>
<artifactId>archidata</artifactId> <artifactId>archidata</artifactId>
<version>0.20.4</version> <version>0.23.6</version>
</dependency> </dependency>
<!-- Loopback of logger JDK logging API to SLF4J --> <!-- Loopback of logger JDK logging API to SLF4J -->
<dependency> <dependency>
@ -39,10 +39,15 @@
<artifactId>xercesImpl</artifactId> <artifactId>xercesImpl</artifactId>
<version>2.12.2</version> <version>2.12.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>3.1.9</version>
</dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>
<version>2.18.0-rc1</version> <version>2.18.1</version>
</dependency> </dependency>
<!-- <!--
************************************************************ ************************************************************
@ -99,16 +104,45 @@
<plugin> <plugin>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId> <artifactId>exec-maven-plugin</artifactId>
<version>1.4.0</version> <version>3.2.0</version>
<executions>
<execution>
<id>prod-mode</id>
<goals>
<goal>java</goal>
</goals>
<configuration> <configuration>
<mainClass>org.kar.karusic.WebLauncher</mainClass> <mainClass>org.kar.karusic.WebLauncher</mainClass>
</configuration> </configuration>
</execution>
<execution>
<id>dev-mode</id>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.kar.karusic.WebLauncherLocal</mainClass>
</configuration>
</execution>
<execution>
<id>generate-api</id>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.kar.karusic.GenerateApi</mainClass>
</configuration>
</execution>
</executions>
<configuration>
<mainClass/>
</configuration>
</plugin> </plugin>
<!-- Create the source bundle --> <!-- Create the source bundle -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId> <artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version> <version>4.0.0-beta-1</version>
<executions> <executions>
<execution> <execution>
<id>attach-sources</id> <id>attach-sources</id>
@ -122,10 +156,12 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version> <version>3.2.5</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId> <artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<configuration> <configuration>
<archive> <archive>
<manifest> <manifest>
@ -137,81 +173,21 @@
</descriptorRefs> </descriptorRefs>
</configuration> </configuration>
</plugin> </plugin>
<!-- Create coverage -->
<!--
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>PACKAGE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.50</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
-->
<!-- Java-doc generation for stand-alone site --> <!-- Java-doc generation for stand-alone site -->
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version> <version>3.3.0</version>
<configuration> <configuration>
<show>private</show> <show>private</show>
<nohelp>true</nohelp> <nohelp>true</nohelp>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>exec-application</id>
<phase>package</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>org.kar.karusic.WebLauncher</mainClass>
</configuration>
</plugin>
<!-- Check the style of the code --> <!-- Check the style of the code -->
<plugin> <plugin>
<groupId>net.revelc.code.formatter</groupId> <groupId>net.revelc.code.formatter</groupId>
<artifactId>formatter-maven-plugin</artifactId> <artifactId>formatter-maven-plugin</artifactId>
<version>2.23.0</version> <version>2.24.1</version>
<configuration> <configuration>
<encoding>UTF-8</encoding> <encoding>UTF-8</encoding>
<lineEnding>LF</lineEnding> <lineEnding>LF</lineEnding>
@ -242,14 +218,6 @@
<configuration> <configuration>
<includeFilterFile>spotbugs-security-include.xml</includeFilterFile> <includeFilterFile>spotbugs-security-include.xml</includeFilterFile>
<excludeFilterFile>spotbugs-security-exclude.xml</excludeFilterFile> <excludeFilterFile>spotbugs-security-exclude.xml</excludeFilterFile>
<!--<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.12.0</version>
</plugin>
</plugins>
-->
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
@ -260,7 +228,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId> <artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version> <version>3.3.0</version>
<configuration> <configuration>
<show>public</show> <show>public</show>
</configuration> </configuration>

View File

@ -0,0 +1,17 @@
package org.kar.karusic;
import org.kar.karusic.migration.Initialization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GenerateApi {
private final static Logger LOGGER = LoggerFactory.getLogger(GenerateApi.class);
private GenerateApi() {}
public static void main(final String[] args) throws Exception {
LOGGER.info("Generate API");
Initialization.generateObjects();
LOGGER.info("STOP the REST server.");
}
}

View File

@ -1,22 +1,10 @@
package org.kar.karusic; package org.kar.karusic;
import java.util.List;
import java.util.logging.LogManager; import java.util.logging.LogManager;
import org.kar.archidata.api.DataResource;
import org.kar.archidata.api.ProxyResource;
import org.kar.archidata.exception.DataAccessException; import org.kar.archidata.exception.DataAccessException;
import org.kar.archidata.externalRestApi.AnalyzeApi;
import org.kar.archidata.externalRestApi.TsGenerateApi;
import org.kar.archidata.tools.ConfigBaseVariable; import org.kar.archidata.tools.ConfigBaseVariable;
import org.kar.karusic.api.AlbumResource; import org.kar.karusic.migration.Initialization;
import org.kar.karusic.api.ArtistResource;
import org.kar.karusic.api.Front;
import org.kar.karusic.api.GenderResource;
import org.kar.karusic.api.HealthCheck;
import org.kar.karusic.api.PlaylistResource;
import org.kar.karusic.api.TrackResource;
import org.kar.karusic.api.UserResource;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler; import org.slf4j.bridge.SLF4JBridgeHandler;
@ -26,27 +14,17 @@ public class WebLauncherLocal extends WebLauncher {
private WebLauncherLocal() {} private WebLauncherLocal() {}
public static void generateObjects() throws Exception {
LOGGER.info("Generate APIs");
final List<Class<?>> listOfResources = List.of(AlbumResource.class, ArtistResource.class, Front.class, GenderResource.class, HealthCheck.class, PlaylistResource.class, UserResource.class,
TrackResource.class, DataResource.class, ProxyResource.class);
final AnalyzeApi api = new AnalyzeApi();
api.addAllApi(listOfResources);
TsGenerateApi.generateApi(api, "../front/src/back-api/");
LOGGER.info("Generate APIs (DONE)");
}
public static void main(final String[] args) throws Exception { public static void main(final String[] args) throws Exception {
// Loop-back of logger JDK logging API to SLF4J // Loop-back of logger JDK logging API to SLF4J
LogManager.getLogManager().reset(); LogManager.getLogManager().reset();
SLF4JBridgeHandler.install(); SLF4JBridgeHandler.install();
// Generate the APIs in type-script // Generate the APIs in type-script
generateObjects(); Initialization.generateObjects();
final WebLauncherLocal launcher = new WebLauncherLocal(); final WebLauncherLocal launcher = new WebLauncherLocal();
launcher.process(); launcher.process();
launcher.LOGGER.info("end-configure the server & wait finish process:"); LOGGER.info("end-configure the server & wait finish process:");
Thread.currentThread().join(); Thread.currentThread().join();
launcher.LOGGER.info("STOP the REST server:"); LOGGER.info("STOP the REST server:");
} }
@Override @Override

View File

@ -1,60 +0,0 @@
package org.kar.karusic.internal;
//import io.scenarium.logger.LogLevel;
//import io.scenarium.logger.Logger;
public class Log {
// private static final String LIB_NAME = "logger";
// private static final String LIB_NAME_DRAW = Logger.getDrawableName(LIB_NAME);
// private static final boolean PRINT_CRITICAL = Logger.getNeedPrint(LIB_NAME, LogLevel.CRITICAL);
// private static final boolean PRINT_ERROR = Logger.getNeedPrint(LIB_NAME, LogLevel.ERROR);
// private static final boolean PRINT_WARNING = Logger.getNeedPrint(LIB_NAME, LogLevel.WARNING);
// private static final boolean PRINT_INFO = Logger.getNeedPrint(LIB_NAME, LogLevel.INFO);
// private static final boolean PRINT_DEBUG = Logger.getNeedPrint(LIB_NAME, LogLevel.DEBUG);
// private static final boolean PRINT_VERBOSE = Logger.getNeedPrint(LIB_NAME, LogLevel.VERBOSE);
// private static final boolean PRINT_TODO = Logger.getNeedPrint(LIB_NAME, LogLevel.TODO);
// private static final boolean PRINT_PRINT = Logger.getNeedPrint(LIB_NAME, LogLevel.PRINT);
//
// private Log() {}
//
// public static void print(String data) {
// if (PRINT_PRINT)
// Logger.print(LIB_NAME_DRAW, data);
// }
//
// public static void todo(String data) {
// if (PRINT_TODO)
// Logger.todo(LIB_NAME_DRAW, data);
// }
//
// public static void critical(String data) {
// if (PRINT_CRITICAL)
// Logger.critical(LIB_NAME_DRAW, data);
// }
//
// public static void error(String data) {
// if (PRINT_ERROR)
// Logger.error(LIB_NAME_DRAW, data);
// }
//
// public static void warning(String data) {
// if (PRINT_WARNING)
// Logger.warning(LIB_NAME_DRAW, data);
// }
//
// public static void info(String data) {
// if (PRINT_INFO)
// Logger.info(LIB_NAME_DRAW, data);
// }
//
// public static void debug(String data) {
// if (PRINT_DEBUG)
// Logger.debug(LIB_NAME_DRAW, data);
// }
//
// public static void verbose(String data) {
// if (PRINT_VERBOSE)
// Logger.verbose(LIB_NAME_DRAW, data);
// }
}

View File

@ -2,10 +2,22 @@ package org.kar.karusic.migration;
import java.util.List; import java.util.List;
import org.kar.archidata.api.DataResource;
import org.kar.archidata.api.ProxyResource;
import org.kar.archidata.dataAccess.DBAccess; import org.kar.archidata.dataAccess.DBAccess;
import org.kar.archidata.externalRestApi.AnalyzeApi;
import org.kar.archidata.externalRestApi.TsGenerateApi;
import org.kar.archidata.migration.MigrationSqlStep; import org.kar.archidata.migration.MigrationSqlStep;
import org.kar.archidata.model.Data; import org.kar.archidata.model.Data;
import org.kar.archidata.model.User; import org.kar.archidata.model.User;
import org.kar.karusic.api.AlbumResource;
import org.kar.karusic.api.ArtistResource;
import org.kar.karusic.api.Front;
import org.kar.karusic.api.GenderResource;
import org.kar.karusic.api.HealthCheck;
import org.kar.karusic.api.PlaylistResource;
import org.kar.karusic.api.TrackResource;
import org.kar.karusic.api.UserResource;
import org.kar.karusic.model.Album; import org.kar.karusic.model.Album;
import org.kar.karusic.model.Artist; import org.kar.karusic.model.Artist;
import org.kar.karusic.model.Gender; import org.kar.karusic.model.Gender;
@ -25,14 +37,20 @@ public class Initialization extends MigrationSqlStep {
return "Initialization"; return "Initialization";
} }
public Initialization() { public static void generateObjects() throws Exception {
LOGGER.info("Generate APIs");
final List<Class<?>> listOfResources = List.of(AlbumResource.class, ArtistResource.class, Front.class, GenderResource.class, HealthCheck.class, PlaylistResource.class, UserResource.class,
TrackResource.class, DataResource.class, ProxyResource.class);
final AnalyzeApi api = new AnalyzeApi();
api.addAllApi(listOfResources);
TsGenerateApi.generateApi(api, "../front/src/back-api/");
LOGGER.info("Generate APIs (DONE)");
} }
@Override @Override
public void generateStep() throws Exception { public void generateStep() throws Exception {
for (final Class<?> elem : CLASSES_BASE) { for (final Class<?> clazz : CLASSES_BASE) {
addClass(elem); addClass(clazz);
} }
addAction((final DBAccess da) -> { addAction((final DBAccess da) -> {
@ -63,6 +81,7 @@ public class Initialization extends MigrationSqlStep {
new Gender(24L, "Bande Originale"), // new Gender(24L, "Bande Originale"), //
new Gender(25L, "Variété Belge"), // new Gender(25L, "Variété Belge"), //
new Gender(26L, "Gospel")); new Gender(26L, "Gospel"));
da.insertMultiple(data);
}); });
// set start increment element to permit to add after default elements // set start increment element to permit to add after default elements
addAction(""" addAction("""

View File

@ -6,7 +6,7 @@ import java.util.List;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists; import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson; import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.dataAccess.options.CheckJPA; import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data; import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete; import org.kar.archidata.model.GenericDataSoftDelete;

View File

@ -6,7 +6,7 @@ import java.util.List;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists; import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson; import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.dataAccess.options.CheckJPA; import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data; import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete; import org.kar.archidata.model.GenericDataSoftDelete;

View File

@ -17,7 +17,7 @@ import java.util.List;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists; import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson; import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.dataAccess.options.CheckJPA; import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data; import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete; import org.kar.archidata.model.GenericDataSoftDelete;

View File

@ -17,7 +17,7 @@ import java.util.List;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists; import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson; import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.dataAccess.options.CheckJPA; import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data; import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete; import org.kar.archidata.model.GenericDataSoftDelete;

View File

@ -17,7 +17,7 @@ import java.util.List;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.kar.archidata.annotation.DataIfNotExists; import org.kar.archidata.annotation.DataIfNotExists;
import org.kar.archidata.annotation.DataJson; import org.kar.archidata.annotation.DataJson;
import org.kar.archidata.dataAccess.options.CheckJPA; import org.kar.archidata.checker.CheckJPA;
import org.kar.archidata.model.Data; import org.kar.archidata.model.Data;
import org.kar.archidata.model.GenericDataSoftDelete; import org.kar.archidata.model.GenericDataSoftDelete;

View File

@ -1,18 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<!-- environment detection (defaut: dev) -->
<property name="LOG_LEVEL_ENV" value="${LOG_LEVEL:-dev}" />
<!-- Appender for development -->
<if condition="property(&quot;LOG_LEVEL_ENV&quot;).equals(&quot;dev&quot;)">
<then>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder> <encoder>
<pattern> <pattern>%green(%d{HH:mm:ss.SSS}) %highlight(%-5level) %-30((%file:%line\)): %msg%n</pattern>
%d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger - %msg%n
</pattern>
<!--
<pattern>
%d{HH:mm:ss.SSS} | %thread | %highlight(%-5level) | %logger - %msg%n
</pattern>
-->
</encoder> </encoder>
</appender> </appender>
<logger name="org.kar.karusic" level="TRACE" />
<root level="info"> <logger name="org.kar.archidata" level="DEBUG" />
<root level="INFO">
<appender-ref ref="CONSOLE" /> <appender-ref ref="CONSOLE" />
</root> </root>
</then>
</if>
<!-- Appender for production -->
<if condition="property(&quot;LOG_LEVEL_ENV&quot;).matches(&quot;^prod.*&quot;)">
<then>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%thread] %level %logger - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</then>
</if>
<if condition="property(&quot;LOG_LEVEL_ENV&quot;).equals(&quot;prod-debug&quot;)">
<then>
<logger name="org.kar.karusic" level="DEBUG" />
</then>
</if>
<if condition="property(&quot;LOG_LEVEL_ENV&quot;).equals(&quot;prod-trace&quot;)">
<then>
<logger name="org.kar.karusic" level="TRACE" />
<logger name="org.kar.archidata" level="DEBUG" />
</then>
</if>
<if condition="property(&quot;LOG_LEVEL_ENV&quot;).equals(&quot;prod-trace-full&quot;)">
<then>
<logger name="org.kar.karusic" level="TRACE" />
<logger name="org.kar.archidata" level="TRACE" />
</then>
</if>
</configuration> </configuration>

View File

@ -1,42 +0,0 @@
# SLF4J's SimpleLogger configuration file
# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err.
# Default logging detail level for all instances of SimpleLogger.
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, defaults to "info".
org.slf4j.simpleLogger.defaultLogLevel=INFO
# Logging detail level for a SimpleLogger instance named "xxxxx".
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, the default logging detail level is used.
#org.slf4j.simpleLogger.log.xxxxx=
org.slf4j.simpleLogger.log.org.kar.archidata=TRACE
org.slf4j.simpleLogger.log.org.kar.karusic=TRACE
# Set to true if you want the current date and time to be included in output messages.
# Default is false, and will output the number of milliseconds elapsed since startup.
#org.slf4j.simpleLogger.showDateTime=false
# The date and time format to be used in the output messages.
# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat.
# If the format is not specified or is invalid, the default format is used.
# The default format is yyyy-MM-dd HH:mm:ss:SSS Z.
#org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z
# Set to true if you want to output the current thread name.
# Defaults to true.
org.slf4j.simpleLogger.showThreadName=true
# Set to true if you want the Logger instance name to be included in output messages.
# Defaults to true.
#org.slf4j.simpleLogger.showLogName=true
# Set to true if you want the last component of the name to be included in output messages.
# Defaults to false.
#org.slf4j.simpleLogger.showShortLogName=false
# Utilise les codes ANSI pour la couleur
org.slf4j.simpleLogger.warnColor=\u001B[33m
org.slf4j.simpleLogger.errorColor=\u001B[31m
org.slf4j.simpleLogger.infoColor=\u001B[32m
org.slf4j.simpleLogger.debugColor=\u001B[34m

View File

@ -35,6 +35,10 @@ public class ConfigureDb {
if (modeTestForced != null) { if (modeTestForced != null) {
modeTest = modeTestForced; modeTest = modeTestForced;
} }
// for local test:
ConfigBaseVariable.apiAdress = "http://127.0.0.1:12342/test/api/";
// Enable the test mode permit to access to the test token (never use it in production).
ConfigBaseVariable.testMode = "true";
final List<Class<?>> listObject = List.of( // final List<Class<?>> listObject = List.of( //
Album.class, // Album.class, //
Artist.class, // Artist.class, //

View File

@ -25,8 +25,6 @@ public class TestBase {
ConfigureDb.configure(); ConfigureDb.configure();
LOGGER.info("configure server ..."); LOGGER.info("configure server ...");
webInterface = new WebLauncherTest(); webInterface = new WebLauncherTest();
LOGGER.info("Clean previous table");
LOGGER.info("Start REST (BEGIN)"); LOGGER.info("Start REST (BEGIN)");
webInterface.process(); webInterface.process();
LOGGER.info("Start REST (DONE)"); LOGGER.info("Start REST (DONE)");

View File

@ -4,23 +4,11 @@ import { Box } from '@chakra-ui/react';
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import theme from '../src/theme'; import { ColorModeProvider } from '../src/components/ui/color-mode';
import { Toaster } from '../src/components/ui/toaster';
// .storybook/preview.js import { systemTheme } from '../src/theme/theme';
export const parameters = {
options: {
storySort: {
order: ['StyleGuide', 'Components', 'Fields', 'App Layout'],
},
},
actions: {},
layout: 'fullscreen',
backgrounds: { disable: true, grid: { disable: true } },
chakra: {
theme,
},
};
// .
const DocumentationWrapper = ({ children }) => { const DocumentationWrapper = ({ children }) => {
return ( return (
<Box id="start-ui-storybook-wrapper" p="4" pb="8" flex="1"> <Box id="start-ui-storybook-wrapper" p="4" pb="8" flex="1">
@ -31,13 +19,16 @@ const DocumentationWrapper = ({ children }) => {
export const decorators = [ export const decorators = [
(Story, context) => ( (Story, context) => (
<ChakraProvider theme={theme}> <ColorModeProvider>
<ChakraProvider value={systemTheme}>
{/* Using MemoryRouter to avoid route clashing with Storybook */} {/* Using MemoryRouter to avoid route clashing with Storybook */}
<MemoryRouter> <MemoryRouter>
<DocumentationWrapper> <DocumentationWrapper>
<Story {...context} /> <Story {...context} />
</DocumentationWrapper> </DocumentationWrapper>
</MemoryRouter> </MemoryRouter>
<Toaster />
</ChakraProvider> </ChakraProvider>
</ColorModeProvider>
), ),
]; ];

View File

@ -1,6 +0,0 @@
{
"display": "2025-01-06",
"version": "0.0.1-dev\n - 2025-01-06T00:49:52+01:00",
"commit": "0.0.1-dev\n",
"date": "2025-01-06T00:49:52+01:00"
}

View File

@ -1,25 +0,0 @@
const dayjs = require('dayjs');
const fs = require('fs');
const generateAppBuild = () => {
const getVersion = () => fs.readFileSync('version.txt', 'utf8');
const commit = process.env.VERCEL_GIT_COMMIT_SHA
? process.env.VERCEL_GIT_COMMIT_SHA
: getVersion();
const appBuildContent = {
display: `${dayjs().format('YYYY-MM-DD')}`,
version: `${commit} - ${dayjs().format()}`,
commit,
date: dayjs().format(),
};
fs.writeFileSync(
'./app-build.json',
JSON.stringify(appBuildContent, null, 2)
);
};
generateAppBuild();

View File

@ -3,16 +3,7 @@ import type { KnipConfig } from 'knip';
const config: KnipConfig = { const config: KnipConfig = {
// Ignoring mostly shell binaries // Ignoring mostly shell binaries
ignoreBinaries: ['export', 'sleep'], ignoreBinaries: ['export', 'sleep'],
ignore: [ ignore: [],
// Related to tests
'tests/**',
'**.conf.js',
'steps.d.ts',
'steps_file.js',
'env_ci/codecept.conf.js',
// Generic components are useful.
'src/components/**',
],
}; };
export default config; export default config;

View File

@ -12,12 +12,13 @@
"node": ">=20" "node": ">=20"
}, },
"scripts": { "scripts": {
"update_packages": "ncu --upgrade", "update_packages": "ncu --target minor",
"upgrade_packages": "ncu --upgrade ",
"install_dependency": "pnpm install", "install_dependency": "pnpm install",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest watch", "test:watch": "vitest watch",
"build": "tsc && vite build", "build": "tsc && vite build",
"static:build": "node build.js && pnpm build", "static:build": "pnpm build",
"dev": "vite", "dev": "vite",
"pretty": "prettier -w .", "pretty": "prettier -w .",
"lint": "pnpm tsc --noEmit", "lint": "pnpm tsc --noEmit",
@ -28,84 +29,63 @@
"*.{ts,tsx,js,jsx,json}": "prettier --write" "*.{ts,tsx,js,jsx,json}": "prettier --write"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/anatomy": "2.2.2", "@trivago/prettier-plugin-sort-imports": "5.2.2",
"@chakra-ui/cli": "2.4.1", "@chakra-ui/cli": "3.7.0",
"@chakra-ui/react": "2.8.2", "@chakra-ui/react": "3.7.0",
"@chakra-ui/theme-tools": "2.2.6",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@dnd-kit/sortable": "10.0.0",
"@dnd-kit/utilities": "3.2.2",
"@emotion/react": "11.14.0", "@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"@formiz/core": "2.4.5",
"@formiz/validations": "2.0.1",
"allotment": "1.20.2", "allotment": "1.20.2",
"css-mediaquery": "0.1.2", "css-mediaquery": "0.1.2",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"history": "5.3.0", "history": "5.3.0",
"react": "18.3.1", "next-themes": "^0.4.4",
"react-color-palette": "7.3.0", "react": "19.0.0-rc.1",
"react-currency-input-field": "3.9.0", "react-dom": "19.0.0-rc.1",
"react-custom-scrollbars": "4.2.1", "react-error-boundary": "5.0.0",
"react-day-picker": "9.5.0", "react-icons": "5.4.0",
"react-dom": "18.3.1", "react-router-dom": "7.1.5",
"react-error-boundary": "4.0.13", "react-select": "5.10.0",
"react-focus-lock": "2.13.2",
"react-icons": "5.3.0",
"react-popper": "2.3.0",
"react-router-dom": "6.26.2",
"react-select": "5.9.0",
"react-simple-keyboard": "3.8.33",
"react-sticky-el": "2.1.1",
"react-use": "17.6.0", "react-use": "17.6.0",
"react-use-draggable-scroll": "0.4.7",
"react-virtuoso": "4.12.3",
"ts-pattern": "5.6.0",
"uuid": "11.0.4",
"zod": "3.24.1", "zod": "3.24.1",
"zustand": "5.0.2" "zustand": "5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@chakra-ui/styled-system": "2.12.0", "@chakra-ui/styled-system": "^2.12.0",
"@playwright/test": "1.49.1", "@playwright/test": "1.50.1",
"@storybook/addon-actions": "8.4.7", "@storybook/addon-actions": "8.5.4",
"@storybook/addon-essentials": "8.4.7", "@storybook/addon-essentials": "8.5.4",
"@storybook/addon-links": "8.4.7", "@storybook/addon-links": "8.5.4",
"@storybook/addon-mdx-gfm": "8.4.7", "@storybook/addon-mdx-gfm": "8.5.4",
"@storybook/react": "8.4.7", "@storybook/react": "8.5.4",
"@storybook/react-vite": "8.4.7", "@storybook/react-vite": "8.5.4",
"@storybook/theming": "8.4.7", "@storybook/theming": "8.5.4",
"@testing-library/jest-dom": "6.6.3", "@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.1.0", "@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.5.2", "@testing-library/user-event": "14.6.1",
"@trivago/prettier-plugin-sort-imports": "5.2.1", "@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/node": "22.10.5", "@types/node": "22.13.1",
"@types/react": "18.3.8", "@types/react": "19.0.8",
"@types/react-dom": "18.3.0", "@types/react-dom": "19.0.3",
"@types/react-sticky-el": "1.0.7", "@typescript-eslint/eslint-plugin": "8.24.0",
"@typescript-eslint/eslint-plugin": "8.19.0", "@typescript-eslint/parser": "8.24.0",
"@typescript-eslint/parser": "8.19.0",
"@vitejs/plugin-react": "4.3.4", "@vitejs/plugin-react": "4.3.4",
"eslint": "9.17.0", "eslint": "9.20.1",
"eslint-plugin-codeceptjs": "1.3.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-react": "7.37.3", "eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "5.1.0", "eslint-plugin-react-hooks": "5.1.0",
"eslint-plugin-storybook": "0.11.2", "eslint-plugin-storybook": "0.11.2",
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "29.7.0",
"knip": "5.41.1", "knip": "5.44.0",
"lint-staged": "15.3.0", "lint-staged": "15.4.3",
"npm-check-updates": "^17.1.13", "npm-check-updates": "^17.1.14",
"prettier": "3.4.2", "prettier": "3.5.0",
"puppeteer": "23.11.1", "puppeteer": "24.2.0",
"react-is": "19.0.0", "react-is": "19.0.0",
"storybook": "8.4.7", "storybook": "8.5.4",
"ts-node": "10.9.2", "ts-node": "10.9.2",
"typescript": "5.7.2", "typescript": "5.7.3",
"vite": "6.0.7", "vite": "6.1.0",
"vitest": "2.1.8" "vitest": "3.0.5"
} }
} }

6910
front/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,117 +1,19 @@
import { useState } from 'react'; import { ErrorBoundary } from '@/errors/ErrorBoundary';
import { ChakraProvider, Select } from '@chakra-ui/react'; import { AudioPlayer } from './components';
import { import { EnvDevelopment } from './components/EnvDevelopment/EnvDevelopment';
Box, import { AppRoutes } from './scene/AppRoutes';
Button, import { ServiceContextProvider } from './service/ServiceContext';
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { environment } from '@/environment';
import { App as SpaApp } from '@/scene/App';
import { USERS } from '@/service/session';
import theme from '@/theme';
import { hashLocalData } from '@/utils/sso';
const AppEnvHint = () => {
const modal = useDisclosure();
const [selectUserTest, setSelectUserTest] = useState<string>('NO_USER');
//const setUser = useRightsStore((store) => store.setUser);
const buildEnv =
process.env.NODE_ENV === 'development'
? 'Development'
: import.meta.env.VITE_DEV_ENV_NAME;
const envName: Array<string> = [];
!!buildEnv && envName.push(buildEnv);
if (!envName.length) {
return null;
}
const handleChange = (selectedOption) => {
console.log(`SELECT: [${selectedOption.target.value}]`);
setSelectUserTest(selectedOption.target.value);
};
const onClose = () => {
modal.onClose();
if (selectUserTest == 'NO_USER') {
window.location.href = `/${environment.applName}/sso/${hashLocalData()}/false/__LOGOUT__`;
} else {
window.location.href = `/${environment.applName}/sso/${hashLocalData()}/true/${USERS[selectUserTest]}`;
}
};
export const App = () => {
return ( return (
<> <ServiceContextProvider>
<Box <EnvDevelopment />
zIndex="100000" <ErrorBoundary>
position="fixed" <AppRoutes />
top="0" </ErrorBoundary>
insetStart="0" <AudioPlayer />
insetEnd="0" </ServiceContextProvider>
h="2px"
bg="warning.400"
as="button"
cursor="pointer"
data-test-id="devtools"
onClick={modal.onOpen}
>
<Text
position="fixed"
top="0"
insetStart="4"
bg="warning.400"
color="warning.900"
fontSize="0.6rem"
fontWeight="bold"
px="10px"
marginLeft="25%"
borderBottomStartRadius="sm"
borderBottomEndRadius="sm"
textTransform="uppercase"
>
{envName.join(' : ')}
</Text>
</Box>
<Modal isOpen={modal.isOpen} onClose={modal.onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Outils développeurs</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Stack>
<Text>Utilisateur</Text>
<Select placeholder="Select test user" onChange={handleChange}>
{Object.keys(USERS).map((key) => (
<option value={key} key={key}>
{key}
</option>
))}
</Select>
</Stack>
</ModalBody>
<ModalFooter>
<Button onClick={onClose}>Apply</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
const App = () => {
return (
<ChakraProvider theme={theme}>
<AppEnvHint />
<SpaApp />
</ChakraProvider>
); );
}; };

View File

@ -1,72 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 24 24"
height="250px"
width="250px"
version="1.1"
id="svg2"
sodipodi:docname="404.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="true"
inkscape:zoom="3.448"
inkscape:cx="134.28074"
inkscape:cy="125"
inkscape:window-width="1918"
inkscape:window-height="1044"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="1"
inkscape:current-layer="svg2">
<inkscape:grid
id="grid2"
units="px"
originx="0"
originy="0"
spacingx="0.096"
spacingy="0.096"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="true" />
</sodipodi:namedview>
<path
fill="none"
d="M0 0h24v24H0z"
id="path1" />
<path
d="M13 10h5l3-3-3-3h-5V2h-2v2H4v6h7v2H6l-3 3 3 3h5v4h2v-4h7v-6h-7z"
id="path2" />
<path
id="rect2"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.384;stroke-linecap:square"
d="M 17.394219,5.0400499 19.459554,6.9903325 17.438107,9.0722051 4.9259029,9.0569946 4.9284452,5.0338374 Z"
sodipodi:nodetypes="cccccc" />
<path
id="rect2-3"
style="fill:#f8fefb;fill-opacity:1;stroke:none;stroke-width:0.384;stroke-linecap:square"
d="m 6.5757719,13.021525 -2.065335,1.950283 2.021447,2.081873 12.5122061,-0.01521 -0.0025,-4.023157 z"
sodipodi:nodetypes="cccccc" />
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -8,7 +8,7 @@ import {ZodLocalDate} from "./local-date";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete"; import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodAlbum = ZodGenericDataSoftDelete.extend({ export const ZodAlbum = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(), name: zod.string().optional(),
description: zod.string().optional(), description: zod.string().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers
@ -30,7 +30,7 @@ export function isAlbum(data: any): data is Album {
} }
} }
export const ZodAlbumWrite = ZodGenericDataSoftDeleteWrite.extend({ export const ZodAlbumWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(), name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(), description: zod.string().nullable().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers

View File

@ -8,14 +8,14 @@ import {ZodLocalDate} from "./local-date";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete"; import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodArtist = ZodGenericDataSoftDelete.extend({ export const ZodArtist = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(), name: zod.string().optional(),
description: zod.string().optional(), description: zod.string().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers
*/ */
covers: zod.array(ZodObjectId).optional(), covers: zod.array(ZodObjectId).optional(),
firstName: zod.string().max(256).optional(), firstName: zod.string().optional(),
surname: zod.string().max(256).optional(), surname: zod.string().optional(),
birth: ZodLocalDate.optional(), birth: ZodLocalDate.optional(),
death: ZodLocalDate.optional(), death: ZodLocalDate.optional(),
@ -33,14 +33,14 @@ export function isArtist(data: any): data is Artist {
} }
} }
export const ZodArtistWrite = ZodGenericDataSoftDeleteWrite.extend({ export const ZodArtistWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(), name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(), description: zod.string().nullable().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers
*/ */
covers: zod.array(ZodObjectId).nullable().optional(), covers: zod.array(ZodObjectId).nullable().optional(),
firstName: zod.string().max(256).nullable().optional(), firstName: zod.string().nullable().optional(),
surname: zod.string().max(256).nullable().optional(), surname: zod.string().nullable().optional(),
birth: ZodLocalDate.nullable().optional(), birth: ZodLocalDate.nullable().optional(),
death: ZodLocalDate.nullable().optional(), death: ZodLocalDate.nullable().optional(),

View File

@ -7,7 +7,7 @@ import {ZodObjectId} from "./object-id";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete"; import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodGender = ZodGenericDataSoftDelete.extend({ export const ZodGender = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(), name: zod.string().optional(),
description: zod.string().optional(), description: zod.string().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers
@ -28,7 +28,7 @@ export function isGender(data: any): data is Gender {
} }
} }
export const ZodGenderWrite = ZodGenericDataSoftDeleteWrite.extend({ export const ZodGenderWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(), name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(), description: zod.string().nullable().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers

View File

@ -24,9 +24,7 @@ export function isGenericDataSoftDelete(data: any): data is GenericDataSoftDelet
return false; return false;
} }
} }
export const ZodGenericDataSoftDeleteWrite = ZodGenericDataWrite.extend({ export const ZodGenericDataSoftDeleteWrite = ZodGenericDataWrite;
});
export type GenericDataSoftDeleteWrite = zod.infer<typeof ZodGenericDataSoftDeleteWrite>; export type GenericDataSoftDeleteWrite = zod.infer<typeof ZodGenericDataSoftDeleteWrite>;

View File

@ -25,9 +25,7 @@ export function isGenericData(data: any): data is GenericData {
return false; return false;
} }
} }
export const ZodGenericDataWrite = ZodGenericTimingWrite.extend({ export const ZodGenericDataWrite = ZodGenericTimingWrite;
});
export type GenericDataWrite = zod.infer<typeof ZodGenericDataWrite>; export type GenericDataWrite = zod.infer<typeof ZodGenericDataWrite>;

View File

@ -7,7 +7,7 @@ import {ZodObjectId} from "./object-id";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete"; import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodPlaylist = ZodGenericDataSoftDelete.extend({ export const ZodPlaylist = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(), name: zod.string().optional(),
description: zod.string().optional(), description: zod.string().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers
@ -29,7 +29,7 @@ export function isPlaylist(data: any): data is Playlist {
} }
} }
export const ZodPlaylistWrite = ZodGenericDataSoftDeleteWrite.extend({ export const ZodPlaylistWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(), name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(), description: zod.string().nullable().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers

View File

@ -3,11 +3,11 @@
*/ */
import { z as zod } from "zod"; import { z as zod } from "zod";
import {ZodUUID} from "./uuid"; import {ZodObjectId} from "./object-id";
import {ZodInteger} from "./integer"; import {ZodInteger} from "./integer";
export const ZodRestErrorResponse = zod.object({ export const ZodRestErrorResponse = zod.object({
uuid: ZodUUID.optional(), oid: ZodObjectId.optional(),
name: zod.string(), name: zod.string(),
message: zod.string(), message: zod.string(),
time: zod.string(), time: zod.string(),

View File

@ -8,7 +8,7 @@ import {ZodLong} from "./long";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete"; import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodTrack = ZodGenericDataSoftDelete.extend({ export const ZodTrack = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(), name: zod.string().optional(),
description: zod.string().optional(), description: zod.string().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers
@ -34,7 +34,7 @@ export function isTrack(data: any): data is Track {
} }
} }
export const ZodTrackWrite = ZodGenericDataSoftDeleteWrite.extend({ export const ZodTrackWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(), name: zod.string().nullable().optional(),
description: zod.string().nullable().optional(), description: zod.string().nullable().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers

View File

@ -5,9 +5,7 @@ import { z as zod } from "zod";
import {ZodUser, ZodUserWrite } from "./user"; import {ZodUser, ZodUserWrite } from "./user";
export const ZodUserKarusic = ZodUser.extend({ export const ZodUserKarusic = ZodUser;
});
export type UserKarusic = zod.infer<typeof ZodUserKarusic>; export type UserKarusic = zod.infer<typeof ZodUserKarusic>;
@ -20,9 +18,7 @@ export function isUserKarusic(data: any): data is UserKarusic {
return false; return false;
} }
} }
export const ZodUserKarusicWrite = ZodUserWrite.extend({ export const ZodUserKarusicWrite = ZodUserWrite;
});
export type UserKarusicWrite = zod.infer<typeof ZodUserKarusicWrite>; export type UserKarusicWrite = zod.infer<typeof ZodUserKarusicWrite>;

View File

@ -8,7 +8,7 @@ import {ZodPartRight} from "./part-right";
export const ZodUserMe = zod.object({ export const ZodUserMe = zod.object({
id: ZodLong, id: ZodLong,
login: zod.string().max(255).optional(), login: zod.string().optional(),
/** /**
* Map<EntityName, Map<PartName, Right>> * Map<EntityName, Map<PartName, Right>>
*/ */

View File

@ -10,7 +10,7 @@ import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generi
export const ZodUser = ZodGenericDataSoftDelete.extend({ export const ZodUser = ZodGenericDataSoftDelete.extend({
login: zod.string().min(3).max(128), login: zod.string().min(3).max(128),
lastConnection: ZodTimestamp.optional(), lastConnection: ZodTimestamp.optional(),
blocked: zod.boolean(), blocked: zod.boolean().optional(),
blockedReason: zod.string().max(512).optional(), blockedReason: zod.string().max(512).optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers
@ -33,7 +33,7 @@ export function isUser(data: any): data is User {
export const ZodUserWrite = ZodGenericDataSoftDeleteWrite.extend({ export const ZodUserWrite = ZodGenericDataSoftDeleteWrite.extend({
login: zod.string().min(3).max(128).optional(), login: zod.string().min(3).max(128).optional(),
lastConnection: ZodTimestamp.nullable().optional(), lastConnection: ZodTimestamp.nullable().optional(),
blocked: zod.boolean(), blocked: zod.boolean().nullable().optional(),
blockedReason: zod.string().max(512).nullable().optional(), blockedReason: zod.string().max(512).nullable().optional(),
/** /**
* List of Id of the specific covers * List of Id of the specific covers

View File

@ -1,26 +1,12 @@
import { SyntheticEvent, useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Box, Flex, IconButton, SliderTrack, Text } from '@chakra-ui/react';
import { import {
Box,
Button,
Flex,
IconButton,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
Text,
position,
} from '@chakra-ui/react';
import {
MdCheck,
MdFastForward, MdFastForward,
MdFastRewind, MdFastRewind,
MdGraphicEq,
MdLooksOne, MdLooksOne,
MdNavigateBefore, MdNavigateBefore,
MdNavigateNext, MdNavigateNext,
MdOutlinePlayArrow,
MdPause, MdPause,
MdPlayArrow, MdPlayArrow,
MdRepeat, MdRepeat,
@ -29,15 +15,17 @@ import {
MdTrendingFlat, MdTrendingFlat,
} from 'react-icons/md'; } from 'react-icons/md';
import { useColorModeValue } from '@/components/ui/color-mode';
import { useActivePlaylistService } from '@/service/ActivePlaylist'; import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificAlbum } from '@/service/Album'; import { useSpecificAlbum } from '@/service/Album';
import { useSpecificArtists } from '@/service/Artist'; import { useSpecificArtists } from '@/service/Artist';
import { useSpecificGender } from '@/service/Gender'; import { useSpecificGender } from '@/service/Gender';
import { useSpecificTrack } from '@/service/Track'; import { useSpecificTrack } from '@/service/Track';
import { DataUrlAccess } from '@/utils/data-url-access'; import { DataUrlAccess } from '@/utils/data-url-access';
import { useThemeMode } from '@/utils/theme-tools';
import { isNullOrUndefined } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
import { Slider } from './ui/slider';
export enum PlayMode { export enum PlayMode {
PLAY_ONE, PLAY_ONE,
PLAY_ALL, PLAY_ALL,
@ -46,10 +34,16 @@ export enum PlayMode {
} }
const playModeIcon = { const playModeIcon = {
[PlayMode.PLAY_ONE]: <MdLooksOne size="30px" />, [PlayMode.PLAY_ONE]: <MdLooksOne style={{ width: '100%', height: '100%' }} />,
[PlayMode.PLAY_ALL]: <MdTrendingFlat size="30px" />, [PlayMode.PLAY_ALL]: (
[PlayMode.PLAY_ONE_LOOP]: <MdRepeatOne size="30px" />, <MdTrendingFlat style={{ width: '100%', height: '100%' }} />
[PlayMode.PLAY_ALL_LOOP]: <MdRepeat size="30px" />, ),
[PlayMode.PLAY_ONE_LOOP]: (
<MdRepeatOne style={{ width: '100%', height: '100%' }} />
),
[PlayMode.PLAY_ALL_LOOP]: (
<MdRepeat style={{ width: '100%', height: '100%' }} />
),
}; };
export type AudioPlayerProps = {}; export type AudioPlayerProps = {};
@ -66,7 +60,6 @@ const formatTime = (time) => {
}; };
export const AudioPlayer = ({}: AudioPlayerProps) => { export const AudioPlayer = ({}: AudioPlayerProps) => {
const { mode } = useThemeMode();
const { playTrackList, trackOffset, previous, next, first } = const { playTrackList, trackOffset, previous, next, first } =
useActivePlaylistService(); useActivePlaylistService();
const audioRef = useRef<HTMLAudioElement>(null); const audioRef = useRef<HTMLAudioElement>(null);
@ -89,14 +82,16 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
: '' : ''
); );
}, [dataTrack, setMediaSource]); }, [dataTrack, setMediaSource]);
const backColor = mode('back.100', 'back.800'); const backColor = useColorModeValue('back.100', 'back.800');
const configButton = { const configButton = {
borderRadius: 'full', borderRadius: 'full',
backgroundColor: '#00000000', backgroundColor: 'transparent',
_hover: { _hover: {
boxShadow: 'outline-over',
bgColor: 'brand.500', bgColor: 'brand.500',
}, },
width: '50px',
height: '50px',
padding: '5px',
}; };
useEffect(() => { useEffect(() => {
@ -209,6 +204,14 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
const onChangeStateToPause = () => { const onChangeStateToPause = () => {
setIsPlaying(false); setIsPlaying(false);
}; };
const marks = () => {
const minutes = Math.floor(duration / 60);
const result: number[] = [];
for (let i = 1; i <= minutes; i++) {
result.push(60 * i);
}
return result;
};
return ( return (
<> <>
{!isNullOrUndefined(trackOffset) && ( {!isNullOrUndefined(trackOffset) && (
@ -230,23 +233,23 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
direction="column" direction="column"
> >
<Text <Text
align="left" alignContent="left"
fontSize="20px" fontSize="20px"
fontWeight="bold" fontWeight="bold"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={1} // noOfLines={1}
> >
{dataTrack?.name ?? '???'} {dataTrack?.name ?? '???'}
</Text> </Text>
<Text <Text
align="left" alignContent="left"
fontSize="16px" fontSize="16px"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={1} // noOfLines={1}
> >
{dataArtists.map((data) => data.name).join(', ')} /{' '} {dataArtists.map((data) => data.name).join(', ')} /{' '}
{dataAlbum && dataAlbum?.name} {dataAlbum && dataAlbum?.name}
@ -254,35 +257,36 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
</Text> </Text>
<Box width="full" paddingX="15px"> <Box width="full" paddingX="15px">
<Slider <Slider
aria-label="slider-ex-4" defaultValue={[0]}
defaultValue={0} value={[timeProgress]}
value={timeProgress}
min={0} min={0}
max={duration} max={duration}
step={0.1} step={0.1}
onChange={onSeek} onValueChange={(e) => onSeek(e.value)}
focusThumbOnChange={false} variant="outline"
colorPalette="brand"
marks={marks()}
//focusCapture={false}
> >
<SliderTrack bg="gray.200" height="10px" borderRadius="full"> <SliderTrack
<SliderFilledTrack bg="brand.600" /> bg="brand.200"
</SliderTrack> height="10px"
<SliderThumb boxSize={6}> borderRadius="full"
<Box color="brand.600" as={MdGraphicEq} /> ></SliderTrack>
</SliderThumb>
</Slider> </Slider>
</Box> </Box>
<Flex> <Flex>
<Text <Text
align="left" alignContent="left"
fontSize="16px" fontSize="16px"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={1} // noOfLines={1}
> >
{formatTime(timeProgress)} {formatTime(timeProgress)}
</Text> </Text>
<Text align="left" fontSize="16px" userSelect="none"> <Text alignContent="left" fontSize="16px" userSelect="none">
{formatTime(duration)} {formatTime(duration)}
</Text> </Text>
</Flex> </Flex>
@ -290,53 +294,65 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'Play'} aria-label={'Play'}
icon={
isPlaying ? (
<MdPause size="30px" />
) : (
<MdPlayArrow size="30px" />
)
}
onClick={onPlay} onClick={onPlay}
/> variant="ghost"
>
{isPlaying ? (
<MdPause style={{ width: '100%', height: '100%' }} />
) : (
<MdPlayArrow style={{ width: '100%', height: '100%' }} />
)}
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'Stop'} aria-label={'Stop'}
icon={<MdStop size="30px" />}
onClick={onStop} onClick={onStop}
/> variant="ghost"
>
<MdStop style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'Previous track'} aria-label={'Previous track'}
icon={<MdNavigateBefore size="30px" />}
onClick={onNavigatePrevious} onClick={onNavigatePrevious}
marginLeft="auto" marginLeft="auto"
/> variant="ghost"
>
<MdNavigateBefore style={{ width: '100%', height: '100%' }} />{' '}
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'jump 15sec in past'} aria-label={'jump 15sec in past'}
icon={<MdFastRewind size="30px" />}
onClick={onFastRewind} onClick={onFastRewind}
/> variant="ghost"
>
<MdFastRewind style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'jump 15sec in future'} aria-label={'jump 15sec in future'}
icon={<MdFastForward size="30px" />}
onClick={onFastForward} onClick={onFastForward}
/> variant="ghost"
>
<MdFastForward style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'Next track'} aria-label={'Next track'}
icon={<MdNavigateNext size="30px" />}
marginRight="auto" marginRight="auto"
onClick={onNavigateNext} onClick={onNavigateNext}
/> variant="ghost"
>
<MdNavigateNext style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'continue to the end'} aria-label={'continue to the end'}
icon={playModeIcon[playingMode]}
onClick={onTypePlay} onClick={onTypePlay}
/> variant="ghost"
>
{playModeIcon[playingMode]}
</IconButton>
</Flex> </Flex>
</Flex> </Flex>
)} )}

View File

@ -1,16 +1,17 @@
import { ReactElement, useEffect, useState } from 'react'; import { ReactElement, useEffect, useState } from 'react';
import { As, Box, BoxProps, Flex, StyleProps } from '@chakra-ui/react'; import { Box, BoxProps, Flex, FlexProps } from '@chakra-ui/react';
import { Image } from '@chakra-ui/react'; import { Image } from '@chakra-ui/react';
import { DataUrlAccess } from '@/utils/data-url-access';
import { Icon } from './Icon';
import { ObjectId } from '@/back-api'; import { ObjectId } from '@/back-api';
import { DataUrlAccess } from '@/utils/data-url-access';
export type CoversProps = BoxProps & { import { Icon } from './Icon';
export type CoversProps = Omit<BoxProps, 'iconEmpty'> & {
data?: ObjectId[]; data?: ObjectId[];
size?: StyleProps["width"]; size?: BoxProps['width'];
iconEmpty?: As; iconEmpty?: ReactElement;
slideshow?: boolean; slideshow?: boolean;
}; };
@ -33,7 +34,9 @@ export const Covers = ({
setPreviousImageIndex(currentImageIndex); setPreviousImageIndex(currentImageIndex);
setTopOpacity(0.0); setTopOpacity(0.0);
setTimeout(() => { setTimeout(() => {
setCurrentImageIndex((prevIndex) => (prevIndex + 1) % (data?.length ?? 1)); setCurrentImageIndex(
(prevIndex) => (prevIndex + 1) % (data?.length ?? 1)
);
setTopOpacity(1.0); setTopOpacity(1.0);
}, 1500); }, 1500);
}, 3000); }, 3000);
@ -42,7 +45,7 @@ export const Covers = ({
if (!data || data.length < 1) { if (!data || data.length < 1) {
if (iconEmpty) { if (iconEmpty) {
return <Icon icon={iconEmpty} sizeIcon={size} />; return <Icon children={iconEmpty} sizeIcon={size} />;
} else { } else {
return ( return (
<Box <Box
@ -60,11 +63,26 @@ export const Covers = ({
} }
if (slideshow === false || data.length === 1) { if (slideshow === false || data.length === 1) {
const url = DataUrlAccess.getThumbnailUrl(data[0]); const url = DataUrlAccess.getThumbnailUrl(data[0]);
return <Image loading="lazy" src={url} maxWidth={size} boxSize={size} {...rest} />; return (
<Image
loading="lazy"
src={url}
maxWidth={size}
boxSize={size} /*{...rest}*/
/>
);
} }
const urlCurrent = DataUrlAccess.getThumbnailUrl(data[currentImageIndex]); const urlCurrent = DataUrlAccess.getThumbnailUrl(data[currentImageIndex]);
const urlPrevious = DataUrlAccess.getThumbnailUrl(data[previousImageIndex]); const urlPrevious = DataUrlAccess.getThumbnailUrl(data[previousImageIndex]);
return <Flex position="relative" {...rest} maxWidth={size} width={size} height={size} overflow="hidden"> return (
<Flex
position="relative"
// {...rest}
maxWidth={size}
width={size}
height={size}
overflow="hidden"
>
<Image <Image
src={urlPrevious} src={urlPrevious}
loading="lazy" loading="lazy"
@ -90,4 +108,5 @@ export const Covers = ({
zIndex={2} zIndex={2}
/> />
</Flex> </Flex>
);
}; };

View File

@ -0,0 +1,117 @@
import { useState } from 'react';
import {
Box,
Button,
Dialog,
Select,
Stack,
Text,
createListCollection,
useDisclosure,
} from '@chakra-ui/react';
import { environment } from '@/environment';
import { USERS } from '@/service/session';
import { hashLocalData } from '@/utils/sso';
export const USERS_COLLECTION = createListCollection({
items: [
{ label: 'karadmin', value: 'adminA@666' },
{ label: 'karuser', value: 'userA@666' },
{ label: 'NO_USER', value: '' },
],
});
export const EnvDevelopment = () => {
const dialog = useDisclosure();
const [selectUserTest, setSelectUserTest] = useState<string>('NO_USER');
//const setUser = useRightsStore((store) => store.setUser);
const buildEnv =
process.env.NODE_ENV === 'development'
? 'Development'
: import.meta.env.VITE_DEV_ENV_NAME;
const envName: Array<string> = [];
!!buildEnv && envName.push(buildEnv);
if (!envName.length) {
return null;
}
const handleChange = (selectedOption) => {
console.log(`SELECT: [${selectedOption.target.value}]`);
setSelectUserTest(selectedOption.target.value);
};
const onClose = () => {
dialog.onClose();
if (selectUserTest == 'NO_USER') {
window.location.href = `/${environment.applName}/sso/${hashLocalData()}/false/__LOGOUT__`;
} else {
window.location.href = `/${environment.applName}/sso/${hashLocalData()}/true/${USERS[selectUserTest]}`;
}
};
return (
<>
<Box
as="button"
zIndex="100000"
position="fixed"
top="0"
insetStart="0"
insetEnd="0"
h="2px"
bg="warning.400"
cursor="pointer"
data-test-id="devtools"
onClick={dialog.onOpen}
>
<Text
position="fixed"
top="0"
insetStart="4"
bg="warning.400"
color="warning.900"
fontSize="0.6rem"
fontWeight="bold"
px="10px"
marginLeft="25%"
borderBottomStartRadius="sm"
borderBottomEndRadius="sm"
textTransform="uppercase"
>
{envName.join(' : ')}
</Text>
</Box>
<Dialog.Root open={dialog.open} onOpenChange={dialog.onClose}>
<Dialog.Positioner>
<Dialog.Backdrop />
<Dialog.Content>
<Dialog.Header>Outils développeurs</Dialog.Header>
<Dialog.Body>
<Stack>
<Text>User</Text>
<Select.Root
onChange={handleChange}
collection={USERS_COLLECTION}
>
<Select.Trigger>
<Select.ValueText placeholder="Select test user" />
</Select.Trigger>
<Select.Content>
{USERS_COLLECTION.items.map((value) => (
<Select.Item item={value} key={value.value}>
{value.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
</Stack>
</Dialog.Body>
<Dialog.Footer>
<Button onClick={onClose}>Close</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Positioner>
</Dialog.Root>
</>
);
};

View File

@ -1,25 +1,18 @@
import { import { ReactNode, forwardRef } from 'react';
As,
Box, import { Box, Flex, FlexProps } from '@chakra-ui/react';
BoxProps,
Icon as ChakraIcon,
IconProps as ChakraIconProps,
Flex,
FlexProps,
forwardRef,
LayoutProps,
} from '@chakra-ui/react';
export type IconProps = FlexProps & { export type IconProps = FlexProps & {
icon: As; children: ReactNode;
color?: string; color?: string;
sizeIcon?: LayoutProps['width']; sizeIcon?: FlexProps['width'];
}; };
export const Icon = forwardRef<IconProps, 'span'>( export const Icon = forwardRef<HTMLDivElement, IconProps>(
({ icon: IconEl, color, sizeIcon = '1em', ...rest }, ref) => { ({ children, color, sizeIcon = '1em', ...rest }, ref) => {
return ( return (
<Flex flex="none" <Flex
flex="none"
minWidth={sizeIcon} minWidth={sizeIcon}
minHeight={sizeIcon} minHeight={sizeIcon}
maxWidth={sizeIcon} maxWidth={sizeIcon}
@ -27,16 +20,21 @@ export const Icon = forwardRef<IconProps, 'span'>(
align="center" align="center"
padding="1px" padding="1px"
ref={ref} ref={ref}
{...rest}> {...rest}
>
<Box <Box
marginX="auto" marginX="auto"
as={IconEl}
width="100%" width="100%"
minWidth="100%" minWidth="100%"
height="100%" height="100%"
color={color} color={color}
/> asChild
>
{children}
</Box>
</Flex> </Flex>
); );
} }
); );
Icon.displayName = 'Icon';

View File

@ -3,7 +3,7 @@ import React, { ReactNode, useEffect } from 'react';
import { Flex, Image } from '@chakra-ui/react'; import { Flex, Image } from '@chakra-ui/react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import background from '@/assets/images/ikon.svg'; import ikon from '@/assets/images/ikon.svg';
import { TOP_BAR_HEIGHT } from '@/components/TopBar/TopBar'; import { TOP_BAR_HEIGHT } from '@/components/TopBar/TopBar';
export type LayoutProps = React.PropsWithChildren<unknown> & { export type LayoutProps = React.PropsWithChildren<unknown> & {
@ -28,9 +28,9 @@ export const PageLayout = ({ children }: LayoutProps) => {
left={0} left={0}
right={0} right={0}
minWidth="300px" minWidth="300px"
zIndex={-1} zIndex={0}
> >
<Image src={background} boxSize="90%" margin="auto" opacity="30%" /> <Image src={ikon} boxSize="90vh" margin="auto" />
</Flex> </Flex>
<Flex <Flex
direction="column" direction="column"

View File

@ -1,11 +1,11 @@
import React, { ReactNode, useEffect } from 'react'; import { ReactNode, useEffect } from 'react';
import { Flex, FlexProps } from '@chakra-ui/react'; import { Flex, FlexProps } from '@chakra-ui/react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { PageLayout } from '@/components/Layout/PageLayout'; import { PageLayout } from '@/components/Layout/PageLayout';
import { colors } from '@/theme/foundations/colors'; import { useColorModeValue } from '@/components/ui/color-mode';
import { useThemeMode } from '@/utils/theme-tools'; import { colors } from '@/theme/colors';
export type LayoutProps = FlexProps & { export type LayoutProps = FlexProps & {
children: ReactNode; children: ReactNode;
@ -22,7 +22,6 @@ export const PageLayoutInfoCenter = ({
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, [pathname]); }, [pathname]);
const { mode } = useThemeMode();
return ( return (
<PageLayout> <PageLayout>
<Flex <Flex
@ -34,7 +33,7 @@ export const PageLayoutInfoCenter = ({
borderRadius="8px" borderRadius="8px"
padding="10px" padding="10px"
boxShadow={'0px 0px 16px ' + colors.back[900]} boxShadow={'0px 0px 16px ' + colors.back[900]}
backgroundColor={mode('#FFFFFF', '#000000')} backgroundColor={useColorModeValue('#FFFFFF', '#000000')}
{...rest} {...rest}
> >
{children} {children}

View File

@ -0,0 +1,20 @@
export {
ParameterLayoutContent as Content,
type ParameterLayoutContentProps as ContentProps,
} from './ParameterLayoutContent';
export {
ParameterLayoutFooter as Footer,
type ParameterLayoutFooterProps as FooterProps,
} from './ParameterLayoutFooter';
export {
ParameterLayoutHeader as Header,
type ParameterLayoutHeaderProps as HeaderProps,
} from './ParameterLayoutHeader';
export {
ParameterLayoutHeaderBase as HeaderBase,
type ParameterLayoutHeaderBaseProps as HeaderBaseProps,
} from './ParameterLayoutHeaderBase';
export {
ParameterLayoutRoot as Root,
type ParameterLayoutRootProps as RootProps,
} from './ParameterLayoutRoot';

View File

@ -0,0 +1,25 @@
import { ReactNode } from 'react';
import { Flex } from '@chakra-ui/react';
export type ParameterLayoutContentProps = {
children?: ReactNode;
};
export const ParameterLayoutContent = ({
children,
}: ParameterLayoutContentProps) => {
return (
<Flex
direction="column"
width="full"
borderY="1px solid black"
paddingY="15px"
paddingX="25px"
minHeight="10px"
background="gray.700"
>
{children}
</Flex>
);
};

View File

@ -0,0 +1,17 @@
import { ReactNode } from 'react';
import { Flex } from '@chakra-ui/react';
export type ParameterLayoutFooterProps = {
children?: ReactNode;
};
export const ParameterLayoutFooter = ({
children,
}: ParameterLayoutFooterProps) => {
return (
<Flex width="full" paddingY="15px" paddingX="25px" minHeight="10px">
{children}
</Flex>
);
};

View File

@ -0,0 +1,17 @@
import { ReactNode } from 'react';
import { Flex } from '@chakra-ui/react';
export type ParameterLayoutHeaderProps = {
children?: ReactNode;
};
export const ParameterLayoutHeader = ({
children,
}: ParameterLayoutHeaderProps) => {
return (
<Flex width="full" paddingY="15px" paddingX="25px" minHeight="10px">
{children}
</Flex>
);
};

View File

@ -0,0 +1,24 @@
import { Flex, Text } from '@chakra-ui/react';
import { ParameterLayoutHeader } from './ParameterLayoutHeader';
export type ParameterLayoutHeaderBaseProps = {
title: string;
description?: string;
};
export const ParameterLayoutHeaderBase = ({
title,
description,
}: ParameterLayoutHeaderBaseProps) => {
return (
<ParameterLayoutHeader>
<Flex direction="column">
<Text fontSize="25px" fontWeight="bold">
{title}
</Text>
{description && <Text>{description}</Text>}
</Flex>
</ParameterLayoutHeader>
);
};

View File

@ -0,0 +1,24 @@
import { ReactNode } from 'react';
import { VStack } from '@chakra-ui/react';
export type ParameterLayoutRootProps = {
children?: ReactNode;
};
export const ParameterLayoutRoot = ({ children }: ParameterLayoutRootProps) => {
return (
<VStack
gap="0px"
marginX="15%"
marginY="20px"
justify="center"
//borderRadius="20px"
borderRadius="0px"
border="1px solid black"
background="gray.500"
>
{children}
</VStack>
);
};

View File

@ -0,0 +1 @@
export * as ParameterLayout from './ParameterLayout';

View File

@ -1,10 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { import { Group, Input } from '@chakra-ui/react';
Input,
InputGroup,
InputLeftElement,
} from '@chakra-ui/react';
import { MdSearch } from 'react-icons/md'; import { MdSearch } from 'react-icons/md';
export type SearchInputProps = { export type SearchInputProps = {
@ -44,16 +40,14 @@ export const SearchInput = ({
} }
} }
return ( return (
<InputGroup maxWidth="200px" marginLeft="auto" {...searchInputProperty}> <Group maxWidth="200px" marginLeft="auto" {...searchInputProperty}>
<InputLeftElement pointerEvents="none">
<MdSearch color="gray.300" /> <MdSearch color="gray.300" />
</InputLeftElement>
<Input <Input
onFocus={onFocusKeep} onFocus={onFocusKeep}
onBlur={() => setTimeout(() => onFocusLost(), 200)} onBlur={() => setTimeout(() => onFocusLost(), 200)}
onChange={onChange} onChange={onChange}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
</InputGroup> </Group>
); );
}; };

View File

@ -1,49 +1,36 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import {
Button,
Drawer, import { Box, Button, ConditionalValue, Flex, HStack, IconButton, Span, Text, useDisclosure } from '@chakra-ui/react';
DrawerBody, import { LuAlignJustify, LuArrowBigLeft, LuCircleUserRound, LuKeySquare, LuLogIn, LuLogOut, LuMoon, LuSettings, LuSun } from 'react-icons/lu';
DrawerContent, import { MdHelp, MdHome, MdMore, MdOutlinePlaylistPlay, MdOutlineUploadFile, MdSupervisedUserCircle } from 'react-icons/md';
DrawerHeader,
DrawerOverlay,
Flex,
HStack,
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
Text,
useDisclosure,
} from '@chakra-ui/react';
import {
LuAlignJustify,
LuArrowBigLeft,
LuArrowUpSquare,
LuHelpCircle,
LuHome,
LuLogIn,
LuLogOut,
LuMoon,
LuPlusCircle,
LuSettings,
LuSun,
LuUserCircle,
} from 'react-icons/lu';
import { useNavigate } from 'react-router-dom'; 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 { useServiceContext } from '@/service/ServiceContext'; import { useServiceContext } from '@/service/ServiceContext';
import { SessionState } from '@/service/SessionState'; import { SessionState } from '@/service/SessionState';
import { colors } from '@/theme/foundations/colors';
import { requestSignIn, requestSignOut, requestSignUp } from '@/utils/sso';
import { useThemeMode } from '@/utils/theme-tools';
import { useSessionService } from '@/service/session'; import { useSessionService } from '@/service/session';
import { colors } from '@/theme/colors';
import { requestOpenSite, requestSignIn, requestSignOut, requestSignUp } from '@/utils/sso';
export const TOP_BAR_HEIGHT = '50px'; export const TOP_BAR_HEIGHT = '50px';
export const BUTTON_TOP_BAR_PROPERTY = { export const BUTTON_TOP_BAR_PROPERTY = {
variant: '@menu', variant: 'ghost' as ConditionalValue<
'ghost' | 'outline' | 'solid' | 'subtle' | 'surface' | 'plain' | undefined
>,
//colorPalette: "brand",
fontSize: '20px',
textTransform: 'uppercase',
height: TOP_BAR_HEIGHT, height: TOP_BAR_HEIGHT,
}; };
@ -52,17 +39,51 @@ export type TopBarProps = {
title?: string; title?: string;
}; };
const ButtonMenuLeft = ({
dest,
title,
icon,
onClickEnd = () => {},
}: {
dest: string;
title: string;
icon: ReactNode;
onClickEnd?: () => void;
}) => {
const navigate = useNavigate();
return (
<>
<Button
background="#00000000"
borderRadius="0px"
onClick={() => {
navigate(dest);
onClickEnd();
}}
width="full"
{...BUTTON_TOP_BAR_PROPERTY}
>
<Box asChild style={{ width: '45px', height: '45px' }}>
{icon}
</Box>
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
{title}
</Text>
</Button>
<Box marginY="5" marginX="10" height="2px" background="brand.600" />
</>
);
};
export const TopBar = ({ title, children }: TopBarProps) => { export const TopBar = ({ title, children }: TopBarProps) => {
const { mode, colorMode, toggleColorMode } = useThemeMode(); const navigate = useNavigate();
const { clearToken } = useSessionService(); const { colorMode, toggleColorMode } = useColorMode();
const { session } = useServiceContext(); const { session } = useServiceContext();
const backColor = mode('back.100', 'back.800'); const { clearToken } = useSessionService();
const backColor = useColorModeValue('back.100', 'back.800');
const drawerDisclose = useDisclosure(); const drawerDisclose = useDisclosure();
const onChangeTheme = () => { const onChangeTheme = () => {
drawerDisclose.onOpen(); drawerDisclose.onOpen();
}; };
const navigate = useNavigate();
const onSignIn = (): void => { const onSignIn = (): void => {
clearToken(); clearToken();
requestSignIn(); requestSignIn();
@ -75,17 +96,8 @@ export const TopBar = ({ title, children }: TopBarProps) => {
clearToken(); clearToken();
requestSignOut(); requestSignOut();
}; };
const onSelectAdd = () => { const onKarso = (): void => {
navigate('/add'); requestOpenSite();
};
const onSelectHome = () => {
navigate('/');
};
const onHelp = () => {
navigate('/help');
};
const onSettings = () => {
navigate('/settings');
}; };
return ( return (
<Flex <Flex
@ -103,10 +115,12 @@ export const TopBar = ({ title, children }: TopBarProps) => {
zIndex={200} zIndex={200}
> >
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onChangeTheme}> <Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onChangeTheme}>
<HStack>
<LuAlignJustify /> <LuAlignJustify />
<Text paddingLeft="3px" fontWeight="bold"> <Text paddingLeft="3px" fontWeight="bold">
Karusic Karusic
</Text> </Text>
</HStack>
</Button> </Button>
{title && ( {title && (
<Text <Text
@ -115,6 +129,7 @@ export const TopBar = ({ title, children }: TopBarProps) => {
textTransform="uppercase" textTransform="uppercase"
marginRight="auto" marginRight="auto"
userSelect="none" userSelect="none"
color="brand.500"
> >
{title} {title}
</Text> </Text>
@ -134,7 +149,7 @@ export const TopBar = ({ title, children }: TopBarProps) => {
onClick={onSignUp} onClick={onSignUp}
disabled={true} disabled={true}
> >
<LuPlusCircle /> <MdMore />
<Text paddingLeft="3px" fontWeight="bold"> <Text paddingLeft="3px" fontWeight="bold">
Sign-up Sign-up
</Text> </Text>
@ -142,86 +157,111 @@ export const TopBar = ({ title, children }: TopBarProps) => {
</> </>
)} )}
{session?.state === SessionState.CONNECTED && ( {session?.state === SessionState.CONNECTED && (
<Menu> <MenuRoot>
<MenuButton <MenuTrigger asChild>
as={IconButton} <IconButton {...BUTTON_TOP_BAR_PROPERTY} width={TOP_BAR_HEIGHT}>
aria-label="Options" <LuCircleUserRound />
icon={<LuUserCircle />} </IconButton>
{...BUTTON_TOP_BAR_PROPERTY} </MenuTrigger>
width={TOP_BAR_HEIGHT} <MenuContent>
/> <MenuItem
<MenuList> value="user"
<MenuItem _hover={{}} color={mode('brand.800', 'brand.200')}> valueText="user"
Sign in as {session?.login ?? 'Fail'} color={useColorModeValue('brand.800', 'brand.200')}
>
<MdSupervisedUserCircle />
<Box flex="1">Sign in as {session?.login ?? 'Fail'}</Box>
</MenuItem> </MenuItem>
<MenuItem icon={<LuSettings />} onClick={onSettings}>Settings</MenuItem> <MenuItem
<MenuItem icon={<LuHelpCircle />} onClick={onHelp}>Help</MenuItem> value="Settings"
<MenuItem icon={<LuLogOut />} onClick={onSignOut}> valueText="Settings"
Sign-out onClick={() => navigate('/settings')}
>
<LuSettings />
Settings
</MenuItem>
<MenuItem
value="Help"
valueText="Help"
onClick={() => navigate('/help')}
>
<MdHelp /> Help
</MenuItem>
<MenuItem
value="Sign-out"
valueText="Sign-out"
onClick={onSignOut}
>
<LuLogOut /> Sign-out
</MenuItem>
<MenuItem value="karso" valueText="Karso" onClick={onKarso}>
<LuKeySquare /> Karso (SSO)
</MenuItem> </MenuItem>
{colorMode === 'light' ? ( {colorMode === 'light' ? (
<MenuItem icon={<LuMoon />} onClick={toggleColorMode}> <MenuItem
Set dark mode value="set-dark"
valueText="set-dark"
onClick={toggleColorMode}
>
<LuMoon /> Set dark mode
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem icon={<LuSun />} onClick={toggleColorMode}> <MenuItem
Set light mode value="set-light"
valueText="set-light"
onClick={toggleColorMode}
>
<LuSun /> Set light mode
</MenuItem> </MenuItem>
)} )}
</MenuList> </MenuContent>
</Menu> </MenuRoot>
)} )}
</Flex> </Flex>
<Drawer <DrawerRoot
placement="left" placement="start"
onClose={drawerDisclose.onClose} onOpenChange={drawerDisclose.onClose}
isOpen={drawerDisclose.isOpen} open={drawerDisclose.open}
data-testid="top-bar_drawer-root"
> >
<DrawerOverlay /> <DrawerContent data-testid="top-bar_drawer-content">
<DrawerContent>
<DrawerHeader <DrawerHeader
paddingY="auto" paddingY="auto"
as="button" as="button"
onClick={drawerDisclose.onClose} onClick={drawerDisclose.onClose}
boxShadow={'0px 2px 4px ' + colors.back[900]} boxShadow={'0px 2px 4px ' + colors.back[900]}
backgroundColor={backColor} backgroundColor={backColor}
color={mode('brand.900', 'brand.50')} color={useColorModeValue('brand.900', 'brand.50')}
textTransform="uppercase" textTransform="uppercase"
> >
<HStack height={TOP_BAR_HEIGHT}> <HStack {...BUTTON_TOP_BAR_PROPERTY} cursor="pointer">
<LuArrowBigLeft /> <LuArrowBigLeft />
<Text as="span" paddingLeft="3px"> <Span paddingLeft="3px">Karusic</Span>
Karusic
</Text>
</HStack> </HStack>
</DrawerHeader> </DrawerHeader>
<DrawerBody paddingX="0px"> <DrawerBody paddingX="0px">
<Button <Box marginY="3" />
background="#00000000" <ButtonMenuLeft
borderRadius="0px" onClickEnd={drawerDisclose.onClose}
onClick={onSelectHome} dest="/"
width="full" title="Home"
> icon={<MdHome />}
<LuHome /> />
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto"> <ButtonMenuLeft
Home onClickEnd={drawerDisclose.onClose}
</Text> dest="/on-air"
</Button> title="On air"
<hr /> icon={<MdOutlinePlaylistPlay />}
<Button />
background="#00000000" <ButtonMenuLeft
borderRadius="0px" onClickEnd={drawerDisclose.onClose}
onClick={onSelectAdd} dest="/add"
width="full" title="Add Media"
> icon={<MdOutlineUploadFile />}
<LuArrowUpSquare /> />
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
Add Media
</Text>
</Button>
</DrawerBody> </DrawerBody>
</DrawerContent> </DrawerContent>
</Drawer> </DrawerRoot>
</Flex> </Flex>
); );
}; };

View File

@ -1,10 +1,10 @@
import { Flex, Text } from '@chakra-ui/react'; import { Flex, Span } from '@chakra-ui/react';
import { LuDisc3 } from 'react-icons/lu'; import { LuDisc3 } from 'react-icons/lu';
import { Album } from '@/back-api'; import { Album } from '@/back-api';
import { Covers } from '@/components/Cover'; import { Covers } from '@/components/Cover';
import { useCountTracksWithAlbumId } from '@/service/Track';
import { BASE_WRAP_ICON_SIZE } from '@/constants/genericSpacing'; import { BASE_WRAP_ICON_SIZE } from '@/constants/genericSpacing';
import { useCountTracksWithAlbumId } from '@/service/Track';
export type DisplayAlbumProps = { export type DisplayAlbumProps = {
dataAlbum?: Album; dataAlbum?: Album;
@ -19,12 +19,17 @@ export const DisplayAlbum = ({ dataAlbum }: DisplayAlbumProps) => {
); );
} }
return ( return (
<Flex direction="row" width="full" height="full"> <Flex
direction="row"
width="full"
height="full"
data-testid="display-album_flex"
>
<Covers <Covers
data={dataAlbum?.covers} data={dataAlbum?.covers}
size={BASE_WRAP_ICON_SIZE} size={BASE_WRAP_ICON_SIZE}
flex={1} flex={1}
iconEmpty={LuDisc3} iconEmpty={<LuDisc3 />}
/> />
<Flex <Flex
direction="column" direction="column"
@ -35,29 +40,27 @@ export const DisplayAlbum = ({ dataAlbum }: DisplayAlbumProps) => {
overflowX="hidden" overflowX="hidden"
flex={1} flex={1}
> >
<Text <Span
as="span" textAlign="left"
align="left"
fontSize="20px" fontSize="20px"
fontWeight="bold" fontWeight="bold"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={[1, 2]} // noOfLines={[1, 2]}
> >
{dataAlbum?.name} {dataAlbum?.name}
</Text> </Span>
<Text <Span
as="span" textAlign="left"
align="left"
fontSize="15px" fontSize="15px"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={1} // noOfLines={1}
> >
{countTracksOfAnAlbum} track{countTracksOfAnAlbum >= 1 && 's'} {countTracksOfAnAlbum} track{countTracksOfAnAlbum >= 1 && 's'}
</Text> </Span>
</Flex> </Flex>
</Flex> </Flex>
); );

View File

@ -6,5 +6,5 @@ export type DisplayAlbumIdProps = {
}; };
export const DisplayAlbumId = ({ id }: DisplayAlbumIdProps) => { export const DisplayAlbumId = ({ id }: DisplayAlbumIdProps) => {
const { dataAlbum } = useSpecificAlbum(id); const { dataAlbum } = useSpecificAlbum(id);
return <DisplayAlbum dataAlbum={dataAlbum} />; return <DisplayAlbum dataAlbum={dataAlbum} data-testid="display-album-id" />;
}; };

View File

@ -1,15 +1,12 @@
import { useState } from 'react'; import { ReactNode } from 'react';
import { import { Box } from '@chakra-ui/react';
IconButton,
Menu,
MenuButton,
MenuItem,
MenuList,
} from '@chakra-ui/react';
import { LuMenu } from 'react-icons/lu'; import { LuMenu } from 'react-icons/lu';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '../ui/menu';
export type MenuElement = { export type MenuElement = {
icon?: ReactNode;
name: string; name: string;
onClick: () => void; onClick: () => void;
}; };
@ -23,20 +20,32 @@ export const ContextMenu = ({ elements }: ContextMenuProps) => {
return <></>; return <></>;
} }
return ( return (
<Menu> <MenuRoot data-testid="context-menu">
<MenuButton <MenuTrigger
as={IconButton} asChild
aria-label="Options"
icon={<LuMenu />}
marginY="auto" marginY="auto"
/> marginRight="4px"
<MenuList> data-testid="context-menu_trigger"
>
<Box asChild color="brand.500" cursor="pointer">
<LuMenu />
</Box>
</MenuTrigger>
<MenuContent data-testid="context-menu_content">
{elements?.map((data) => ( {elements?.map((data) => (
<MenuItem key={data.name} onClick={data.onClick}> <MenuItem
key={data.name}
value={data.name}
onClick={data.onClick}
height="65px"
fontSize="25px"
data-test-id="context-menu_item"
>
{data.icon}
{data.name} {data.name}
</MenuItem> </MenuItem>
))} ))}
</MenuList> </MenuContent>
</Menu> </MenuRoot>
); );
}; };

View File

@ -1,25 +1,13 @@
import { import { DragEventHandler, ReactNode, RefObject } from 'react';
DragEventHandler,
RefObject,
} from 'react';
import { import { Box, BoxProps, Center, Flex, HStack, Image } from '@chakra-ui/react';
Box, import { MdHighlightOff, MdUploadFile } from 'react-icons/md';
BoxProps,
Center,
Image,
Wrap,
WrapItem,
} from '@chakra-ui/react';
import {
MdHighlightOff,
MdUploadFile,
} from 'react-icons/md';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup } from '@/components/form/FormGroup';
import { UseFormidableReturn } from '@/components/form/Formidable';
import { DataUrlAccess } from '@/utils/data-url-access'; import { DataUrlAccess } from '@/utils/data-url-access';
import { useFormidableContextElement } from '../formidable';
export type DragNdropProps = { export type DragNdropProps = {
onFilesSelected?: (file: File[]) => void; onFilesSelected?: (file: File[]) => void;
onUriSelected?: (uri: string) => void; onUriSelected?: (uri: string) => void;
@ -92,33 +80,33 @@ export const DragNdrop = ({
}; };
export type CenterIconProps = BoxProps & { export type CenterIconProps = BoxProps & {
icon: any; children: ReactNode;
sizeIcon?: string; sizeIcon?: string;
}; };
export const CenterIcon = ({ export const CenterIcon = ({
icon: IconEl, children,
sizeIcon = '15px', sizeIcon = '15px',
...rest ...rest
}: CenterIconProps) => { }: CenterIconProps) => {
return ( return (
<Box position="relative" w={sizeIcon} h={sizeIcon} flex="none" {...rest}> <Box position="relative" w={sizeIcon} h={sizeIcon} flex="none" {...rest}>
<Box <Box
as={IconEl}
w={sizeIcon} w={sizeIcon}
h={sizeIcon} h={sizeIcon}
position="absolute" position="absolute"
top="50%" top="50%"
left="50%" left="50%"
transform="translate(-50%, -50%)" transform="translate(-50%, -50%)"
/> >
{children}
</Box>
</Box> </Box>
); );
}; };
export type FormCoversProps = { export type FormCoversProps = {
form: UseFormidableReturn; name: string;
variableName: string;
ref?: RefObject<any>; ref?: RefObject<any>;
label?: string; label?: string;
isRequired?: boolean; isRequired?: boolean;
@ -127,51 +115,48 @@ export type FormCoversProps = {
onRemove?: (index: number) => void; onRemove?: (index: number) => void;
}; };
/** This field component is a direct insertion component ==> not manage with formidable */
export const FormCovers = ({ export const FormCovers = ({
form, name,
variableName,
ref, ref,
onFilesSelected = () => {}, onFilesSelected = () => {},
onUriSelected = () => {}, onUriSelected = () => {},
onRemove = () => {}, onRemove = () => {},
...rest ...rest
}: FormCoversProps) => { }: FormCoversProps) => {
const urls = const { value } = useFormidableContextElement(name);
DataUrlAccess.getListThumbnailUrl(form.values[variableName]) ?? []; const urls = DataUrlAccess.getListThumbnailUrl(value) ?? [];
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
isModify={form.isModify[variableName]} <HStack wrap="wrap" width="full">
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<Wrap width="full">
{urls.map((data, index) => ( {urls.map((data, index) => (
<WrapItem key={data}> <Flex align="flex-start" key={data}>
<Box width="125px" height="125px" position="relative"> <Box width="125px" height="125px" position="relative">
<Box width="125px" height="125px" position="absolute"> <Box width="125px" height="125px" position="absolute">
<CenterIcon <CenterIcon
icon={MdHighlightOff}
width="125px" width="125px"
sizeIcon="100%" sizeIcon="100%"
zIndex="+1" zIndex="+1"
color="#00000020" color="#00000020"
_hover={{ color: 'red' }} _hover={{ color: 'red' }}
onClick={() => onRemove && onRemove(index)} onClick={() => onRemove && onRemove(index)}
/> >
<MdHighlightOff />
</CenterIcon>
</Box> </Box>
<Image loading="lazy" src={data} boxSize="full" /> <Image loading="lazy" src={data} boxSize="full" />
</Box> </Box>
</WrapItem> </Flex>
))} ))}
<WrapItem key="data"> <Flex align="flex-start" key="data">
<DragNdrop <DragNdrop
height="125px" height="125px"
width="125px" width="125px"
onFilesSelected={onFilesSelected} onFilesSelected={onFilesSelected}
onUriSelected={onUriSelected} onUriSelected={onUriSelected}
/> />
</WrapItem> </Flex>
</Wrap> </HStack>
</FormGroup> </FormGroup>
); );
}; };

View File

@ -3,35 +3,21 @@ import { ReactNode } from 'react';
import { Flex, Text } from '@chakra-ui/react'; import { Flex, Text } from '@chakra-ui/react';
import { MdErrorOutline, MdHelpOutline, MdRefresh } from 'react-icons/md'; import { MdErrorOutline, MdHelpOutline, MdRefresh } from 'react-icons/md';
export type FormGroupProps = { import { Icon } from '../Icon';
error?: ReactNode; import { useFormidableContextElement } from '../formidable';
help?: ReactNode;
label?: ReactNode;
isModify?: boolean;
onRestore?: () => void;
isRequired?: boolean;
children: ReactNode;
};
export const FormGroup = ({ const DisplayLabel = ({
children,
error,
help,
label, label,
isModify = false, isRequired,
isRequired = false, }: {
onRestore, label?: ReactNode;
}: FormGroupProps) => ( isRequired: boolean;
<Flex }) => {
borderLeftWidth="3px" if (!label) {
borderLeftColor={error ? 'red' : isModify ? 'blue' : '#00000000'} return <></>;
paddingLeft="7px" }
paddingY="4px" return (
direction="column" <Text marginRight="auto" paddingY="5px" fontWeight="bold">
>
<Flex direction="row" width="full" gap="52px">
{!!label && (
<Text marginRight="auto" fontWeight="bold">
{label}{' '} {label}{' '}
{isRequired && ( {isRequired && (
<Text as="span" color="red.600"> <Text as="span" color="red.600">
@ -39,24 +25,151 @@ export const FormGroup = ({
</Text> </Text>
)} )}
</Text> </Text>
)} );
{!!onRestore && isModify && ( };
<MdRefresh size="15px" onClick={onRestore} cursor="pointer" />
)} const DisplayHelp = ({ help }: { help?: ReactNode }) => {
</Flex> if (!help) {
{children} return <></>;
{!!help && ( }
return (
<Flex direction="row"> <Flex direction="row">
<MdHelpOutline /> <MdHelpOutline />
<Text>{help}</Text> <Text alignContent="center">{help}</Text>
</Flex> </Flex>
)} );
};
{!!error && ( const DisplayError = ({ error }: { error?: ReactNode }) => {
<Flex direction="row"> if (!error) {
return <></>;
}
return (
<Flex direction="row" color="red.600">
<MdErrorOutline /> <MdErrorOutline />
<Text>{error}</Text> <Text alignContent="center">{error}</Text>
</Flex> </Flex>
);
};
export type FormGroupProps = {
children: ReactNode;
name: string;
error?: ReactNode;
help?: ReactNode;
label?: ReactNode;
isRequired?: boolean;
disableSingleLine?: boolean;
};
export const FormGroup = ({
children,
name,
help,
label,
isRequired = false,
disableSingleLine,
}: FormGroupProps) => {
const { form, error, isModify, onRestore } =
useFormidableContextElement(name);
const enableModifyNotification =
form.configuration.enableModifyNotification ?? false;
const enableReset = form.configuration.enableReset ?? false;
const singleLine = disableSingleLine
? false
: form.configuration.singleLineForm;
return (
<FormGroupShow
help={help}
label={label}
isRequired={isRequired}
error={error}
isModify={isModify}
enableModifyNotification={enableModifyNotification}
enableReset={enableReset}
singleLine={singleLine}
onRestore={onRestore}
>
{children}
</FormGroupShow>
);
};
export type FormGroupShowProps = {
children: ReactNode;
help?: ReactNode;
label?: ReactNode;
isRequired?: boolean;
error?: ReactNode;
isModify?: boolean;
enableModifyNotification?: boolean;
enableReset?: boolean;
singleLine?: boolean;
onRestore?: () => void;
};
export const FormGroupShow = ({
children,
help,
label,
isRequired = false,
error,
isModify = false,
enableModifyNotification = true,
enableReset = true,
singleLine = false,
onRestore,
}: FormGroupShowProps) => {
return (
<Flex
borderLeftWidth="3px"
borderLeftColor={
error
? 'red.600'
: enableModifyNotification && isModify
? 'blue.600'
: '#00000000'
}
paddingLeft="7px"
paddingY="4px"
width="full"
direction="column"
>
{singleLine && (
<>
<Flex direction="row" width="full" gap="52px">
<Flex width="10%">
<DisplayLabel label={label} isRequired={isRequired} />
{!!onRestore && isModify && enableReset && (
<Icon sizeIcon="150px">
<MdRefresh onClick={onRestore} cursor="pointer" />
</Icon>
)}
</Flex>
<Flex direction="column" width={'90%'} gap="5px">
{children}
<DisplayHelp help={help} />
<DisplayError error={error} />
</Flex>
</Flex>
</>
)}
{!singleLine && (
<>
<Flex direction="row" width="full" gap="52px">
<Flex width="full">
<DisplayLabel label={label} isRequired={isRequired} />
{!!onRestore && isModify && enableReset && (
<Icon sizeIcon="30px" onClick={onRestore} cursor="pointer">
<MdRefresh />
</Icon>
)}
</Flex>
</Flex>
{children}
<DisplayHelp help={help} />
<DisplayError error={error} />
</>
)} )}
</Flex> </Flex>
); );
};

View File

@ -2,35 +2,34 @@ import { RefObject } from 'react';
import { Input } from '@chakra-ui/react'; import { Input } from '@chakra-ui/react';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup, FormGroupProps } from '@/components/form/FormGroup';
import { UseFormidableReturn } from '@/components/form/Formidable';
import { useFormidableContextElement } from '../formidable';
export type FormInputProps = { export type FormInputProps = {
form: UseFormidableReturn; name: string;
variableName: string;
ref?: RefObject<any>; ref?: RefObject<any>;
label?: string; label?: string;
placeholder?: string; placeholder?: string;
isRequired?: boolean; isRequired?: boolean;
}; } & Omit<FormGroupProps, 'children'>;
export const FormInput = ({ export const FormInput = ({
form, name,
variableName,
ref, ref,
placeholder, placeholder,
...rest ...rest
}: FormInputProps) => { }: FormInputProps) => {
const { value, onChange } = useFormidableContextElement(name);
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
isModify={form.isModify[variableName]}
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<Input <Input
ref={ref} ref={ref}
value={form.values[variableName]} type="text"
onChange={(e) => form.setValues({ [variableName]: e.target.value })} name={name}
autoComplete={name}
value={value}
onChange={(e) => onChange(e.target.value)}
/> />
</FormGroup> </FormGroup>
); );

View File

@ -1,23 +1,19 @@
import { RefObject } from 'react'; import { RefObject } from 'react';
import { FormGroup } from '@/components/form/FormGroup';
import { useFormidableContextElement } from '../formidable';
import { import {
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField, NumberInputField,
NumberInputProps, NumberInputProps,
NumberInputStepper, NumberInputRoot,
} from '@chakra-ui/react'; } from '../ui/number-input';
import { FormGroup } from '@/components/form/FormGroup';
import { UseFormidableReturn } from '@/components/form/Formidable';
export type FormNumberProps = Pick< export type FormNumberProps = Pick<
NumberInputProps, NumberInputProps,
'step' | 'defaultValue' | 'min' | 'max' 'step' | 'defaultValue' | 'min' | 'max'
> & { > & {
form: UseFormidableReturn; name: string;
variableName: string;
ref?: RefObject<any>; ref?: RefObject<any>;
label?: string; label?: string;
placeholder?: string; placeholder?: string;
@ -25,8 +21,7 @@ export type FormNumberProps = Pick<
}; };
export const FormNumber = ({ export const FormNumber = ({
form, name,
variableName,
ref, ref,
placeholder, placeholder,
step, step,
@ -35,27 +30,21 @@ export const FormNumber = ({
defaultValue, defaultValue,
...rest ...rest
}: FormNumberProps) => { }: FormNumberProps) => {
const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
isModify={form.isModify[variableName]} <NumberInputRoot
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<NumberInput
ref={ref} ref={ref}
value={form.values[variableName]} value={value}
onChange={(_, value) => form.setValues({ [variableName]: value })} onValueChange={(e) => onChange(e.value)}
step={step} step={step}
defaultValue={defaultValue} defaultValue={defaultValue}
min={min} min={min}
max={max} max={max}
> >
<NumberInputField /> <NumberInputField />
<NumberInputStepper> </NumberInputRoot>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
</FormGroup> </FormGroup>
); );
}; };

View File

@ -0,0 +1,50 @@
import { RefObject, useState } from 'react';
import { chakra, Group, Input } from '@chakra-ui/react';
import { FormGroup, FormGroupProps } from '@/components/form/FormGroup';
import { Button } from '../ui/button';
import { LuEye, LuEyeOff } from 'react-icons/lu';
import { useFormidableContextElement } from '../formidable';
export type FormInputProps = {
name: string;
ref?: RefObject<any>;
label?: string;
placeholder?: string;
isRequired?: boolean;
} & Omit<FormGroupProps, 'children'>;
export const FormPassword = ({
name,
ref,
placeholder,
...rest
}: FormInputProps) => {
const {value, onChange} = useFormidableContextElement(name);
const [showPassword, setShowPassword] = useState<boolean>(false);
function toggleVisible(): void {
setShowPassword((value) => ! value)
}
return (
<FormGroup
name={name}
{...rest}
>
<chakra.div position="relative" width="full">
<Input
ref={ref}
type={showPassword? "text" : "password"}
name={name}
autoComplete={name}
value={value}
onChange={(e) => onChange(e.target.value)}
paddingRight="47px"
/>
<Button variant="ghost" onClick={toggleVisible} position="absolute" top="0" right="0" _hover={{bg:"#0000", shadow:"none", color:"black"}}>
{showPassword? <LuEye/> : <LuEyeOff/>}
</Button>
</chakra.div>
</FormGroup>
);
};

View File

@ -3,7 +3,9 @@ import { useState } from 'react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { FormSelect } from '@/components/form/FormSelect'; import { FormSelect } from '@/components/form/FormSelect';
import { useFormidable } from '@/components/form/Formidable'; import { useFormidable } from '@/components/formidable/FormidableConfig';
import { Formidable } from '../formidable';
export default { export default {
title: 'Components/FormSelect', title: 'Components/FormSelect',
@ -16,23 +18,24 @@ type BasicFormData = {
export const Default = () => { export const Default = () => {
const form = useFormidable<BasicFormData>({}); const form = useFormidable<BasicFormData>({});
return ( return (
<Formidable.From form={form}>
<FormSelect <FormSelect
label="Simple Title" label="Simple Title"
form={form} name="data"
variableName={'data'}
keyInputValue="id" keyInputValue="id"
options={[{ id: 111 }, { id: 222 }, { id: 333 }, { id: 123 }]} options={[{ id: 111 }, { id: 222 }, { id: 333 }, { id: 123 }]}
/> />
</Formidable.From>
); );
}; };
export const ChangeKeys = () => { export const ChangeKeys = () => {
const form = useFormidable<BasicFormData>({}); const form = useFormidable<BasicFormData>({});
return ( return (
<Formidable.From form={form}>
<FormSelect <FormSelect
label="Simple Title for (ChangeKeys)" label="Simple Title for (ChangeKeys)"
form={form} name="data"
variableName={'data'}
keyInputKey="key" keyInputKey="key"
keyInputValue="plop" keyInputValue="plop"
options={[ options={[
@ -41,21 +44,23 @@ export const ChangeKeys = () => {
{ key: 333, plop: 'third item' }, { key: 333, plop: 'third item' },
]} ]}
/> />
</Formidable.From>
); );
}; };
export const ChangeName = () => { export const ChangeName = () => {
const form = useFormidable<BasicFormData>({}); const form = useFormidable<BasicFormData>({});
return ( return (
<Formidable.From form={form}>
<FormSelect <FormSelect
label="Simple Title for (ChangeName)" label="Simple Title for (ChangeName)"
form={form} name="data"
variableName={'data'}
options={[ options={[
{ id: 111, name: 'first Item' }, { id: 111, name: 'first Item' },
{ id: 222, name: 'Second Item' }, { id: 222, name: 'Second Item' },
{ id: 333, name: 'third item' }, { id: 333, name: 'third item' },
]} ]}
/> />
</Formidable.From>
); );
}; };
export const AddableItem = () => { export const AddableItem = () => {
@ -66,10 +71,10 @@ export const AddableItem = () => {
{ id: 333, name: 'third item' }, { id: 333, name: 'third item' },
]); ]);
return ( return (
<Formidable.From form={form}>
<FormSelect <FormSelect
label="Simple Title for (ChangeName)" label="Simple Title for (ChangeName)"
form={form} name="data"
variableName={'data'}
addNewItem={(data: string) => { addNewItem={(data: string) => {
return new Promise((resolve, _rejects) => { return new Promise((resolve, _rejects) => {
let upperId = 0; let upperId = 0;
@ -87,6 +92,7 @@ export const AddableItem = () => {
}} }}
options={data} options={data}
/> />
</Formidable.From>
); );
}; };
@ -94,11 +100,11 @@ export const DarkBackground = {
render: () => { render: () => {
const form = useFormidable<BasicFormData>({}); const form = useFormidable<BasicFormData>({});
return ( return (
<Formidable.From form={form}>
<Box p="4" color="white" bg="gray.800"> <Box p="4" color="white" bg="gray.800">
<FormSelect <FormSelect
label="Simple Title for (DarkBackground)" label="Simple Title for (DarkBackground)"
form={form} name="data"
variableName={'data'}
options={[ options={[
{ id: 111, name: 'first Item' }, { id: 111, name: 'first Item' },
{ id: 222, name: 'Second Item' }, { id: 222, name: 'Second Item' },
@ -106,6 +112,7 @@ export const DarkBackground = {
]} ]}
/> />
</Box> </Box>
</Formidable.From>
); );
}, },

View File

@ -1,16 +1,13 @@
import { RefObject } from 'react'; import { RefObject } from 'react';
import { Text } from '@chakra-ui/react';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup } from '@/components/form/FormGroup';
import { UseFormidableReturn } from '@/components/form/Formidable';
import { SelectSingle } from '@/components/select/SelectSingle'; import { SelectSingle } from '@/components/select/SelectSingle';
import { useFormidableContextElement } from '../formidable';
export type FormSelectProps = { export type FormSelectProps = {
// Generic Form input
form: UseFormidableReturn;
// Form: Name of the variable // Form: Name of the variable
variableName: string; name: string;
// Forward object reference // Forward object reference
ref?: RefObject<any>; ref?: RefObject<any>;
// Form: Label of the input // Form: Label of the input
@ -32,8 +29,7 @@ export type FormSelectProps = {
}; };
export const FormSelect = ({ export const FormSelect = ({
form, name,
variableName,
ref, ref,
placeholder, placeholder,
options, options,
@ -43,23 +39,23 @@ export const FormSelect = ({
addNewItem, addNewItem,
...rest ...rest
}: FormSelectProps) => { }: FormSelectProps) => {
const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
// if set add capability to add the search item // if set add capability to add the search item
const onCreate = !addNewItem const onCreate = !addNewItem
? undefined ? undefined
: (data: string) => { : (data: string) => {
addNewItem(data).then((data: object) => form.setValues({ [variableName]: data[keyInputKey] })); addNewItem(data).then((data: object) =>
form.setValues({ [name]: data[keyInputKey] })
);
}; };
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
isModify={form.isModify[variableName]}
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<SelectSingle <SelectSingle
ref={ref} ref={ref}
value={form.values[variableName]} value={value}
options={options} options={options}
onChange={(value) => form.setValues({ [variableName]: value })} onChange={(value) => onChange(value)}
keyKey={keyInputKey} keyKey={keyInputKey}
keyValue={keyInputValue} keyValue={keyInputValue}
onCreate={onCreate} onCreate={onCreate}

View File

@ -3,7 +3,9 @@ import { useState } from 'react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { FormSelectMultiple } from '@/components/form/FormSelectMultiple'; import { FormSelectMultiple } from '@/components/form/FormSelectMultiple';
import { useFormidable } from '@/components/form/Formidable'; import { useFormidable } from '@/components/formidable/FormidableConfig';
import { Formidable } from '../formidable';
export default { export default {
title: 'Components/FormSelectMultipleMultiple', title: 'Components/FormSelectMultipleMultiple',
@ -16,23 +18,24 @@ type BasicFormData = {
export const Default = () => { export const Default = () => {
const form = useFormidable<BasicFormData>({}); const form = useFormidable<BasicFormData>({});
return ( return (
<Formidable.From form={form}>
<FormSelectMultiple <FormSelectMultiple
label="Simple Title" label="Simple Title"
form={form} name={'data'}
variableName={'data'}
keyInputValue="id" keyInputValue="id"
options={[{ id: 111 }, { id: 222 }, { id: 333 }, { id: 123 }]} options={[{ id: 111 }, { id: 222 }, { id: 333 }, { id: 123 }]}
/> />
</Formidable.From>
); );
}; };
export const ChangeKeys = () => { export const ChangeKeys = () => {
const form = useFormidable<BasicFormData>({}); const form = useFormidable<BasicFormData>({});
return ( return (
<Formidable.From form={form}>
<FormSelectMultiple <FormSelectMultiple
label="Simple Title for (ChangeKeys)" label="Simple Title for (ChangeKeys)"
form={form} name="data"
variableName={'data'}
keyInputKey="key" keyInputKey="key"
keyInputValue="plop" keyInputValue="plop"
options={[ options={[
@ -41,21 +44,23 @@ export const ChangeKeys = () => {
{ key: 333, plop: 'third item' }, { key: 333, plop: 'third item' },
]} ]}
/> />
</Formidable.From>
); );
}; };
export const ChangeName = () => { export const ChangeName = () => {
const form = useFormidable<BasicFormData>({}); const form = useFormidable<BasicFormData>({});
return ( return (
<Formidable.From form={form}>
<FormSelectMultiple <FormSelectMultiple
label="Simple Title for (ChangeName)" label="Simple Title for (ChangeName)"
form={form} name="data"
variableName={'data'}
options={[ options={[
{ id: 111, name: 'first Item' }, { id: 111, name: 'first Item' },
{ id: 222, name: 'Second Item' }, { id: 222, name: 'Second Item' },
{ id: 333, name: 'third item' }, { id: 333, name: 'third item' },
]} ]}
/> />
</Formidable.From>
); );
}; };
export const AddableItem = () => { export const AddableItem = () => {
@ -66,10 +71,10 @@ export const AddableItem = () => {
{ id: 333, name: 'third item' }, { id: 333, name: 'third item' },
]); ]);
return ( return (
<Formidable.From form={form}>
<FormSelectMultiple <FormSelectMultiple
label="Simple Title for (ChangeName)" label="Simple Title for (ChangeName)"
form={form} name="data"
variableName={'data'}
addNewItem={(data: string) => { addNewItem={(data: string) => {
return new Promise((resolve, _rejects) => { return new Promise((resolve, _rejects) => {
let upperId = 0; let upperId = 0;
@ -87,6 +92,7 @@ export const AddableItem = () => {
}} }}
options={data} options={data}
/> />
</Formidable.From>
); );
}; };
@ -94,11 +100,11 @@ export const DarkBackground = {
render: () => { render: () => {
const form = useFormidable<BasicFormData>({}); const form = useFormidable<BasicFormData>({});
return ( return (
<Formidable.From form={form}>
<Box p="4" color="white" bg="gray.800"> <Box p="4" color="white" bg="gray.800">
<FormSelectMultiple <FormSelectMultiple
label="Simple Title for (DarkBackground)" label="Simple Title for (DarkBackground)"
form={form} name="data"
variableName={'data'}
options={[ options={[
{ id: 111, name: 'first Item' }, { id: 111, name: 'first Item' },
{ id: 222, name: 'Second Item' }, { id: 222, name: 'Second Item' },
@ -106,6 +112,7 @@ export const DarkBackground = {
]} ]}
/> />
</Box> </Box>
</Formidable.From>
); );
}, },

View File

@ -1,14 +1,16 @@
import { RefObject } from 'react'; import { RefObject } from 'react';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup } from '@/components/form/FormGroup';
import { UseFormidableReturn } from '@/components/form/Formidable';
import { SelectMultiple } from '@/components/select/SelectMultiple'; import { SelectMultiple } from '@/components/select/SelectMultiple';
import {
useFormidableContext,
useFormidableContextElement,
} from '../formidable';
export type FormSelectMultipleProps = { export type FormSelectMultipleProps = {
// Generic Form input
form: UseFormidableReturn;
// Form: Name of the variable // Form: Name of the variable
variableName: string; name: string;
// Forward object reference // Forward object reference
ref?: RefObject<any>; ref?: RefObject<any>;
// Form: Label of the input // Form: Label of the input
@ -28,8 +30,7 @@ export type FormSelectMultipleProps = {
}; };
export const FormSelectMultiple = ({ export const FormSelectMultiple = ({
form, name,
variableName,
ref, ref,
placeholder, placeholder,
options, options,
@ -38,23 +39,25 @@ export const FormSelectMultiple = ({
addNewItem, addNewItem,
...rest ...rest
}: FormSelectMultipleProps) => { }: FormSelectMultipleProps) => {
const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
// if set add capability to add the search item // if set add capability to add the search item
const onCreate = !addNewItem const onCreate = !addNewItem
? undefined ? undefined
: (data: string) => { : (data: string) => {
addNewItem(data).then((data: object) => form.setValues({ [variableName]: [...(form.values[variableName] ?? []), data[keyInputKey]] })); addNewItem(data).then((data: object) =>
form.setValues({
[name]: [...(form.values[name] ?? []), data[keyInputKey]],
})
);
}; };
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
isModify={form.isModify[variableName]}
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<SelectMultiple <SelectMultiple
//ref={ref} //ref={ref}
values={form.values[variableName]} values={value}
options={options} options={options}
onChange={(value) => form.setValues({ [variableName]: value })} onChange={(value) => onChange(value)}
keyKey={keyInputKey} keyKey={keyInputKey}
keyValue={keyInputValue} keyValue={keyInputValue}
onCreate={onCreate} onCreate={onCreate}

View File

@ -3,11 +3,11 @@ import { RefObject } from 'react';
import { Textarea } from '@chakra-ui/react'; import { Textarea } from '@chakra-ui/react';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroup } from '@/components/form/FormGroup';
import { UseFormidableReturn } from '@/components/form/Formidable';
import { useFormidableContextElement } from '../formidable';
export type FormTextareaProps = { export type FormTextareaProps = {
form: UseFormidableReturn; name: string;
variableName: string;
ref?: RefObject<any>; ref?: RefObject<any>;
label?: string; label?: string;
placeholder?: string; placeholder?: string;
@ -15,22 +15,20 @@ export type FormTextareaProps = {
}; };
export const FormTextarea = ({ export const FormTextarea = ({
form, name,
variableName,
ref, ref,
placeholder, placeholder,
...rest ...rest
}: FormTextareaProps) => { }: FormTextareaProps) => {
const { value, onChange } = useFormidableContextElement(name);
return ( return (
<FormGroup <FormGroup name={name} {...rest}>
isModify={form.isModify[variableName]}
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<Textarea <Textarea
name={name}
ref={ref} ref={ref}
value={form.values[variableName]} autoComplete={name}
onChange={(e) => form.setValues({ [variableName]: e.target.value })} value={value}
onChange={(e) => onChange(e.target.value)}
/> />
</FormGroup> </FormGroup>
); );

View File

@ -1,66 +1,44 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { isArray, isNullOrUndefined, isObject } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => { import { getDifferences, hasAnyTrue } from './utils';
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] === true) { export type FormidableDeltaConfig<TYPE> = {
return true; omit?: string[]; // (keyof TYPE)[];
} only?: string[]; // (keyof TYPE)[];
}
return false;
}; };
function getDifferences( export type FormidableConfig = {
obj1: object, enableReset?: boolean;
obj2: object enableModifyNotification?: boolean;
): { [key: string]: boolean } { singleLineForm?: boolean;
// Create an empty object to store the differences };
const result: { [key: string]: boolean } = {}; const initialFormConfig: Required<FormidableConfig> = {
// Recursive function to compare values enableReset: true,
function compareValues(value1: any, value2: any): boolean { enableModifyNotification: true,
// If both values are objects, compare their properties recursively singleLineForm: false,
if (isObject(value1) && isObject(value2)) { };
return hasAnyTrue(getDifferences(value1, value2));
}
// If both values are arrays, compare their elements
if (isArray(value1) && isArray(value2)) {
//console.log(`Check is array: ${JSON.stringify(value1)} =?= ${JSON.stringify(value2)}`);
if (value1.length !== value2.length) {
return true;
}
for (let i = 0; i < value1.length; i++) {
if (compareValues(value1[i], value2[i])) {
return true;
}
}
return false;
}
// Otherwise, compare the values directly
//console.log(`compare : ${value1} =?= ${value2}`);
return value1 !== value2;
}
// Get all keys from both objects
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
// Iterate over all keys
for (const key of allKeys) {
if (compareValues(obj1[key], obj2[key])) {
result[key] = true;
} else {
result[key] = false;
}
}
return result;
}
export const useFormidable = <TYPE extends object = object>({ export const useFormidable = <TYPE extends object = object>({
initialValues = {} as TYPE, initialValues = {} as TYPE,
configuration: inputConfiguration = initialFormConfig,
deltaConfig,
resolver = (_data: TYPE) => {
return {};
},
}: { }: {
initialValues?: TYPE; initialValues?: TYPE;
configuration?: FormidableConfig;
deltaConfig?: FormidableDeltaConfig<TYPE>;
resolver?: (data: any) => Record<string, string>;
}) => { }) => {
const configuration: Required<FormidableConfig> = {
...initialFormConfig,
...inputConfiguration,
};
const [values, setValues] = useState<TYPE>({ ...initialValues } as TYPE); const [values, setValues] = useState<TYPE>({ ...initialValues } as TYPE);
const [errors, setErrors] = useState<object>({});
const [initialData, setInitialData] = useState<TYPE>(initialValues); const [initialData, setInitialData] = useState<TYPE>(initialValues);
const [isModify, setIsModify] = useState<{ [key: string]: boolean }>({}); const [isModify, setIsModify] = useState<{ [key: string]: boolean }>({});
const [isFormModified, setIsFormModified] = useState<boolean>(false); const [isFormModified, setIsFormModified] = useState<boolean>(false);
@ -97,10 +75,11 @@ export const useFormidable = <TYPE extends object = object>({
const ret = getDifferences(initialData, newValues); const ret = getDifferences(initialData, newValues);
setIsModify(ret); setIsModify(ret);
setIsFormModified(hasAnyTrue(ret)); setIsFormModified(hasAnyTrue(ret));
setErrors(resolver(newValues));
return newValues; return newValues;
}); });
}, },
[setValues, initialData] [setValues, initialData, setErrors, setIsFormModified, setIsModify]
); );
const restoreValue = useCallback( const restoreValue = useCallback(
(data: object) => { (data: object) => {
@ -133,7 +112,7 @@ export const useFormidable = <TYPE extends object = object>({
[setValues, initialData, setIsFormModified, setIsModify] [setValues, initialData, setIsFormModified, setIsModify]
); );
const getDeltaData = useCallback( const getDeltaData = useCallback(
({ omit = [], only }: { omit?: string[]; only?: string[] }) => { ({ omit = [], only }: FormidableDeltaConfig<TYPE> = {}) => {
const out = {}; const out = {};
Object.keys(isModify).forEach((key) => { Object.keys(isModify).forEach((key) => {
if (omit.includes(key) || (only && !only.includes(key))) { if (omit.includes(key) || (only && !only.includes(key))) {
@ -161,6 +140,9 @@ export const useFormidable = <TYPE extends object = object>({
restoreValue, restoreValue,
setValues: setValuesExternal, setValues: setValuesExternal,
values, values,
errors,
configuration,
deltaConfig,
}; };
}; };

View File

@ -0,0 +1,92 @@
import {
ReactNode,
createContext,
useCallback,
useContext,
useMemo,
} from 'react';
import { UseFormidableReturn } from './FormidableConfig';
export type FromContextProps = {
form: UseFormidableReturn;
};
export const formContext = createContext<FromContextProps>({
form: {
getDeltaData: ({}: { omit?: string[]; only?: string[] }) => {
return {};
},
isFormModified: false,
isModify: {},
restoreValues: () => {},
restoreValue: (_data: object) => {},
setValues: (_data: object) => {},
values: {},
errors: {},
configuration: {
enableReset: false,
enableModifyNotification: false,
singleLineForm: false,
},
deltaConfig: {},
},
});
export const useFormidableContext = () => {
const context = useContext(formContext);
if (!context) {
throw new Error('useFormContext must be used within a FormProvider');
}
if (!context.form) {
throw new Error('useFormContext without defining a From');
}
return context;
};
export const useFormidableContextElement = (name: string) => {
const { form } = useFormidableContext();
if (name === undefined) {
console.error(
"Can not request useFormidableContextElement with empty 'name'"
);
}
const onChange = useCallback(
(value) => {
console.log(`new values: ${name}=>${value}`);
form.setValues({ [name]: value });
},
[name, form, form.setValues]
);
const onRestore = useCallback(() => {
console.log('Restore value : ' + name);
form.restoreValue({ [name]: true });
}, [name, form, form.restoreValue]);
return {
form,
value: form.values[name] || '',
error: form.errors[name],
isModify: form.isModify[name],
onChange,
onRestore,
};
};
export type FormidableContextProps = {
form: UseFormidableReturn;
children: ReactNode;
};
export const FormidableContext = ({
form,
children,
}: FormidableContextProps) => {
const memoContext = useMemo(
() => ({
form,
}),
[form]
);
return (
<formContext.Provider value={memoContext}>{children}</formContext.Provider>
);
};

View File

@ -0,0 +1,43 @@
import { ReactNode } from 'react';
import { Box } from '@chakra-ui/react';
import { useFormidable } from './FormidableConfig';
import { FormidableContext } from './FormidableContext';
export interface FormidableFormProps<TYPE extends object = object> {
form: ReturnType<typeof useFormidable<TYPE>>;
children: ReactNode;
onSubmit?: (data: TYPE) => void;
onSubmitDelta?: (data: Partial<TYPE>) => void;
}
export const FormidableForm = <TYPE extends object = object>({
onSubmit,
onSubmitDelta,
form,
children,
}: FormidableFormProps<TYPE>) => {
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
const hasErrors = false; //Object.values(errors).some((err) => err);
if (!hasErrors) {
console.log(
`request From submit !!! ${JSON.stringify(form.values, null, 2)}`
);
if (onSubmit) {
onSubmit(form.values);
}
if (onSubmitDelta) {
onSubmitDelta(form.getDeltaData(form.deltaConfig));
}
}
};
return (
<FormidableContext form={form}>
<Box as="form" onSubmit={handleSubmit}>
{children}
</Box>
</FormidableContext>
);
};

View File

@ -0,0 +1,8 @@
export {
type FormidableConfig as config,
useFormidable,
} from './FormidableConfig';
export {
FormidableForm as From,
type FormidableFormProps as FormProps,
} from './FormidableForm';

View File

@ -0,0 +1,7 @@
export * as Formidable from './Fromidable';
export {
useFormidableContext,
useFormidableContextElement,
} from './FormidableContext';
export { useFormidable } from './FormidableConfig';
export { zodResolver } from './utils';

View File

@ -0,0 +1,87 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ZodError } from 'zod';
import { isArray, isNullOrUndefined, isObject } from '@/utils/validator';
export const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => {
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] === true) {
return true;
}
}
return false;
};
export function getDifferences(
obj1: object,
obj2: object
): { [key: string]: boolean } {
// Create an empty object to store the differences
const result: { [key: string]: boolean } = {};
// Recursive function to compare values
function compareValues(value1: any, value2: any): boolean {
// If both values are objects, compare their properties recursively
if (isObject(value1) && isObject(value2)) {
return hasAnyTrue(getDifferences(value1, value2));
}
// If both values are arrays, compare their elements
if (isArray(value1) && isArray(value2)) {
//console.log(`Check is array: ${JSON.stringify(value1)} =?= ${JSON.stringify(value2)}`);
if (value1.length !== value2.length) {
return true;
}
for (let i = 0; i < value1.length; i++) {
if (compareValues(value1[i], value2[i])) {
return true;
}
}
return false;
}
// Otherwise, compare the values directly
//console.log(`compare : ${value1} =?= ${value2}`);
return value1 !== value2;
}
// Get all keys from both objects
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
// Iterate over all keys
for (const key of allKeys) {
if (compareValues(obj1[key], obj2[key])) {
result[key] = true;
} else {
result[key] = false;
}
}
return result;
}
export const zodResolver = (zodModel) => {
return (data: any) => {
try {
console.log(`check resolver of: ${JSON.stringify(data, null, 2)}`);
zodModel.parse(data);
return {};
} catch (error) {
if (error instanceof ZodError) {
console.log(
`catch error with resolver: ${JSON.stringify(error, null, 2)}`
);
const formattedErrors = error.issues.reduce(
(acc, issue) => {
if (issue.path.length > 0) {
acc[issue.path[0]] = issue.message;
}
return acc;
},
{} as Record<string, string>
);
console.log(`get errors: ${JSON.stringify(formattedErrors, null, 2)}`);
return formattedErrors;
}
// prevent zod error
throw error;
}
};
};

View File

@ -23,7 +23,7 @@ export const DisplayGender = ({ dataGender }: DisplayGenderProps) => {
data={dataGender?.covers} data={dataGender?.covers}
size="100" size="100"
height="full" height="full"
iconEmpty={LuDisc3} iconEmpty={<LuDisc3 />}
/> />
<Flex <Flex
direction="column" direction="column"
@ -35,24 +35,24 @@ export const DisplayGender = ({ dataGender }: DisplayGenderProps) => {
> >
<Text <Text
as="span" as="span"
align="left" alignContent="left"
fontSize="20px" fontSize="20px"
fontWeight="bold" fontWeight="bold"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={[1, 2]} //TODO: noOfLines={[1, 2]}
> >
{dataGender?.name} {dataGender?.name}
</Text> </Text>
<Text <Text
as="span" as="span"
align="left" alignContent="left"
fontSize="15px" fontSize="15px"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={1} //TODO: noOfLines={1}
> >
{countTracksOnAGender} track{countTracksOnAGender >= 1 && 's'} {countTracksOnAGender} track{countTracksOnAGender >= 1 && 's'}
</Text> </Text>

View File

@ -1,18 +1,6 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { import { Flex, Text, useDisclosure } from '@chakra-ui/react';
Button,
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { import {
MdAdminPanelSettings, MdAdminPanelSettings,
MdDeleteForever, MdDeleteForever,
@ -21,18 +9,27 @@ import {
} from 'react-icons/md'; } from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Album, AlbumResource } from '@/back-api'; import { AlbumResource, AlbumWrite } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers'; import { FormCovers } from '@/components/form/FormCovers';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroupShow } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput'; import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea'; import { FormTextarea } from '@/components/form/FormTextarea';
import { useFormidable } from '@/components/form/Formidable';
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp'; import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
import {
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
} from '@/components/ui/dialog';
import { useAlbumService, useSpecificAlbum } from '@/service/Album'; import { useAlbumService, useSpecificAlbum } from '@/service/Album';
import { useServiceContext } from '@/service/ServiceContext'; import { useServiceContext } from '@/service/ServiceContext';
import { useCountTracksWithAlbumId } from '@/service/Track'; import { useCountTracksWithAlbumId } from '@/service/Track';
import { isNullOrUndefined } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
import { Formidable, useFormidable } from '../formidable';
import { Button } from '../ui/button';
export type AlbumEditPopUpProps = {}; export type AlbumEditPopUpProps = {};
export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => { export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => {
@ -65,21 +62,21 @@ export const AlbumEditPopUp = ({ }: AlbumEditPopUpProps) => {
); );
onClose(); onClose();
}; };
const initialRef = useRef(null); const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef(null); const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<Album>({ const form = useFormidable<AlbumWrite>({
initialValues: dataAlbum, initialValues: dataAlbum,
deltaConfig: { omit: ['covers'] },
}); });
const onSave = async () => { const onSave = async (deltaData: AlbumWrite) => {
if (isNullOrUndefined(albumIdInt)) { if (isNullOrUndefined(albumIdInt)) {
return; return;
} }
const dataThatNeedToBeUpdated = form.getDeltaData({ omit: ['covers'] }); console.log(`onSave = ${JSON.stringify(deltaData, null, 2)}`);
console.log(`onSave = ${JSON.stringify(dataThatNeedToBeUpdated, null, 2)}`);
store.update( store.update(
AlbumResource.patch({ AlbumResource.patch({
restConfig: session.getRestConfig(), restConfig: session.getRestConfig(),
data: dataThatNeedToBeUpdated, data: deltaData,
params: { params: {
id: albumIdInt, id: albumIdInt,
}, },
@ -141,43 +138,44 @@ export const AlbumEditPopUp = ({ }: AlbumEditPopUpProps) => {
); );
}; };
return ( return (
<Modal <DialogRoot
initialFocusRef={initialRef} //initialFocusRef={initialRef}
finalFocusRef={finalRef} //finalFocusRef={finalRef}
closeOnOverlayClick={false} //closeOnOverlayClick={false}
onClose={onClose} //onOpenChange={onClose}
isOpen={true} open={true}
data-testid="album-edit-pop-up"
> >
<ModalOverlay /> <DialogContent>
<ModalContent> <Formidable.From form={form} onSubmitDelta={onSave}>
<ModalHeader>Edit Album</ModalHeader> <DialogHeader>Edit Album</DialogHeader>
<ModalCloseButton ref={finalRef} /> {/* <DialogCloseButton ref={finalRef} /> */}
<ModalBody pb={6} gap="0px" paddingLeft="18px"> <DialogBody pb={6} gap="0px" paddingLeft="18px">
{admin && ( {admin && (
<> <>
<FormGroup isRequired label="Id"> <FormGroupShow isRequired label="Id">
<Text>{dataAlbum?.id}</Text> <Text>{dataAlbum?.id}</Text>
</FormGroup> </FormGroupShow>
{countTracksOfAnAlbum !== 0 && ( {countTracksOfAnAlbum !== 0 && (
<Flex paddingLeft="14px"> <Flex paddingLeft="14px">
<MdWarning color="red.600" /> <MdWarning color="red.600" />
<Text paddingLeft="6px" color="red.600" fontWeight="bold"> <Text paddingLeft="6px" color="red.600" fontWeight="bold">
Can not remove album {countTracksOfAnAlbum} track(s) depend Can not remove album {countTracksOfAnAlbum} track(s)
on it. depend on it.
</Text> </Text>
</Flex> </Flex>
)} )}
<FormGroup label="Action(s):"> <FormGroupShow label="Action(s):">
<Button <Button
onClick={disclosure.onOpen} onClick={disclosure.onOpen}
marginRight="auto" marginRight="auto"
variant="@danger" colorPalette="@danger"
isDisabled={countTracksOfAnAlbum !== 0} disabled={countTracksOfAnAlbum !== 0}
> >
<MdDeleteForever /> Remove Media <MdDeleteForever /> Remove Media
</Button> </Button>
</FormGroup> </FormGroupShow>
<ConfirmPopUp <ConfirmPopUp
disclosure={disclosure} disclosure={disclosure}
title="Remove album" title="Remove album"
@ -190,33 +188,23 @@ export const AlbumEditPopUp = ({ }: AlbumEditPopUpProps) => {
{!admin && ( {!admin && (
<> <>
<FormInput <FormInput
form={form} name="name"
variableName="name"
isRequired isRequired
label="Title" label="Title"
ref={initialRef} ref={initialRef}
/> />
<FormTextarea <FormTextarea name="description" label="Description" />
form={form} <FormInput name="publication" label="Publication" />
variableName="description"
label="Description"
/>
<FormInput
form={form}
variableName="publication"
label="Publication"
/>
<FormCovers <FormCovers
form={form} name="covers"
variableName="covers"
onFilesSelected={onFilesSelected} onFilesSelected={onFilesSelected}
onUriSelected={onUriSelected} onUriSelected={onUriSelected}
onRemove={onRemoveCover} onRemove={onRemoveCover}
/> />
</> </>
)} )}
</ModalBody> </DialogBody>
<ModalFooter> <DialogFooter>
<Button <Button
onClick={() => setAdmin((value) => !value)} onClick={() => setAdmin((value) => !value)}
marginRight="auto" marginRight="auto"
@ -234,13 +222,14 @@ export const AlbumEditPopUp = ({ }: AlbumEditPopUpProps) => {
)} )}
</Button> </Button>
{!admin && form.isFormModified && ( {!admin && form.isFormModified && (
<Button colorScheme="blue" mr={3} onClick={onSave}> <Button colorScheme="blue" mr={3} type="submit">
Save Save
</Button> </Button>
)} )}
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
</ModalFooter> </DialogFooter>
</ModalContent> </Formidable.From>
</Modal> </DialogContent>
</DialogRoot>
); );
}; };

View File

@ -1,18 +1,6 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { import { Button, Flex, Text, useDisclosure } from '@chakra-ui/react';
Button,
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { import {
MdAdminPanelSettings, MdAdminPanelSettings,
MdDeleteForever, MdDeleteForever,
@ -21,18 +9,26 @@ import {
} from 'react-icons/md'; } from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Artist, ArtistResource } from '@/back-api'; import { ArtistResource, ArtistWrite } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers'; import { FormCovers } from '@/components/form/FormCovers';
import { FormGroup } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput'; import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea'; import { FormTextarea } from '@/components/form/FormTextarea';
import { useFormidable } from '@/components/form/Formidable';
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp'; import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
import {
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
} from '@/components/ui/dialog';
import { useArtistService, useSpecificArtist } from '@/service/Artist'; import { useArtistService, useSpecificArtist } from '@/service/Artist';
import { useServiceContext } from '@/service/ServiceContext'; import { useServiceContext } from '@/service/ServiceContext';
import { useCountTracksOfAnArtist } from '@/service/Track'; import { useCountTracksOfAnArtist } from '@/service/Track';
import { isNullOrUndefined } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
import { FormGroupShow } from '../form/FormGroup';
import { Formidable, useFormidable } from '../formidable';
export type ArtistEditPopUpProps = {}; export type ArtistEditPopUpProps = {};
export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => { export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => {
@ -65,21 +61,21 @@ export const ArtistEditPopUp = ({ }: ArtistEditPopUpProps) => {
); );
onClose(); onClose();
}; };
const initialRef = useRef(null); const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef(null); const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<Artist>({ const form = useFormidable<ArtistWrite>({
initialValues: dataArtist, initialValues: dataArtist,
deltaConfig: { omit: ['covers'] },
}); });
const onSave = async () => { const onSave = async (dataDelta: ArtistWrite) => {
if (isNullOrUndefined(artistIdInt)) { if (isNullOrUndefined(artistIdInt)) {
return; return;
} }
const dataThatNeedToBeUpdated = form.getDeltaData({ omit: ['covers'] }); console.log(`onSave = ${JSON.stringify(dataDelta, null, 2)}`);
console.log(`onSave = ${JSON.stringify(dataThatNeedToBeUpdated, null, 2)}`);
store.update( store.update(
ArtistResource.patch({ ArtistResource.patch({
restConfig: session.getRestConfig(), restConfig: session.getRestConfig(),
data: dataThatNeedToBeUpdated, data: dataDelta,
params: { params: {
id: artistIdInt, id: artistIdInt,
}, },
@ -140,24 +136,26 @@ export const ArtistEditPopUp = ({ }: ArtistEditPopUpProps) => {
); );
}; };
return ( return (
<Modal <DialogRoot
initialFocusRef={initialRef} //initialFocusRef={initialRef}
finalFocusRef={finalRef} //finalFocusRef={finalRef}
closeOnOverlayClick={false} //closeOnOverlayClick={false}
onClose={onClose} onOpenChange={onClose}
isOpen={true} open={true}
data-testid="artist-edit-pop-up"
> >
<ModalOverlay /> {/* <DialogOverlay /> */}
<ModalContent> <DialogContent>
<ModalHeader>Edit Artist</ModalHeader> <Formidable.From form={form} onSubmitDelta={onSave}>
<ModalCloseButton ref={finalRef} /> <DialogHeader>Edit Artist</DialogHeader>
{/* <DialogCloseButton ref={finalRef} /> */}
<ModalBody pb={6} gap="0px" paddingLeft="18px"> <DialogBody pb={6} gap="0px" paddingLeft="18px">
{admin && ( {admin && (
<> <>
<FormGroup isRequired label="Id"> <FormGroupShow isRequired label="Id">
<Text>{dataArtist?.id}</Text> <Text>{dataArtist?.id}</Text>
</FormGroup> </FormGroupShow>
{countTracksOnAnArtist !== 0 && ( {countTracksOnAnArtist !== 0 && (
<Flex paddingLeft="14px"> <Flex paddingLeft="14px">
<MdWarning color="red.600" /> <MdWarning color="red.600" />
@ -167,16 +165,16 @@ export const ArtistEditPopUp = ({ }: ArtistEditPopUpProps) => {
</Text> </Text>
</Flex> </Flex>
)} )}
<FormGroup label="Action(s):"> <FormGroupShow label="Action(s):">
<Button <Button
onClick={disclosure.onOpen} onClick={disclosure.onOpen}
marginRight="auto" marginRight="auto"
variant="@danger" colorPalette="@danger"
isDisabled={countTracksOnAnArtist !== 0} disabled={countTracksOnAnArtist !== 0}
> >
<MdDeleteForever /> Remove Media <MdDeleteForever /> Remove Media
</Button> </Button>
</FormGroup> </FormGroupShow>
<ConfirmPopUp <ConfirmPopUp
disclosure={disclosure} disclosure={disclosure}
title="Remove artist" title="Remove artist"
@ -189,39 +187,30 @@ export const ArtistEditPopUp = ({ }: ArtistEditPopUpProps) => {
{!admin && ( {!admin && (
<> <>
<FormInput <FormInput
form={form} name="name"
variableName="name"
isRequired isRequired
label="Artist name" label="Artist name"
ref={initialRef} ref={initialRef}
/> />
<FormTextarea <FormTextarea name="description" label="Description" />
form={form} <FormInput name="firstName" label="First Name" />
variableName="description" <FormInput name="surname" label="SurName" />
label="Description" <FormInput name="birth" label="Birth date" />
/> <FormInput name="death" label="Death date" />
<FormInput
form={form}
variableName="firstName"
label="First Name"
/>
<FormInput form={form} variableName="surname" label="SurName" />
<FormInput form={form} variableName="birth" label="Birth date" />
<FormInput form={form} variableName="death" label="Death date" />
<FormCovers <FormCovers
form={form} name="covers"
variableName="covers"
onFilesSelected={onFilesSelected} onFilesSelected={onFilesSelected}
onUriSelected={onUriSelected} onUriSelected={onUriSelected}
onRemove={onRemoveCover} onRemove={onRemoveCover}
/> />
</> </>
)} )}
</ModalBody> </DialogBody>
<ModalFooter> <DialogFooter>
<Button <Button
onClick={() => setAdmin((value) => !value)} onClick={() => setAdmin((value) => !value)}
marginRight="auto" marginRight="auto"
colorPalette={admin ? undefined : '@danger'}
> >
{admin ? ( {admin ? (
<> <>
@ -236,13 +225,14 @@ export const ArtistEditPopUp = ({ }: ArtistEditPopUpProps) => {
)} )}
</Button> </Button>
{!admin && form.isFormModified && ( {!admin && form.isFormModified && (
<Button colorScheme="blue" mr={3} onClick={onSave}> <Button colorScheme="blue" mr={3} type="submit">
Save Save
</Button> </Button>
)} )}
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
</ModalFooter> </DialogFooter>
</ModalContent> </Formidable.From>
</Modal> </DialogContent>
</DialogRoot>
); );
}; };

View File

@ -1,15 +1,14 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { Button, UseDisclosureReturn } from '@chakra-ui/react';
import { import {
AlertDialog, DialogBody,
AlertDialogBody, DialogContent,
AlertDialogContent, DialogFooter,
AlertDialogFooter, DialogHeader,
AlertDialogHeader, DialogRoot,
AlertDialogOverlay, } from '@/components/ui/dialog';
Button,
UseDisclosureReturn,
} from '@chakra-ui/react';
export type ConfirmPopUpProps = { export type ConfirmPopUpProps = {
title: string; title: string;
@ -30,31 +29,31 @@ export const ConfirmPopUp = ({
onConfirm(); onConfirm();
disclosure.onClose(); disclosure.onClose();
}; };
const cancelRef = useRef(null); const cancelRef = useRef<HTMLButtonElement>(null);
return ( return (
<AlertDialog <DialogRoot
isOpen={disclosure.isOpen} role="alertdialog"
leastDestructiveRef={cancelRef} open={disclosure.open}
onClose={disclosure.onClose} //leastDestructiveRef={cancelRef}
onOpenChange={disclosure.onClose}
data-testid="confirm-pop-up"
> >
<AlertDialogOverlay> <DialogContent>
<AlertDialogContent> <DialogHeader fontSize="lg" fontWeight="bold">
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title} {title}
</AlertDialogHeader> </DialogHeader>
<AlertDialogBody>{body}</AlertDialogBody> <DialogBody>{body}</DialogBody>
<AlertDialogFooter> <DialogFooter>
<Button onClick={disclosure.onClose} ref={cancelRef}> <Button onClick={disclosure.onClose} ref={cancelRef}>
Cancel Cancel
</Button> </Button>
<Button colorScheme="red" onClick={onClickConfirm} ml={3}> <Button colorScheme="red" onClick={onClickConfirm} ml={3}>
{confirmTitle} {confirmTitle}
</Button> </Button>
</AlertDialogFooter> </DialogFooter>
</AlertDialogContent> </DialogContent>
</AlertDialogOverlay> </DialogRoot>
</AlertDialog>
); );
}; };

View File

@ -1,18 +1,6 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { import { Button, Flex, Text, useDisclosure } from '@chakra-ui/react';
Button,
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { import {
MdAdminPanelSettings, MdAdminPanelSettings,
MdDeleteForever, MdDeleteForever,
@ -21,18 +9,26 @@ import {
} from 'react-icons/md'; } from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Gender, GenderResource } from '@/back-api'; import { GenderResource, GenderWrite } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers'; import { FormCovers } from '@/components/form/FormCovers';
import { FormGroup } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput'; import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea'; import { FormTextarea } from '@/components/form/FormTextarea';
import { useFormidable } from '@/components/form/Formidable';
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp'; import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
import {
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
} from '@/components/ui/dialog';
import { useGenderService, useSpecificGender } from '@/service/Gender'; import { useGenderService, useSpecificGender } from '@/service/Gender';
import { useServiceContext } from '@/service/ServiceContext'; import { useServiceContext } from '@/service/ServiceContext';
import { useCountTracksOfAGender } from '@/service/Track'; import { useCountTracksOfAGender } from '@/service/Track';
import { isNullOrUndefined } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
import { FormGroupShow } from '../form/FormGroup';
import { Formidable, useFormidable } from '../formidable';
export type GenderEditPopUpProps = {}; export type GenderEditPopUpProps = {};
export const GenderEditPopUp = ({}: GenderEditPopUpProps) => { export const GenderEditPopUp = ({}: GenderEditPopUpProps) => {
@ -65,21 +61,21 @@ export const GenderEditPopUp = ({ }: GenderEditPopUpProps) => {
); );
onClose(); onClose();
}; };
const initialRef = useRef(null); const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef(null); const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<Gender>({ const form = useFormidable<GenderWrite>({
initialValues: dataGender, initialValues: dataGender,
deltaConfig: { omit: ['covers'] },
}); });
const onSave = async () => { const onSave = async (dataDelta: GenderWrite) => {
if (isNullOrUndefined(genderIdInt)) { if (isNullOrUndefined(genderIdInt)) {
return; return;
} }
const dataThatNeedToBeUpdated = form.getDeltaData({ omit: ['covers'] }); console.log(`onSave = ${JSON.stringify(dataDelta, null, 2)}`);
console.log(`onSave = ${JSON.stringify(dataThatNeedToBeUpdated, null, 2)}`);
store.update( store.update(
GenderResource.patch({ GenderResource.patch({
restConfig: session.getRestConfig(), restConfig: session.getRestConfig(),
data: dataThatNeedToBeUpdated, data: dataDelta,
params: { params: {
id: genderIdInt, id: genderIdInt,
}, },
@ -139,43 +135,45 @@ export const GenderEditPopUp = ({ }: GenderEditPopUpProps) => {
); );
}; };
return ( return (
<Modal <DialogRoot
initialFocusRef={initialRef} //initialFocusRef={initialRef}
finalFocusRef={finalRef} //finalFocusRef={finalRef}
closeOnOverlayClick={false} //closeOnOverlayClick={false}
onClose={onClose} onOpenChange={onClose}
isOpen={true} open={true}
data-testid="gender-edit-pop-up"
> >
<ModalOverlay /> {/* <DialogOverlay /> */}
<ModalContent> <DialogContent>
<ModalHeader>Edit Gender</ModalHeader> <Formidable.From form={form} onSubmitDelta={onSave}>
<ModalCloseButton ref={finalRef} /> <DialogHeader>Edit Gender</DialogHeader>
{/* <DialogCloseButton ref={finalRef} /> */}
<ModalBody pb={6} gap="0px" paddingLeft="18px"> <DialogBody pb={6} gap="0px" paddingLeft="18px">
{admin && ( {admin && (
<> <>
<FormGroup isRequired label="Id"> <FormGroupShow isRequired label="Id">
<Text>{dataGender?.id}</Text> <Text>{dataGender?.id}</Text>
</FormGroup> </FormGroupShow>
{countTracksOnAGender !== 0 && ( {countTracksOnAGender !== 0 && (
<Flex paddingLeft="14px"> <Flex paddingLeft="14px">
<MdWarning color="red.600" /> <MdWarning color="red.600" />
<Text paddingLeft="6px" color="red.600" fontWeight="bold"> <Text paddingLeft="6px" color="red.600" fontWeight="bold">
Can not remove gender {countTracksOnAGender} track(s) depend Can not remove gender {countTracksOnAGender} track(s)
on it. depend on it.
</Text> </Text>
</Flex> </Flex>
)} )}
<FormGroup label="Action(s):"> <FormGroupShow label="Action(s):">
<Button <Button
onClick={disclosure.onOpen} onClick={disclosure.onOpen}
marginRight="auto" marginRight="auto"
variant="@danger" colorPalette="@danger"
isDisabled={countTracksOnAGender !== 0} disabled={countTracksOnAGender !== 0}
> >
<MdDeleteForever /> Remove gender <MdDeleteForever /> Remove gender
</Button> </Button>
</FormGroup> </FormGroupShow>
<ConfirmPopUp <ConfirmPopUp
disclosure={disclosure} disclosure={disclosure}
title="Remove gender" title="Remove gender"
@ -188,28 +186,22 @@ export const GenderEditPopUp = ({ }: GenderEditPopUpProps) => {
{!admin && ( {!admin && (
<> <>
<FormInput <FormInput
form={form} name="name"
variableName="name"
isRequired isRequired
label="Gender name" label="Gender name"
ref={initialRef} ref={initialRef}
/> />
<FormTextarea <FormTextarea name="description" label="Description" />
form={form}
variableName="description"
label="Description"
/>
<FormCovers <FormCovers
form={form} name="covers"
variableName="covers"
onFilesSelected={onFilesSelected} onFilesSelected={onFilesSelected}
onUriSelected={onUriSelected} onUriSelected={onUriSelected}
onRemove={onRemoveCover} onRemove={onRemoveCover}
/> />
</> </>
)} )}
</ModalBody> </DialogBody>
<ModalFooter> <DialogFooter>
<Button <Button
onClick={() => setAdmin((value) => !value)} onClick={() => setAdmin((value) => !value)}
marginRight="auto" marginRight="auto"
@ -227,13 +219,14 @@ export const GenderEditPopUp = ({ }: GenderEditPopUpProps) => {
)} )}
</Button> </Button>
{!admin && form.isFormModified && ( {!admin && form.isFormModified && (
<Button colorScheme="blue" mr={3} onClick={onSave}> <Button colorScheme="blue" mr={3} type="submit">
Save Save
</Button> </Button>
)} )}
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
</ModalFooter> </DialogFooter>
</ModalContent> </Formidable.From>
</Modal> </DialogContent>
</DialogRoot>
); );
}; };

View File

@ -1,38 +1,14 @@
import { ReactElement, useRef, useState } from 'react'; import { useRef } from 'react';
import { Button, Flex, Progress, Text } from '@chakra-ui/react';
import { import {
Button, DialogBody,
Flex, DialogContent,
Modal, DialogFooter,
ModalBody, DialogHeader,
ModalCloseButton, DialogRoot,
ModalContent, } from '@/components/ui/dialog';
ModalFooter,
ModalHeader,
ModalOverlay,
Progress,
Text,
useDisclosure,
} from '@chakra-ui/react';
import {
MdAdminPanelSettings,
MdDeleteForever,
MdEdit,
MdWarning,
} from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom';
import { Artist, ArtistResource } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers';
import { FormGroup } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea';
import { useFormidable } from '@/components/form/Formidable';
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
import { useArtistService, useSpecificArtist } from '@/service/Artist';
import { useServiceContext } from '@/service/ServiceContext';
import { useCountTracksOfAnArtist } from '@/service/Track';
import { isNullOrUndefined } from '@/utils/validator';
export type PopUpUploadProgressProps = { export type PopUpUploadProgressProps = {
title: string; title: string;
@ -63,22 +39,23 @@ export const PopUpUploadProgress = ({
title, title,
totalSize, totalSize,
}: PopUpUploadProgressProps) => { }: PopUpUploadProgressProps) => {
const initialRef = useRef(null); const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef(null); const finalRef = useRef<HTMLButtonElement>(null);
return ( return (
<Modal <DialogRoot
initialFocusRef={initialRef} //initialFocusRef={initialRef}
finalFocusRef={finalRef} //finalFocusRef={finalRef}
closeOnOverlayClick={false} //closeOnOverlayClick={false}
onClose={onClose} onOpenChange={onClose}
isOpen={true} open={true}
data-testid="upload-progress-edit-pop-up"
> >
<ModalOverlay /> {/* <DialogOverlay /> */}
<ModalContent> <DialogContent>
<ModalHeader>{title}</ModalHeader> <DialogHeader>{title}</DialogHeader>
<ModalCloseButton ref={finalRef} /> {/* <DialogCloseButton ref={finalRef} /> */}
<ModalBody pb={6} paddingLeft="18px"> <DialogBody pb={6} paddingLeft="18px">
<Flex direction="column" gap="10px"> <Flex direction="column" gap="10px">
{isFinished ? ( {isFinished ? (
<Text fontSize="20px" fontWeight="bold"> <Text fontSize="20px" fontWeight="bold">
@ -89,11 +66,11 @@ export const PopUpUploadProgress = ({
[{index + 1}/{elements.length}] {elements[index]} [{index + 1}/{elements.length}] {elements[index]}
</Text> </Text>
)} )}
<Progress <Progress.Root
colorScheme="green" colorScheme="green"
hasStripe striped
value={currentSize} value={currentSize}
isAnimated animated
max={totalSize} max={totalSize}
height="24px" height="24px"
/> />
@ -109,10 +86,10 @@ export const PopUpUploadProgress = ({
</Text> </Text>
)} )}
</Flex> </Flex>
</ModalBody> </DialogBody>
<ModalFooter> <DialogFooter>
{isFinished ? ( {isFinished ? (
<Button onClick={onClose} variant="@success"> <Button onClick={onClose} colorPalette="green">
Ok Ok
</Button> </Button>
) : ( ) : (
@ -120,8 +97,8 @@ export const PopUpUploadProgress = ({
Abort Abort
</Button> </Button>
)} )}
</ModalFooter> </DialogFooter>
</ModalContent> </DialogContent>
</Modal> </DialogRoot>
); );
}; };

View File

@ -1,29 +1,24 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { import { Button, Text, useDisclosure } from '@chakra-ui/react';
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { MdAdminPanelSettings, MdDeleteForever, MdEdit } from 'react-icons/md'; import { MdAdminPanelSettings, MdDeleteForever, MdEdit } from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Track, TrackResource } from '@/back-api'; import { TrackResource, TrackWrite } from '@/back-api';
import { FormGroup } from '@/components/form/FormGroup'; import { FormGroupShow } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput'; import { FormInput } from '@/components/form/FormInput';
import { FormNumber } from '@/components/form/FormNumber'; import { FormNumber } from '@/components/form/FormNumber';
import { FormSelect } from '@/components/form/FormSelect'; import { FormSelect } from '@/components/form/FormSelect';
import { FormSelectMultiple } from '@/components/form/FormSelectMultiple'; import { FormSelectMultiple } from '@/components/form/FormSelectMultiple';
import { FormTextarea } from '@/components/form/FormTextarea'; import { FormTextarea } from '@/components/form/FormTextarea';
import { useFormidable } from '@/components/form/Formidable';
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp'; import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
import {
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
} from '@/components/ui/dialog';
import { useOrderedAlbums } from '@/service/Album'; import { useOrderedAlbums } from '@/service/Album';
import { useOrderedArtists } from '@/service/Artist'; import { useOrderedArtists } from '@/service/Artist';
import { useOrderedGenders } from '@/service/Gender'; import { useOrderedGenders } from '@/service/Gender';
@ -31,6 +26,8 @@ import { useServiceContext } from '@/service/ServiceContext';
import { useSpecificTrack, useTrackService } from '@/service/Track'; import { useSpecificTrack, useTrackService } from '@/service/Track';
import { isNullOrUndefined } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
import { Formidable, useFormidable } from '../formidable';
export type TrackEditPopUpProps = {}; export type TrackEditPopUpProps = {};
export const TrackEditPopUp = ({}: TrackEditPopUpProps) => { export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
@ -65,25 +62,21 @@ export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
); );
onClose(); onClose();
}; };
const initialRef = useRef(null); const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef(null); const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<Track>({ const form = useFormidable<TrackWrite>({
//onSubmit,
//onValuesChange,
initialValues: dataTrack, initialValues: dataTrack,
//onValid: () => console.log('onValid'), deltaConfig: { omit: ['covers'] },
//onInvalid: () => console.log('onInvalid'),
}); });
const onSave = async () => { const onSave = async (dataDelta: TrackWrite) => {
if (isNullOrUndefined(trackIdInt)) { if (isNullOrUndefined(trackIdInt)) {
return; return;
} }
const dataThatNeedToBeUpdated = form.getDeltaData({ omit: ['covers'] }); console.log(`onSave = ${JSON.stringify(dataDelta, null, 2)}`);
console.log(`onSave = ${JSON.stringify(dataThatNeedToBeUpdated, null, 2)}`);
store.update( store.update(
TrackResource.patch({ TrackResource.patch({
restConfig: session.getRestConfig(), restConfig: session.getRestConfig(),
data: dataThatNeedToBeUpdated, data: dataDelta,
params: { params: {
id: trackIdInt, id: trackIdInt,
}, },
@ -91,36 +84,38 @@ export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
); );
}; };
return ( return (
<Modal <DialogRoot
initialFocusRef={initialRef} //initialFocusRef={initialRef}
finalFocusRef={finalRef} //finalFocusRef={finalRef}
closeOnOverlayClick={false} //closeOnOverlayClick={false}
onClose={onClose} onOpenChange={onClose}
isOpen={true} open={true}
data-testid="track-edit-pop-up"
> >
<ModalOverlay /> {/* <DialogOverlay /> */}
<ModalContent> <DialogContent>
<ModalHeader>Edit Track</ModalHeader> <Formidable.From form={form} onSubmitDelta={onSave}>
<ModalCloseButton ref={finalRef} /> <DialogHeader>Edit Track</DialogHeader>
{/* <DialogCloseButton ref={finalRef} /> */}
<ModalBody pb={6} gap="0px" paddingLeft="18px"> <DialogBody pb={6} gap="0px" paddingLeft="18px">
{admin && ( {admin && (
<> <>
<FormGroup isRequired label="Id"> <FormGroupShow isRequired label="Id">
<Text>{dataTrack?.id}</Text> <Text>{dataTrack?.id}</Text>
</FormGroup> </FormGroupShow>
<FormGroup label="Data Id"> <FormGroupShow label="Data Id">
<Text>{dataTrack?.dataId}</Text> <Text>{dataTrack?.dataId}</Text>
</FormGroup> </FormGroupShow>
<FormGroup label="Action(s):"> <FormGroupShow label="Action(s):">
<Button <Button
onClick={disclosure.onOpen} onClick={disclosure.onOpen}
marginRight="auto" marginRight="auto"
variant="@danger" colorPalette="@danger"
> >
<MdDeleteForever /> Remove Media <MdDeleteForever /> Remove Media
</Button> </Button>
</FormGroup> </FormGroupShow>
<ConfirmPopUp <ConfirmPopUp
disclosure={disclosure} disclosure={disclosure}
title="Remove track" title="Remove track"
@ -133,48 +128,35 @@ export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
{!admin && ( {!admin && (
<> <>
<FormInput <FormInput
form={form} name="name"
variableName="name"
isRequired isRequired
label="Title" label="Title"
ref={initialRef} ref={initialRef}
/> />
<FormTextarea <FormTextarea name="description" label="Description" />
form={form}
variableName="description"
label="Description"
/>
<FormSelect <FormSelect
form={form} name="genderId"
variableName="genderId"
options={dataGenders} options={dataGenders}
label="Gender" label="Gender"
/> />
<FormSelectMultiple <FormSelectMultiple
form={form} name="artists"
variableName="artists"
options={dataArtist} options={dataArtist}
label="Artist(s)" label="Artist(s)"
/> />
<FormSelect <FormSelect name="albumId" options={dataAlbums} label="Album" />
form={form}
variableName="albumId"
options={dataAlbums}
label="Album"
/>
<FormNumber <FormNumber
form={form} name="track"
variableName="track"
label="Track n°" label="Track n°"
step={1} step={1}
defaultValue={0} //defaultValue={0}
min={0} min={0}
max={1000} max={1000}
/> />
</> </>
)} )}
</ModalBody> </DialogBody>
<ModalFooter> <DialogFooter>
<Button <Button
onClick={() => setAdmin((value) => !value)} onClick={() => setAdmin((value) => !value)}
marginRight="auto" marginRight="auto"
@ -192,13 +174,14 @@ export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
)} )}
</Button> </Button>
{!admin && form.isFormModified && ( {!admin && form.isFormModified && (
<Button colorScheme="blue" mr={3} onClick={onSave}> <Button colorScheme="blue" mr={3} type="submit">
Save Save
</Button> </Button>
)} )}
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
</ModalFooter> </DialogFooter>
</ModalContent> </Formidable.From>
</Modal> </DialogContent>
</DialogRoot>
); );
}; };

View File

@ -1,16 +1,6 @@
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'; import { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { import { Button, Flex, HStack, Input, Spinner, Tag } from '@chakra-ui/react';
Button,
Flex,
Input,
Spinner,
Tag,
TagCloseButton,
TagLabel,
Wrap,
WrapItem,
} from '@chakra-ui/react';
import { MdEdit, MdKeyboardArrowDown, MdKeyboardArrowUp } from 'react-icons/md'; import { MdEdit, MdKeyboardArrowDown, MdKeyboardArrowUp } from 'react-icons/md';
import { SelectList, SelectListModel } from '@/components/select/SelectList'; import { SelectList, SelectListModel } from '@/components/select/SelectList';
@ -111,22 +101,30 @@ export const SelectMultiple = ({
return ( return (
<Flex direction="column" width="full" gap="0px"> <Flex direction="column" width="full" gap="0px">
{selectedOptions && ( {selectedOptions && (
<Wrap spacing="5px" justify="left" width="full" marginBottom="2px"> <HStack
{selectedOptions.map((data) => ( wrap="wrap"
<WrapItem key={data[keyKey]}> gap="5px"
<Tag justify="left"
size="md" width="full"
key="md" marginBottom="2px"
borderRadius="5px"
variant="solid"
backgroundColor="green.500"
> >
<TagLabel>{data[keyValue] ?? `id=${data[keyKey]}`}</TagLabel> {selectedOptions.map((data) => (
<TagCloseButton onClick={() => selectValue(data)} /> <Flex align="flex-start" key={data[keyKey]}>
</Tag> <Tag.Root
</WrapItem> size="xl"
borderRadius="5px"
variant="surface"
backgroundColor="green.800"
>
<Tag.Label>{data[keyValue] ?? `id=${data[keyKey]}`}</Tag.Label>
<Tag.CloseTrigger
boxSize="5"
onClick={() => selectValue(data)}
/>
</Tag.Root>
</Flex>
))} ))}
</Wrap> </HStack>
)} )}
<Flex> <Flex>
@ -137,7 +135,13 @@ export const SelectMultiple = ({
//onSubmit={onSubmit} //onSubmit={onSubmit}
onFocus={() => setShowList(true)} onFocus={() => setShowList(true)}
onBlur={() => setTimeout(() => setShowList(false), 200)} onBlur={() => setTimeout(() => setShowList(false), 200)}
value={showList ? (currentSearch ?? '') : hasSuggestion ? `suggest: ${currentSearch}` : ''} value={
showList
? (currentSearch ?? '')
: hasSuggestion
? `suggest: ${currentSearch}`
: ''
}
borderRadius="5px 0 0 5px" borderRadius="5px 0 0 5px"
/> />
<Button <Button

View File

@ -50,7 +50,9 @@ export const SelectSingle = ({
onCreate ? suggestion : undefined onCreate ? suggestion : undefined
); );
useEffect(() => { useEffect(() => {
console.log(`Update suggestion : ${onCreate} ${suggestion} ==> ${onCreate ? suggestion : undefined} .. ${onCreate && !isNullOrUndefined(suggestion) ? true : false}`); console.log(
`Update suggestion : ${onCreate} ${suggestion} ==> ${onCreate ? suggestion : undefined} .. ${onCreate && !isNullOrUndefined(suggestion) ? true : false}`
);
setCurrentSearch(onCreate ? suggestion : undefined); setCurrentSearch(onCreate ? suggestion : undefined);
setHasSuggestion(onCreate && !isNullOrUndefined(suggestion) ? true : false); setHasSuggestion(onCreate && !isNullOrUndefined(suggestion) ? true : false);
}, [suggestion]); }, [suggestion]);
@ -110,10 +112,13 @@ export const SelectSingle = ({
onFocus={() => setShowList(true)} onFocus={() => setShowList(true)}
onBlur={() => setTimeout(() => setShowList(false), 200)} onBlur={() => setTimeout(() => setShowList(false), 200)}
value={ value={
showList ? (currentSearch ?? '') : (selectedOptions?.name ?? (hasSuggestion ? `suggest: ${currentSearch}` : '')) showList
? (currentSearch ?? '')
: (selectedOptions?.name ??
(hasSuggestion ? `suggest: ${currentSearch}` : ''))
} }
backgroundColor={ backgroundColor={
showList || !selectedOptions ? undefined : 'green.500' showList || !selectedOptions ? undefined : 'green.800'
} }
borderRadius="5px 0 0 5px" borderRadius="5px 0 0 5px"
/> />

View File

@ -23,9 +23,7 @@ export const DisplayTrack = ({
data={track?.covers} data={track?.covers}
size="50" size="50"
height="full" height="full"
iconEmpty={ iconEmpty={trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />}
trackActive?.id === track.id ? LuPlay : LuMusic2
}
onClick={onClick} onClick={onClick}
/> />
<Flex <Flex
@ -38,13 +36,13 @@ export const DisplayTrack = ({
> >
<Text <Text
as="span" as="span"
align="left" alignContent="left"
fontSize="20px" fontSize="20px"
fontWeight="bold" fontWeight="bold"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={[1, 2]} // TODO: noOfLines={[1, 2]}
marginY="auto" marginY="auto"
color={trackActive?.id === track.id ? 'green.700' : undefined} color={trackActive?.id === track.id ? 'green.700' : undefined}
> >

View File

@ -1,5 +1,3 @@
import { Suspense } from 'react';
import { Flex, Text } from '@chakra-ui/react'; import { Flex, Text } from '@chakra-ui/react';
import { LuMusic2, LuPlay } from 'react-icons/lu'; import { LuMusic2, LuPlay } from 'react-icons/lu';
@ -26,14 +24,17 @@ export const DisplayTrackFull = ({
const { dataGender } = useSpecificGender(track?.genderId); const { dataGender } = useSpecificGender(track?.genderId);
const { dataArtists } = useSpecificArtists(track?.artists); const { dataArtists } = useSpecificArtists(track?.artists);
return ( return (
<Flex direction="row" width="full" height="full"> <Flex
direction="row"
width="full"
height="full"
data-testid="display-track-full"
>
<Covers <Covers
data={track?.covers} data={track?.covers}
size="50" size="60px"
marginY="auto" marginY="auto"
iconEmpty={ iconEmpty={trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />}
trackActive?.id === track.id ? LuPlay : LuMusic2
}
onClick={onClick} onClick={onClick}
/> />
<Flex <Flex
@ -46,13 +47,13 @@ export const DisplayTrackFull = ({
> >
<Text <Text
as="span" as="span"
align="left" alignContent="left"
fontSize="20px" fontSize="20px"
fontWeight="bold" fontWeight="bold"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={1} // TODO: noOfLines={1}
color={trackActive?.id === track.id ? 'green.700' : undefined} color={trackActive?.id === track.id ? 'green.700' : undefined}
> >
{track.name} {track.track && ` [${track.track}]`} {track.name} {track.track && ` [${track.track}]`}
@ -60,53 +61,65 @@ export const DisplayTrackFull = ({
{dataAlbum && ( {dataAlbum && (
<Text <Text
as="span" as="span"
align="left" alignContent="left"
fontSize="15px" fontSize="15px"
fontWeight="bold" fontWeight="bold"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={1} //noOfLines={1}
marginY="auto" marginY="auto"
color={trackActive?.id === track.id ? 'green.700' : undefined} color={trackActive?.id === track.id ? 'green.700' : undefined}
> >
<Text as="span" fontWeight="normal">Album:</Text> {dataAlbum.name} <Text as="span" fontWeight="normal">
Album:
</Text>{' '}
{dataAlbum.name}
</Text> </Text>
)} )}
{dataArtists && ( {dataArtists && (
<Text <Text
as="span" as="span"
align="left" alignContent="left"
fontSize="15px" fontSize="15px"
fontWeight="bold" fontWeight="bold"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={1} //noOfLines={1}
marginY="auto" marginY="auto"
color={trackActive?.id === track.id ? 'green.700' : undefined} color={trackActive?.id === track.id ? 'green.700' : undefined}
> >
<Text as="span" fontWeight="normal">Artist(s):</Text> {dataArtists.map((data) => data.name).join(', ')} <Text as="span" fontWeight="normal">
Artist(s):
</Text>{' '}
{dataArtists.map((data) => data.name).join(', ')}
</Text> </Text>
)} )}
{dataGender && ( {dataGender && (
<Text <Text
as="span" as="span"
align="left" alignContent="left"
fontSize="15px" fontSize="15px"
fontWeight="bold" fontWeight="bold"
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
noOfLines={1} //noOfLines={1}
marginY="auto" marginY="auto"
color={trackActive?.id === track.id ? 'green.700' : undefined} color={trackActive?.id === track.id ? 'green.700' : undefined}
> >
<Text as="span" fontWeight="normal">Gender:</Text> {dataGender.name} <Text as="span" fontWeight="normal">
Gender:
</Text>{' '}
{dataGender.name}
</Text> </Text>
)} )}
</Flex> </Flex>
<ContextMenu elements={contextMenu} /> <ContextMenu
elements={contextMenu}
data-testid="display-track-full_context-menu"
/>
</Flex> </Flex>
); );
}; };

View File

@ -0,0 +1,30 @@
import { Track } from '@/back-api';
import { MenuElement } from '@/components/contextMenu/ContextMenu';
import { useSpecificTrack } from '@/service/Track';
import { DisplayTrackFull } from './DisplayTrackFull';
import { DisplayTrackSkeleton } from './DisplayTrackSkeleton';
export type DisplayTrackProps = {
trackId: Track['id'];
onClick?: () => void;
contextMenu?: MenuElement[];
};
export const DisplayTrackFullId = ({
trackId,
onClick,
contextMenu,
}: DisplayTrackProps) => {
const { dataTrack } = useSpecificTrack(trackId);
if (dataTrack) {
return (
<DisplayTrackFull
track={dataTrack}
onClick={onClick}
contextMenu={contextMenu}
/>
);
} else {
return <DisplayTrackSkeleton />;
}
};

View File

@ -1,4 +1,4 @@
import { Flex, Skeleton, SkeletonText } from '@chakra-ui/react'; import { Flex, Skeleton } from '@chakra-ui/react';
export const DisplayTrackSkeleton = () => { export const DisplayTrackSkeleton = () => {
return ( return (
@ -17,13 +17,13 @@ export const DisplayTrackSkeleton = () => {
paddingLeft="5px" paddingLeft="5px"
overflowX="hidden" overflowX="hidden"
> >
<SkeletonText {/* <SkeletonText
skeletonHeight="20px" skeletonHeight="20px"
noOfLines={1} noOfLines={1}
spacing={0} gap={0}
width="50%" width="50%"
marginY="auto" marginY="auto"
/> /> */}
</Flex> </Flex>
</Flex> </Flex>
); );

View File

@ -0,0 +1,75 @@
'use client';
import * as React from 'react';
import type { GroupProps, SlotRecipeProps } from '@chakra-ui/react';
import { Avatar as ChakraAvatar, Group } from '@chakra-ui/react';
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>;
export interface AvatarProps extends ChakraAvatar.RootProps {
name?: string;
src?: string;
srcSet?: string;
loading?: ImageProps['loading'];
icon?: React.ReactElement;
fallback?: React.ReactNode;
}
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
function Avatar(props, ref) {
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
props;
return (
<ChakraAvatar.Root ref={ref} {...rest}>
<AvatarFallback name={name} icon={icon}>
{fallback}
</AvatarFallback>
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
{children}
</ChakraAvatar.Root>
);
}
);
interface AvatarFallbackProps extends ChakraAvatar.FallbackProps {
name?: string;
icon?: React.ReactElement;
}
const AvatarFallback = React.forwardRef<HTMLDivElement, AvatarFallbackProps>(
function AvatarFallback(props, ref) {
const { name, icon, children, ...rest } = props;
return (
<ChakraAvatar.Fallback ref={ref} {...rest}>
{children}
{name != null && children == null && <>{getInitials(name)}</>}
{name == null && children == null && (
<ChakraAvatar.Icon asChild={!!icon}>{icon}</ChakraAvatar.Icon>
)}
</ChakraAvatar.Fallback>
);
}
);
function getInitials(name: string) {
const names = name.trim().split(' ');
const firstName = names[0] != null ? names[0] : '';
const lastName = names.length > 1 ? names[names.length - 1] : '';
return firstName && lastName
? `${firstName.charAt(0)}${lastName.charAt(0)}`
: firstName.charAt(0);
}
interface AvatarGroupProps extends GroupProps, SlotRecipeProps<'avatar'> {}
export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
function AvatarGroup(props, ref) {
const { size, variant, borderless, ...rest } = props;
return (
<ChakraAvatar.PropsProvider value={{ size, variant, borderless }}>
<Group gap="0" spaceX="-3" ref={ref} {...rest} />
</ChakraAvatar.PropsProvider>
);
}
);

View File

@ -0,0 +1,41 @@
import * as React from 'react';
import type { ButtonProps as ChakraButtonProps } from '@chakra-ui/react';
import {
AbsoluteCenter,
Button as ChakraButton,
Span,
Spinner,
} from '@chakra-ui/react';
interface ButtonLoadingProps {
loading?: boolean;
loadingText?: React.ReactNode;
}
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
function Button(props, ref) {
const { loading, disabled, loadingText, children, ...rest } = props;
return (
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}>
{loading && !loadingText ? (
<>
<AbsoluteCenter display="inline-flex">
<Spinner size="inherit" color="inherit" />
</AbsoluteCenter>
<Span opacity={0}>{children}</Span>
</>
) : loading && loadingText ? (
<>
<Spinner size="inherit" color="inherit" />
{loadingText}
</>
) : (
children
)}
</ChakraButton>
);
}
);

View File

@ -0,0 +1,26 @@
import * as React from 'react';
import { Checkbox as ChakraCheckbox } from '@chakra-ui/react';
export interface CheckboxProps extends ChakraCheckbox.RootProps {
icon?: React.ReactNode;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
rootRef?: React.Ref<HTMLLabelElement>;
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
function Checkbox(props, ref) {
const { icon, children, inputProps, rootRef, ...rest } = props;
return (
<ChakraCheckbox.Root ref={rootRef} {...rest}>
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
<ChakraCheckbox.Control>
{icon || <ChakraCheckbox.Indicator />}
</ChakraCheckbox.Control>
{children != null && (
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
)}
</ChakraCheckbox.Root>
);
}
);

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import type { ButtonProps } from '@chakra-ui/react';
import { IconButton as ChakraIconButton } from '@chakra-ui/react';
import { LuX } from 'react-icons/lu';
export type CloseButtonProps = ButtonProps;
export const CloseButton = React.forwardRef<
HTMLButtonElement,
CloseButtonProps
>(function CloseButton(props, ref) {
return (
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
{props.children ?? <LuX />}
</ChakraIconButton>
);
});

View File

@ -0,0 +1,76 @@
'use client';
import * as React from 'react';
import type { IconButtonProps } from '@chakra-ui/react';
import { ClientOnly, IconButton, Skeleton } from '@chakra-ui/react';
import { ThemeProvider, useTheme } from 'next-themes';
import type { ThemeProviderProps } from 'next-themes';
import { LuMoon, LuSun } from 'react-icons/lu';
export interface ColorModeProviderProps extends ThemeProviderProps {}
export function ColorModeProvider(props: ColorModeProviderProps) {
return (
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
);
}
export type ColorMode = 'light' | 'dark';
export interface UseColorModeReturn {
colorMode: ColorMode;
setColorMode: (colorMode: ColorMode) => void;
toggleColorMode: () => void;
}
export function useColorMode(): UseColorModeReturn {
const { resolvedTheme, setTheme } = useTheme();
const toggleColorMode = () => {
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
};
return {
colorMode: resolvedTheme as ColorMode,
setColorMode: setTheme,
toggleColorMode,
};
}
export function useColorModeValue<T>(light: T, dark: T) {
const { colorMode } = useColorMode();
return colorMode === 'dark' ? dark : light;
}
export function ColorModeIcon() {
const { colorMode } = useColorMode();
return colorMode === 'dark' ? <LuMoon /> : <LuSun />;
}
interface ColorModeButtonProps extends Omit<IconButtonProps, 'aria-label'> {}
export const ColorModeButton = React.forwardRef<
HTMLButtonElement,
ColorModeButtonProps
>(function ColorModeButton(props, ref) {
const { toggleColorMode } = useColorMode();
return (
<ClientOnly fallback={<Skeleton boxSize="8" />}>
<IconButton
onClick={toggleColorMode}
variant="ghost"
aria-label="Toggle color mode"
size="sm"
ref={ref}
{...props}
css={{
_icon: {
width: '5',
height: '5',
},
}}
>
<ColorModeIcon />
</IconButton>
</ClientOnly>
);
});

View File

@ -0,0 +1,64 @@
import * as React from 'react';
import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react';
import { CloseButton } from './close-button';
interface DialogContentProps extends ChakraDialog.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement>;
backdrop?: boolean;
}
export const DialogContent = React.forwardRef<
HTMLDivElement,
DialogContentProps
>(function DialogContent(props, ref) {
const {
children,
portalled = true,
portalRef,
backdrop = true,
...rest
} = props;
return (
<Portal disabled={!portalled} container={portalRef}>
{backdrop && <ChakraDialog.Backdrop />}
<ChakraDialog.Positioner>
<ChakraDialog.Content ref={ref} {...rest} asChild={false}>
{children}
</ChakraDialog.Content>
</ChakraDialog.Positioner>
</Portal>
);
});
export const DialogCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraDialog.CloseTriggerProps
>(function DialogCloseTrigger(props, ref) {
return (
<ChakraDialog.CloseTrigger
position="absolute"
top="2"
insetEnd="2"
{...props}
asChild
>
<CloseButton size="sm" ref={ref}>
{props.children}
</CloseButton>
</ChakraDialog.CloseTrigger>
);
});
export const DialogRoot = ChakraDialog.Root;
export const DialogFooter = ChakraDialog.Footer;
export const DialogHeader = ChakraDialog.Header;
export const DialogBody = ChakraDialog.Body;
export const DialogBackdrop = ChakraDialog.Backdrop;
export const DialogTitle = ChakraDialog.Title;
export const DialogDescription = ChakraDialog.Description;
export const DialogTrigger = ChakraDialog.Trigger;
export const DialogActionTrigger = ChakraDialog.ActionTrigger;

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import { Drawer as ChakraDrawer, Portal } from '@chakra-ui/react';
import { CloseButton } from './close-button';
interface DrawerContentProps extends ChakraDrawer.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement>;
offset?: ChakraDrawer.ContentProps['padding'];
}
export const DrawerContent = React.forwardRef<
HTMLDivElement,
DrawerContentProps
>(function DrawerContent(props, ref) {
const { children, portalled = true, portalRef, offset, ...rest } = props;
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraDrawer.Positioner padding={offset}>
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
{children}
</ChakraDrawer.Content>
</ChakraDrawer.Positioner>
</Portal>
);
});
export const DrawerCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraDrawer.CloseTriggerProps
>(function DrawerCloseTrigger(props, ref) {
return (
<ChakraDrawer.CloseTrigger
position="absolute"
top="2"
insetEnd="2"
{...props}
asChild
>
<CloseButton size="sm" ref={ref} />
</ChakraDrawer.CloseTrigger>
);
});
export const DrawerTrigger = ChakraDrawer.Trigger;
export const DrawerRoot = ChakraDrawer.Root;
export const DrawerFooter = ChakraDrawer.Footer;
export const DrawerHeader = ChakraDrawer.Header;
export const DrawerBody = ChakraDrawer.Body;
export const DrawerBackdrop = ChakraDrawer.Backdrop;
export const DrawerDescription = ChakraDrawer.Description;
export const DrawerTitle = ChakraDrawer.Title;
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger;

View File

@ -0,0 +1,34 @@
import * as React from 'react';
import { Field as ChakraField } from '@chakra-ui/react';
export interface FieldProps extends Omit<ChakraField.RootProps, 'label'> {
label?: React.ReactNode;
helperText?: React.ReactNode;
errorText?: React.ReactNode;
optionalText?: React.ReactNode;
}
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
function Field(props, ref) {
const { label, children, helperText, errorText, optionalText, ...rest } =
props;
return (
<ChakraField.Root ref={ref} {...rest}>
{label && (
<ChakraField.Label>
{label}
<ChakraField.RequiredIndicator fallback={optionalText} />
</ChakraField.Label>
)}
{children}
{helperText && (
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
)}
{errorText && (
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
)}
</ChakraField.Root>
);
}
);

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import type { BoxProps, InputElementProps } from '@chakra-ui/react';
import { Group, InputElement } from '@chakra-ui/react';
export interface InputGroupProps extends BoxProps {
startElementProps?: InputElementProps;
endElementProps?: InputElementProps;
startElement?: React.ReactNode;
endElement?: React.ReactNode;
children: React.ReactElement<InputElementProps>;
startOffset?: InputElementProps['paddingStart'];
endOffset?: InputElementProps['paddingEnd'];
}
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
function InputGroup(props, ref) {
const {
startElement,
startElementProps,
endElement,
endElementProps,
children,
startOffset = '6px',
endOffset = '6px',
...rest
} = props;
const child =
React.Children.only<React.ReactElement<InputElementProps>>(children);
return (
<Group ref={ref} {...rest}>
{startElement && (
<InputElement pointerEvents="none" {...startElementProps}>
{startElement}
</InputElement>
)}
{React.cloneElement(child, {
...(startElement && {
ps: `calc(var(--input-height) - ${startOffset})`,
}),
...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }),
...children.props,
})}
{endElement && (
<InputElement placement="end" {...endElementProps}>
{endElement}
</InputElement>
)}
</Group>
);
}
);

View File

@ -0,0 +1,111 @@
'use client';
import * as React from 'react';
import { AbsoluteCenter, Menu as ChakraMenu, Portal } from '@chakra-ui/react';
import { LuCheck, LuChevronRight } from 'react-icons/lu';
interface MenuContentProps extends ChakraMenu.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement>;
}
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(
function MenuContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props;
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraMenu.Positioner>
<ChakraMenu.Content ref={ref} {...rest} />
</ChakraMenu.Positioner>
</Portal>
);
}
);
export const MenuArrow = React.forwardRef<
HTMLDivElement,
ChakraMenu.ArrowProps
>(function MenuArrow(props, ref) {
return (
<ChakraMenu.Arrow ref={ref} {...props}>
<ChakraMenu.ArrowTip />
</ChakraMenu.Arrow>
);
});
export const MenuCheckboxItem = React.forwardRef<
HTMLDivElement,
ChakraMenu.CheckboxItemProps
>(function MenuCheckboxItem(props, ref) {
return (
<ChakraMenu.CheckboxItem ref={ref} {...props}>
<ChakraMenu.ItemIndicator hidden={false}>
<LuCheck />
</ChakraMenu.ItemIndicator>
{props.children}
</ChakraMenu.CheckboxItem>
);
});
export const MenuRadioItem = React.forwardRef<
HTMLDivElement,
ChakraMenu.RadioItemProps
>(function MenuRadioItem(props, ref) {
const { children, ...rest } = props;
return (
<ChakraMenu.RadioItem ps="8" ref={ref} {...rest}>
<AbsoluteCenter axis="horizontal" left="4" asChild>
<ChakraMenu.ItemIndicator>
<LuCheck />
</ChakraMenu.ItemIndicator>
</AbsoluteCenter>
<ChakraMenu.ItemText>{children}</ChakraMenu.ItemText>
</ChakraMenu.RadioItem>
);
});
export const MenuItemGroup = React.forwardRef<
HTMLDivElement,
ChakraMenu.ItemGroupProps
>(function MenuItemGroup(props, ref) {
const { title, children, ...rest } = props;
return (
<ChakraMenu.ItemGroup ref={ref} {...rest}>
{title && (
<ChakraMenu.ItemGroupLabel userSelect="none">
{title}
</ChakraMenu.ItemGroupLabel>
)}
{children}
</ChakraMenu.ItemGroup>
);
});
export interface MenuTriggerItemProps extends ChakraMenu.ItemProps {
startIcon?: React.ReactNode;
}
export const MenuTriggerItem = React.forwardRef<
HTMLDivElement,
MenuTriggerItemProps
>(function MenuTriggerItem(props, ref) {
const { startIcon, children, ...rest } = props;
return (
<ChakraMenu.TriggerItem ref={ref} {...rest}>
{startIcon}
{children}
<LuChevronRight />
</ChakraMenu.TriggerItem>
);
});
export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup;
export const MenuContextTrigger = ChakraMenu.ContextTrigger;
export const MenuRoot = ChakraMenu.Root;
export const MenuSeparator = ChakraMenu.Separator;
export const MenuItem = ChakraMenu.Item;
export const MenuItemText = ChakraMenu.ItemText;
export const MenuItemCommand = ChakraMenu.ItemCommand;
export const MenuTrigger = ChakraMenu.Trigger;

View File

@ -0,0 +1,25 @@
import * as React from 'react';
import { NumberInput as ChakraNumberInput } from '@chakra-ui/react';
export interface NumberInputProps extends ChakraNumberInput.RootProps {}
export const NumberInputRoot = React.forwardRef<
HTMLDivElement,
NumberInputProps
>(function NumberInput(props, ref) {
const { children, ...rest } = props;
return (
<ChakraNumberInput.Root ref={ref} variant="outline" {...rest}>
{children}
<ChakraNumberInput.Control>
<ChakraNumberInput.IncrementTrigger />
<ChakraNumberInput.DecrementTrigger />
</ChakraNumberInput.Control>
</ChakraNumberInput.Root>
);
});
export const NumberInputField = ChakraNumberInput.Input;
export const NumberInputScrubber = ChakraNumberInput.Scrubber;
export const NumberInputLabel = ChakraNumberInput.Label;

View File

@ -0,0 +1,61 @@
import * as React from 'react';
import { Popover as ChakraPopover, Portal } from '@chakra-ui/react';
import { CloseButton } from './close-button';
interface PopoverContentProps extends ChakraPopover.ContentProps {
portalled?: boolean;
portalRef?: React.RefObject<HTMLElement>;
}
export const PopoverContent = React.forwardRef<
HTMLDivElement,
PopoverContentProps
>(function PopoverContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props;
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraPopover.Positioner>
<ChakraPopover.Content ref={ref} {...rest} />
</ChakraPopover.Positioner>
</Portal>
);
});
export const PopoverArrow = React.forwardRef<
HTMLDivElement,
ChakraPopover.ArrowProps
>(function PopoverArrow(props, ref) {
return (
<ChakraPopover.Arrow {...props} ref={ref}>
<ChakraPopover.ArrowTip />
</ChakraPopover.Arrow>
);
});
export const PopoverCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraPopover.CloseTriggerProps
>(function PopoverCloseTrigger(props, ref) {
return (
<ChakraPopover.CloseTrigger
position="absolute"
top="1"
insetEnd="1"
{...props}
asChild
ref={ref}
>
<CloseButton size="sm" />
</ChakraPopover.CloseTrigger>
);
});
export const PopoverTitle = ChakraPopover.Title;
export const PopoverDescription = ChakraPopover.Description;
export const PopoverFooter = ChakraPopover.Footer;
export const PopoverHeader = ChakraPopover.Header;
export const PopoverRoot = ChakraPopover.Root;
export const PopoverBody = ChakraPopover.Body;
export const PopoverTrigger = ChakraPopover.Trigger;

View File

@ -0,0 +1,13 @@
'use client';
import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
import { ColorModeProvider, type ColorModeProviderProps } from './color-mode';
export function Provider(props: ColorModeProviderProps) {
return (
<ChakraProvider value={defaultSystem}>
<ColorModeProvider {...props} />
</ChakraProvider>
);
}

View File

@ -0,0 +1,25 @@
import * as React from 'react';
import { RadioGroup as ChakraRadioGroup } from '@chakra-ui/react';
export interface RadioProps extends ChakraRadioGroup.ItemProps {
rootRef?: React.Ref<HTMLDivElement>;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
}
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
function Radio(props, ref) {
const { children, inputProps, rootRef, ...rest } = props;
return (
<ChakraRadioGroup.Item ref={rootRef} {...rest}>
<ChakraRadioGroup.ItemHiddenInput ref={ref} {...inputProps} />
<ChakraRadioGroup.ItemIndicator />
{children && (
<ChakraRadioGroup.ItemText>{children}</ChakraRadioGroup.ItemText>
)}
</ChakraRadioGroup.Item>
);
}
);
export const RadioGroup = ChakraRadioGroup.Root;

View File

@ -0,0 +1,83 @@
import * as React from 'react';
import { Slider as ChakraSlider, For, HStack } from '@chakra-ui/react';
export interface SliderProps extends ChakraSlider.RootProps {
marks?: Array<number | { value: number; label: React.ReactNode }>;
label?: React.ReactNode;
showValue?: boolean;
}
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
function Slider(props, ref) {
const { marks: marksProp, label, showValue, ...rest } = props;
const value = props.defaultValue ?? props.value;
const marks = marksProp?.map((mark) => {
if (typeof mark === 'number') return { value: mark, label: undefined };
return mark;
});
const hasMarkLabel = !!marks?.some((mark) => mark.label);
return (
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
{label && !showValue && (
<ChakraSlider.Label>{label}</ChakraSlider.Label>
)}
{label && showValue && (
<HStack justify="space-between">
<ChakraSlider.Label>{label}</ChakraSlider.Label>
<ChakraSlider.ValueText />
</HStack>
)}
<ChakraSlider.Control data-has-mark-label={hasMarkLabel || undefined}>
<ChakraSlider.Track>
<ChakraSlider.Range />
</ChakraSlider.Track>
<SliderThumbs value={value} />
<SliderMarks marks={marks} />
</ChakraSlider.Control>
</ChakraSlider.Root>
);
}
);
function SliderThumbs(props: { value?: number[] }) {
const { value } = props;
return (
<For each={value}>
{(_, index) => (
<ChakraSlider.Thumb key={index} index={index}>
<ChakraSlider.HiddenInput />
</ChakraSlider.Thumb>
)}
</For>
);
}
interface SliderMarksProps {
marks?: Array<number | { value: number; label: React.ReactNode }>;
}
const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>(
function SliderMarks(props, ref) {
const { marks } = props;
if (!marks?.length) return null;
return (
<ChakraSlider.MarkerGroup ref={ref}>
{marks.map((mark, index) => {
const value = typeof mark === 'number' ? mark : mark.value;
const label = typeof mark === 'number' ? undefined : mark.label;
return (
<ChakraSlider.Marker key={index} value={value}>
<ChakraSlider.MarkerIndicator />
{label}
</ChakraSlider.Marker>
);
})}
</ChakraSlider.MarkerGroup>
);
}
);

View File

@ -0,0 +1,52 @@
'use client';
import {
Toaster as ChakraToaster,
Portal,
Spinner,
Stack,
Toast,
createToaster,
} from '@chakra-ui/react';
import { RestErrorResponse } from '@/back-api';
export const toaster = createToaster({
placement: 'bottom-end',
pauseOnPageIdle: true,
});
export const toasterAPIError = (error: RestErrorResponse) => {
toaster.create({
title: `[${error.status}] ${error.statusMessage}`,
description: error.message,
});
};
export const Toaster = () => {
return (
<Portal>
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
{(toast) => (
<Toast.Root width={{ md: 'sm' }}>
{toast.type === 'loading' ? (
<Spinner size="sm" color="blue.solid" />
) : (
<Toast.Indicator />
)}
<Stack gap="1" flex="1" maxWidth="100%">
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
{toast.description && (
<Toast.Description>{toast.description}</Toast.Description>
)}
</Stack>
{toast.action && (
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
)}
{toast.meta?.closable && <Toast.CloseTrigger />}
</Toast.Root>
)}
</ChakraToaster>
</Portal>
);
};

Some files were not shown because too many files have changed in this diff Show More