Compare commits

..

No commits in common. "de61cc156fc2d62fb933037b2af87b68ecebef5d" and "80dfabcf4882809d6ed884cb817523472db9b604" have entirely different histories.

124 changed files with 1860 additions and 13482 deletions

1
.gitignore vendored
View File

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

View File

@ -6,71 +6,38 @@ Karideo
Run in local: Run in local:
============= =============
Start tools so simple...
-----------
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)
docker compose -f env_dev/docker-compose.yaml up -d cd bdd
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:
-------------------
backend is developed in JAVA convert in an angular application:
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
```
Install the dependency: Link with the external sub-library (front)
```bash ------------------------------------------
mvn install
```
Run the test Link:
```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 install pnpm run link_kar_cw
pnpm dev
``` ```
Display the result: un-link:
-------------------
[show the webpage: http://localhost:4203](http://localhost:4203)
Some other dev tools:
=====================
Format code:
------------
```bash ```bash
export PATH=$(ls -d --color=never /usr/lib/jvm/java-2*-openjdk)/bin:$PATH cd front
mvn formatter:format pnpm run unlink_kar_cw
mvn test
``` ```
Tools in production mode Tools in production mode

View File

@ -104,38 +104,9 @@
<plugin> <plugin>
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId> <artifactId>exec-maven-plugin</artifactId>
<version>3.2.0</version> <version>1.4.0</version>
<executions>
<execution>
<id>prod-mode</id>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>org.kar.karusic.WebLauncher</mainClass>
</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> <configuration>
<mainClass/> <mainClass>org.kar.karusic.WebLauncher</mainClass>
</configuration> </configuration>
</plugin> </plugin>
<!-- Create the source bundle --> <!-- Create the source bundle -->

View File

@ -1,17 +0,0 @@
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,9 +1,22 @@
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.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;
@ -13,12 +26,22 @@ 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
Initialization.generateObjects(); 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:"); launcher.LOGGER.info("end-configure the server & wait finish process:");

View File

@ -2,22 +2,10 @@ 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;
@ -37,16 +25,6 @@ public class Initialization extends MigrationSqlStep {
return "Initialization"; return "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<?> clazz : CLASSES_BASE) { for (final Class<?> clazz : CLASSES_BASE) {

View File

@ -4,11 +4,23 @@ 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 { ColorModeProvider } from '../src/components/ui/color-mode'; import theme from '../src/theme';
import { Toaster } from '../src/components/ui/toaster';
import { systemTheme } from '../src/theme/theme'; // .storybook/preview.js
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">
@ -19,16 +31,13 @@ const DocumentationWrapper = ({ children }) => {
export const decorators = [ export const decorators = [
(Story, context) => ( (Story, context) => (
<ColorModeProvider> <ChakraProvider theme={theme}>
<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> </ChakraProvider>
<Toaster />
</ChakraProvider>
</ColorModeProvider>
), ),
]; ];

6
front/app-build.json Normal file
View File

@ -0,0 +1,6 @@
{
"display": "2025-01-14",
"version": "0.0.1-dev\n - 2025-01-14T20:19:20+01:00",
"commit": "0.0.1-dev\n",
"date": "2025-01-14T20:19:20+01:00"
}

25
front/build.js Normal file
View File

@ -0,0 +1,25 @@
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();

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,16 @@ 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

@ -18,7 +18,7 @@
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest watch", "test:watch": "vitest watch",
"build": "tsc && vite build", "build": "tsc && vite build",
"static:build": "pnpm build", "static:build": "node build.js && pnpm build",
"dev": "vite", "dev": "vite",
"pretty": "prettier -w .", "pretty": "prettier -w .",
"lint": "pnpm tsc --noEmit", "lint": "pnpm tsc --noEmit",
@ -29,47 +29,46 @@
"*.{ts,tsx,js,jsx,json}": "prettier --write" "*.{ts,tsx,js,jsx,json}": "prettier --write"
}, },
"dependencies": { "dependencies": {
"@trivago/prettier-plugin-sort-imports": "5.2.2", "@chakra-ui/cli": "3.3.1",
"@chakra-ui/cli": "3.7.0", "@chakra-ui/react": "3.3.1",
"@chakra-ui/react": "3.7.0",
"@emotion/react": "11.14.0", "@emotion/react": "11.14.0",
"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",
"next-themes": "^0.4.4", "next-themes": "^0.4.4",
"react": "19.0.0", "react": "18.3.1",
"react-dom": "19.0.0", "react-dom": "18.3.1",
"react-error-boundary": "5.0.0", "react-error-boundary": "5.0.0",
"react-icons": "5.4.0", "react-icons": "5.4.0",
"react-router-dom": "7.1.5", "react-router-dom": "7.1.1",
"react-select": "5.10.0", "react-select": "5.9.0",
"react-use": "17.6.0", "react-use": "17.6.0",
"zod": "3.24.1", "zod": "3.24.1",
"zustand": "5.0.3" "zustand": "5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@chakra-ui/styled-system": "^2.12.0", "@chakra-ui/styled-system": "^2.12.0",
"@playwright/test": "1.50.1", "@playwright/test": "1.49.1",
"@storybook/addon-actions": "8.5.3", "@storybook/addon-actions": "8.4.7",
"@storybook/addon-essentials": "8.5.3", "@storybook/addon-essentials": "8.4.7",
"@storybook/addon-links": "8.5.3", "@storybook/addon-links": "8.4.7",
"@storybook/addon-mdx-gfm": "8.5.3", "@storybook/addon-mdx-gfm": "8.4.7",
"@storybook/react": "8.5.3", "@storybook/react": "8.4.7",
"@storybook/react-vite": "8.5.3", "@storybook/react-vite": "8.4.7",
"@storybook/theming": "8.5.3", "@storybook/theming": "8.4.7",
"@testing-library/jest-dom": "6.6.3", "@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0", "@testing-library/react": "16.1.0",
"@testing-library/user-event": "14.6.1", "@testing-library/user-event": "14.5.2",
"@trivago/prettier-plugin-sort-imports": "5.2.2", "@trivago/prettier-plugin-sort-imports": "5.2.1",
"@types/jest": "29.5.14", "@types/jest": "29.5.14",
"@types/node": "22.13.1", "@types/node": "22.10.6",
"@types/react": "19.0.8", "@types/react": "18.3.8",
"@types/react-dom": "19.0.3", "@types/react-dom": "18.3.0",
"@typescript-eslint/eslint-plugin": "8.23.0", "@typescript-eslint/eslint-plugin": "8.20.0",
"@typescript-eslint/parser": "8.23.0", "@typescript-eslint/parser": "8.20.0",
"@vitejs/plugin-react": "4.3.4", "@vitejs/plugin-react": "4.3.4",
"eslint": "9.20.0", "eslint": "9.18.0",
"eslint-plugin-codeceptjs": "1.3.0", "eslint-plugin-codeceptjs": "1.3.0",
"eslint-plugin-import": "2.31.0", "eslint-plugin-import": "2.31.0",
"eslint-plugin-react": "7.37.4", "eslint-plugin-react": "7.37.4",
@ -77,16 +76,16 @@
"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.43.6", "knip": "5.42.0",
"lint-staged": "15.4.3", "lint-staged": "15.3.0",
"npm-check-updates": "^17.1.14", "npm-check-updates": "^17.1.13",
"prettier": "3.4.2", "prettier": "3.4.2",
"puppeteer": "24.2.0", "puppeteer": "24.0.0",
"react-is": "19.0.0", "react-is": "19.0.0",
"storybook": "8.5.3", "storybook": "8.4.7",
"ts-node": "10.9.2", "ts-node": "10.9.2",
"typescript": "5.7.3", "typescript": "5.7.3",
"vite": "6.1.0", "vite": "6.0.7",
"vitest": "3.0.5" "vitest": "2.1.8"
} }
} }

View File

@ -1,19 +1,128 @@
import { ErrorBoundary } from '@/errors/ErrorBoundary'; import { useState } from 'react';
import { AudioPlayer } from './components'; import {
import { EnvDevelopment } from './components/EnvDevelopment/EnvDevelopment'; Box,
import { AppRoutes } from './scene/AppRoutes'; Button,
import { ServiceContextProvider } from './service/ServiceContext'; ChakraProvider,
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
DialogTrigger,
SelectContent,
SelectItem,
SelectRoot,
SelectTrigger,
SelectValueText,
Stack,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { environment } from '@/environment';
import { App as SpaApp } from '@/scene/App';
import { USERS, USERS_COLLECTION } from '@/service/session';
import { hashLocalData } from '@/utils/sso';
import { Toaster } from './components/ui/toaster';
import { systemTheme } from './theme/theme';
import { CloseButton } from './components/ui/close-button';
const AppEnvHint = () => {
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]}`;
}
};
export const App = () => {
return ( return (
<ServiceContextProvider> <>
<EnvDevelopment /> <Box
<ErrorBoundary> as="button"
<AppRoutes /> zIndex="100000"
</ErrorBoundary> position="fixed"
<AudioPlayer /> top="0"
</ServiceContextProvider> 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 >
<DialogRoot open={dialog.open} onOpenChange={dialog.onClose}>
<DialogContent>
<DialogHeader>Outils développeurs</DialogHeader>
<DialogBody>
<Stack>
<Text>User</Text>
<SelectRoot onChange={handleChange} collection={USERS_COLLECTION}>
<SelectTrigger>
<SelectValueText placeholder="Select test user" />
</SelectTrigger>
<SelectContent>
{USERS_COLLECTION.items.map((value) => (
<SelectItem item={value} key={value.value}>
{value.label}
</SelectItem>
))}
</SelectContent>
</SelectRoot>
</Stack>
</DialogBody>
<DialogFooter>
<Button onClick={onClose}>Close</Button>
</DialogFooter>
</DialogContent>
</DialogRoot>
</>
);
};
const App = () => {
return (
<ChakraProvider value={systemTheme}>
<AppEnvHint />
<SpaApp />
<Toaster />
</ChakraProvider>
); );
}; };

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().optional(), name: zod.string().max(256).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().nullable().optional(), name: zod.string().max(256).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().optional(), name: zod.string().max(256).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().optional(), firstName: zod.string().max(256).optional(),
surname: zod.string().optional(), surname: zod.string().max(256).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().nullable().optional(), name: zod.string().max(256).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().nullable().optional(), firstName: zod.string().max(256).nullable().optional(),
surname: zod.string().nullable().optional(), surname: zod.string().max(256).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().optional(), name: zod.string().max(256).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().nullable().optional(), name: zod.string().max(256).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,7 +24,9 @@ export function isGenericDataSoftDelete(data: any): data is GenericDataSoftDelet
return false; return false;
} }
} }
export const ZodGenericDataSoftDeleteWrite = ZodGenericDataWrite; export const ZodGenericDataSoftDeleteWrite = ZodGenericDataWrite.extend({
});
export type GenericDataSoftDeleteWrite = zod.infer<typeof ZodGenericDataSoftDeleteWrite>; export type GenericDataSoftDeleteWrite = zod.infer<typeof ZodGenericDataSoftDeleteWrite>;

View File

@ -25,7 +25,9 @@ export function isGenericData(data: any): data is GenericData {
return false; return false;
} }
} }
export const ZodGenericDataWrite = ZodGenericTimingWrite; export const ZodGenericDataWrite = ZodGenericTimingWrite.extend({
});
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().optional(), name: zod.string().max(256).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().nullable().optional(), name: zod.string().max(256).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,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().optional(), name: zod.string().max(256).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().nullable().optional(), name: zod.string().max(256).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,7 +5,9 @@ import { z as zod } from "zod";
import {ZodUser, ZodUserWrite } from "./user"; import {ZodUser, ZodUserWrite } from "./user";
export const ZodUserKarusic = ZodUser; export const ZodUserKarusic = ZodUser.extend({
});
export type UserKarusic = zod.infer<typeof ZodUserKarusic>; export type UserKarusic = zod.infer<typeof ZodUserKarusic>;
@ -18,7 +20,9 @@ export function isUserKarusic(data: any): data is UserKarusic {
return false; return false;
} }
} }
export const ZodUserKarusicWrite = ZodUserWrite; export const ZodUserKarusicWrite = ZodUserWrite.extend({
});
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().optional(), login: zod.string().max(255).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().optional(), blocked: zod.boolean(),
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().nullable().optional(), blocked: zod.boolean(),
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,9 +1,16 @@
import { useEffect, useRef, useState } from 'react'; import { SyntheticEvent, useEffect, useRef, useState } from 'react';
import { Box, Flex, IconButton, SliderTrack, Text } from '@chakra-ui/react'; import {
Box,
Flex,
IconButton,
SliderTrack,
Text,
} from '@chakra-ui/react';
import { import {
MdFastForward, MdFastForward,
MdFastRewind, MdFastRewind,
MdGraphicEq,
MdLooksOne, MdLooksOne,
MdNavigateBefore, MdNavigateBefore,
MdNavigateNext, MdNavigateNext,
@ -15,15 +22,15 @@ 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 { useColorModeValue } from '@/components/ui/color-mode';
import { isNullOrUndefined } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
import { Icon } from './Icon';
import { Slider } from './ui/slider'; import { Slider } from './ui/slider';
export enum PlayMode { export enum PlayMode {
@ -34,16 +41,10 @@ export enum PlayMode {
} }
const playModeIcon = { const playModeIcon = {
[PlayMode.PLAY_ONE]: <MdLooksOne style={{ width: '100%', height: '100%' }} />, [PlayMode.PLAY_ONE]: <MdLooksOne style={{ width: "100%", height: "100%" }} />,
[PlayMode.PLAY_ALL]: ( [PlayMode.PLAY_ALL]: <MdTrendingFlat style={{ width: "100%", height: "100%" }} />,
<MdTrendingFlat style={{ width: '100%', height: '100%' }} /> [PlayMode.PLAY_ONE_LOOP]: <MdRepeatOne style={{ width: "100%", height: "100%" }} />,
), [PlayMode.PLAY_ALL_LOOP]: <MdRepeat style={{ width: "100%", height: "100%" }} />,
[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 = {};
@ -59,7 +60,7 @@ const formatTime = (time) => {
return '00:00'; return '00:00';
}; };
export const AudioPlayer = ({}: AudioPlayerProps) => { export const AudioPlayer = ({ }: AudioPlayerProps) => {
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,9 +90,9 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
_hover: { _hover: {
bgColor: 'brand.500', bgColor: 'brand.500',
}, },
width: '50px', width: "50px",
height: '50px', height: "50px",
padding: '5px', padding: "5px",
}; };
useEffect(() => { useEffect(() => {
@ -197,7 +198,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
console.log(`onTimeUpdate ${audioRef.current.currentTime}`); console.log(`onTimeUpdate ${audioRef.current.currentTime}`);
setTimeProgress(audioRef.current.currentTime); setTimeProgress(audioRef.current.currentTime);
}; };
const onDurationChange = (event) => {}; const onDurationChange = (event) => { };
const onChangeStateToPlay = () => { const onChangeStateToPlay = () => {
setIsPlaying(true); setIsPlaying(true);
}; };
@ -211,7 +212,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
result.push(60 * i); result.push(60 * i);
} }
return result; return result;
}; }
return ( return (
<> <>
{!isNullOrUndefined(trackOffset) && ( {!isNullOrUndefined(trackOffset) && (
@ -239,7 +240,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
// noOfLines={1} // noOfLines={1}
> >
{dataTrack?.name ?? '???'} {dataTrack?.name ?? '???'}
</Text> </Text>
@ -249,7 +250,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
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}
@ -266,13 +267,10 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
variant="outline" variant="outline"
colorPalette="brand" colorPalette="brand"
marks={marks()} marks={marks()}
//focusCapture={false} //focusCapture={false}
> >
<SliderTrack <SliderTrack bg="brand.200" height="10px" borderRadius="full">
bg="brand.200" </SliderTrack>
height="10px"
borderRadius="full"
></SliderTrack>
</Slider> </Slider>
</Box> </Box>
<Flex> <Flex>
@ -282,7 +280,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
// noOfLines={1} // noOfLines={1}
> >
{formatTime(timeProgress)} {formatTime(timeProgress)}
</Text> </Text>
@ -298,9 +296,9 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
variant="ghost" variant="ghost"
> >
{isPlaying ? ( {isPlaying ? (
<MdPause style={{ width: '100%', height: '100%' }} /> <MdPause style={{ width: "100%", height: "100%" }} />
) : ( ) : (
<MdPlayArrow style={{ width: '100%', height: '100%' }} /> <MdPlayArrow style={{ width: "100%", height: "100%" }} />
)} )}
</IconButton> </IconButton>
<IconButton <IconButton
@ -308,51 +306,39 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
aria-label={'Stop'} aria-label={'Stop'}
onClick={onStop} onClick={onStop}
variant="ghost" variant="ghost"
> ><MdStop style={{ width: "100%", height: "100%" }} /></IconButton>
<MdStop style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'Previous track'} aria-label={'Previous track'}
onClick={onNavigatePrevious} onClick={onNavigatePrevious}
marginLeft="auto" marginLeft="auto"
variant="ghost" variant="ghost"
> ><MdNavigateBefore style={{ width: "100%", height: "100%" }} /> </IconButton>
<MdNavigateBefore style={{ width: '100%', height: '100%' }} />{' '}
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'jump 15sec in past'} aria-label={'jump 15sec in past'}
onClick={onFastRewind} onClick={onFastRewind}
variant="ghost" variant="ghost"
> ><MdFastRewind style={{ width: "100%", height: "100%" }} /></IconButton>
<MdFastRewind style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'jump 15sec in future'} aria-label={'jump 15sec in future'}
onClick={onFastForward} onClick={onFastForward}
variant="ghost" variant="ghost"
> ><MdFastForward style={{ width: "100%", height: "100%" }} /></IconButton>
<MdFastForward style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'Next track'} aria-label={'Next track'}
marginRight="auto" marginRight="auto"
onClick={onNavigateNext} onClick={onNavigateNext}
variant="ghost" variant="ghost"
> ><MdNavigateNext style={{ width: "100%", height: "100%" }} /></IconButton>
<MdNavigateNext style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton <IconButton
{...configButton} {...configButton}
aria-label={'continue to the end'} aria-label={'continue to the end'}
onClick={onTypePlay} onClick={onTypePlay}
variant="ghost" variant="ghost"
> >{playModeIcon[playingMode]}</IconButton>
{playModeIcon[playingMode]}
</IconButton>
</Flex> </Flex>
</Flex> </Flex>
)} )}

View File

@ -3,14 +3,13 @@ import { ReactElement, useEffect, useState } from 'react';
import { Box, BoxProps, Flex, FlexProps } 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 { ObjectId } from '@/back-api';
import { DataUrlAccess } from '@/utils/data-url-access'; import { DataUrlAccess } from '@/utils/data-url-access';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { ObjectId } from '@/back-api';
export type CoversProps = Omit<BoxProps, 'iconEmpty'> & { export type CoversProps = Omit<BoxProps, "iconEmpty"> & {
data?: ObjectId[]; data?: ObjectId[];
size?: BoxProps['width']; size?: BoxProps["width"];
iconEmpty?: ReactElement; iconEmpty?: ReactElement;
slideshow?: boolean; slideshow?: boolean;
}; };
@ -34,9 +33,7 @@ export const Covers = ({
setPreviousImageIndex(currentImageIndex); setPreviousImageIndex(currentImageIndex);
setTopOpacity(0.0); setTopOpacity(0.0);
setTimeout(() => { setTimeout(() => {
setCurrentImageIndex( setCurrentImageIndex((prevIndex) => (prevIndex + 1) % (data?.length ?? 1));
(prevIndex) => (prevIndex + 1) % (data?.length ?? 1)
);
setTopOpacity(1.0); setTopOpacity(1.0);
}, 1500); }, 1500);
}, 3000); }, 3000);
@ -45,7 +42,7 @@ export const Covers = ({
if (!data || data.length < 1) { if (!data || data.length < 1) {
if (iconEmpty) { if (iconEmpty) {
return <Icon children={iconEmpty} sizeIcon={size} />; return <Icon icon={iconEmpty} sizeIcon={size} />;
} else { } else {
return ( return (
<Box <Box
@ -63,50 +60,40 @@ 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 ( return <Image loading="lazy" src={url} maxWidth={size} boxSize={size} /*{...rest}*/ />;
<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 ( return <Flex
<Flex position="relative"
position="relative" // {...rest}
// {...rest} maxWidth={size}
maxWidth={size} width={size}
width={size} height={size}
height={size} overflow="hidden">
overflow="hidden" <Image
> src={urlPrevious}
<Image loading="lazy"
src={urlPrevious} position="absolute"
loading="lazy" top="0"
position="absolute" left="0"
top="0" width="100%"
left="0" height="100%"
width="100%" zIndex={1}
height="100%" boxSize={size}
zIndex={1} />
boxSize={size} <Image
/> src={urlCurrent}
<Image loading="lazy"
src={urlCurrent} position="absolute"
loading="lazy" top="0"
position="absolute" left="0"
top="0" width="100%"
left="0" height="100%"
width="100%" boxSize={size}
height="100%" transition="opacity 0.5s ease-in-out"
boxSize={size} opacity={topOpacity}
transition="opacity 0.5s ease-in-out" zIndex={2}
opacity={topOpacity} />
zIndex={2} </Flex>
/>
</Flex>
);
}; };

View File

@ -6,8 +6,8 @@ export const EmptyEnd = () => {
width="full" width="full"
height="25%" height="25%"
minHeight="250px" minHeight="250px"
// borderWidth="1px" // borderWidth="1px"
// borderColor="red" // borderColor="red"
></Box> ></Box>
); );
}; };

View File

@ -1,117 +0,0 @@
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,40 +1,41 @@
import { ReactNode, forwardRef } from 'react'; import {
Box,
import { Box, Flex, FlexProps } from '@chakra-ui/react'; Flex,
FlexProps,
} from '@chakra-ui/react';
import { forwardRef, ReactNode } from 'react';
export type IconProps = FlexProps & { export type IconProps = FlexProps & {
children: ReactNode; icon: ReactNode;
color?: string; color?: string;
sizeIcon?: FlexProps['width']; sizeIcon?: FlexProps['width'];
}; };
export const Icon = forwardRef<HTMLDivElement, IconProps>( export const Icon = forwardRef<HTMLDivElement, IconProps>(
({ children, color, sizeIcon = '1em', ...rest }, ref) => { ({ icon: IconEl, color, sizeIcon = '1em', ...rest }, ref) => {
return ( return (
<Flex <Flex flex="none"
flex="none" minWidth={sizeIcon}
minWidth={sizeIcon} minHeight={sizeIcon}
minHeight={sizeIcon} maxWidth={sizeIcon}
maxWidth={sizeIcon} maxHeight={sizeIcon}
maxHeight={sizeIcon} align="center"
align="center" padding="1px"
padding="1px" ref={ref}
ref={ref} {...rest}>
{...rest} <Box
> marginX="auto"
<Box width="100%"
marginX="auto" minWidth="100%"
width="100%" height="100%"
minWidth="100%" color={color}
height="100%" asChild
color={color} >
asChild {IconEl}
> </Box>
{children} </Flex>
</Box> );
</Flex> }
);
}
); );
Icon.displayName = 'Icon'; 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 ikon from '@/assets/images/ikon.svg'; import background 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={0} zIndex={-1}
> >
<Image src={ikon} boxSize="90vh" margin="auto" /> <Image src={background} boxSize="90%" margin="auto" opacity="30%" />
</Flex> </Flex>
<Flex <Flex
direction="column" direction="column"

View File

@ -1,11 +1,11 @@
import { ReactNode, useEffect } from 'react'; import React, { 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 { useColorModeValue } from '@/components/ui/color-mode';
import { colors } from '@/theme/colors'; import { colors } from '@/theme/colors';
import { useColorModeValue } from '@/components/ui/color-mode';
export type LayoutProps = FlexProps & { export type LayoutProps = FlexProps & {
children: ReactNode; children: ReactNode;

View File

@ -1,20 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,17 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,24 +0,0 @@
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

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

View File

@ -1,6 +1,9 @@
import { useState } from 'react'; import { useState } from 'react';
import { Group, Input } from '@chakra-ui/react'; import {
Group,
Input,
} from '@chakra-ui/react';
import { MdSearch } from 'react-icons/md'; import { MdSearch } from 'react-icons/md';
export type SearchInputProps = { export type SearchInputProps = {

View File

@ -2,14 +2,14 @@ import { ReactNode } from 'react';
import { import {
Box, Box,
Button,
ConditionalValue,
Flex, Flex,
HStack, HStack,
IconButton, IconButton,
Span,
Text, Text,
useDisclosure, useDisclosure,
Button,
ConditionalValue,
Span,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { import {
LuAlignJustify, LuAlignJustify,
@ -21,16 +21,15 @@ import {
LuSettings, LuSettings,
LuSun, LuSun,
} from 'react-icons/lu'; } from 'react-icons/lu';
import {
MdHelp,
MdHome,
MdMore,
MdOutlinePlaylistPlay,
MdOutlineUploadFile,
MdSupervisedUserCircle,
} from 'react-icons/md';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionState } from '@/service/SessionState';
import { colors } from '@/theme/colors';
import { requestOpenSite, requestSignIn, requestSignOut, requestSignUp } from '@/utils/sso';
import { useSessionService } from '@/service/session';
import { MdHelp, MdHome, MdMore, MdOutlinePlaylistPlay, MdOutlineUploadFile, MdSupervisedUserCircle } from 'react-icons/md';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '@/components/ui/menu';
import { useColorMode, useColorModeValue } from '@/components/ui/color-mode'; import { useColorMode, useColorModeValue } from '@/components/ui/color-mode';
import { import {
DrawerBody, DrawerBody,
@ -38,29 +37,11 @@ import {
DrawerHeader, DrawerHeader,
DrawerRoot, DrawerRoot,
} from '@/components/ui/drawer'; } from '@/components/ui/drawer';
import {
MenuContent,
MenuItem,
MenuRoot,
MenuTrigger,
} from '@/components/ui/menu';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionState } from '@/service/SessionState';
import { useSessionService } from '@/service/session';
import { colors } from '@/theme/colors';
import {
requestOpenSite,
requestSignIn,
requestSignOut,
requestSignUp,
} from '@/utils/sso';
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: 'ghost' as ConditionalValue< variant: "ghost" as ConditionalValue<"ghost" | "outline" | "solid" | "subtle" | "surface" | "plain" | undefined>,
'ghost' | 'outline' | 'solid' | 'subtle' | 'surface' | 'plain' | undefined
>,
//colorPalette: "brand", //colorPalette: "brand",
fontSize: '20px', fontSize: '20px',
textTransform: 'uppercase', textTransform: 'uppercase',
@ -72,41 +53,6 @@ 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 { colorMode, toggleColorMode } = useColorMode(); const { colorMode, toggleColorMode } = useColorMode();
const { clearToken } = useSessionService(); const { clearToken } = useSessionService();
@ -133,6 +79,21 @@ export const TopBar = ({ title, children }: TopBarProps) => {
const onKarso = (): void => { const onKarso = (): void => {
requestOpenSite(); requestOpenSite();
}; };
const onSelectAdd = () => {
navigate('/add');
};
const onSelectHome = () => {
navigate('/');
};
const onSelectOnAir = () => {
navigate('/on-air');
};
const onHelp = () => {
navigate('/help');
};
const onSettings = () => {
navigate('/settings');
};
return ( return (
<Flex <Flex
position="absolute" position="absolute"
@ -194,60 +155,31 @@ export const TopBar = ({ title, children }: TopBarProps) => {
<MenuRoot> <MenuRoot>
<MenuTrigger asChild> <MenuTrigger asChild>
<IconButton <IconButton
asChild
aria-label="Options"
{...BUTTON_TOP_BAR_PROPERTY} {...BUTTON_TOP_BAR_PROPERTY}
width={TOP_BAR_HEIGHT} width={TOP_BAR_HEIGHT}
> ><MdSupervisedUserCircle /></IconButton>
<LuCircleUserRound />
</IconButton>
</MenuTrigger> </MenuTrigger>
<MenuContent> <MenuContent>
<MenuItem <MenuItem value="user" valueText="user" color={useColorModeValue('brand.800', 'brand.200')}>
value="user"
valueText="user"
color={useColorModeValue('brand.800', 'brand.200')}
>
<MdSupervisedUserCircle /> <MdSupervisedUserCircle />
<Box flex="1">Sign in as {session?.login ?? 'Fail'}</Box> <Box flex="1">Sign in as {session?.login ?? 'Fail'}</Box>
</MenuItem> </MenuItem>
<MenuItem <MenuItem value="Settings" valueText="Settings" onClick={onSettings}><LuSettings />Settings</MenuItem>
value="Settings" <MenuItem value="Help" valueText="Help" onClick={onHelp}><MdHelp /> Help</MenuItem>
valueText="Settings" <MenuItem value="Sign-out" valueText="Sign-out" onClick={onSignOut}>
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 <LuLogOut /> Sign-out
</MenuItem> </MenuItem>
<MenuItem value="karso" valueText="Karso" onClick={onKarso}> <MenuItem value="karso" valueText="Karso" onClick={onKarso}>
<LuKeySquare /> Karso (SSO) <LuKeySquare /> Karso (SSO)
</MenuItem> </MenuItem>
{colorMode === 'light' ? ( {colorMode === 'light' ? (
<MenuItem <MenuItem value="set-dark" valueText="set-dark" onClick={toggleColorMode}>
value="set-dark"
valueText="set-dark"
onClick={toggleColorMode}
>
<LuMoon /> Set dark mode <LuMoon /> Set dark mode
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem <MenuItem value="set-light" valueText="set-light" onClick={toggleColorMode}>
value="set-light"
valueText="set-light"
onClick={toggleColorMode}
>
<LuSun /> Set light mode <LuSun /> Set light mode
</MenuItem> </MenuItem>
)} )}
@ -261,7 +193,8 @@ export const TopBar = ({ title, children }: TopBarProps) => {
open={drawerDisclose.open} open={drawerDisclose.open}
data-testid="top-bar_drawer-root" data-testid="top-bar_drawer-root"
> >
<DrawerContent data-testid="top-bar_drawer-content"> <DrawerContent
data-testid="top-bar_drawer-content">
<DrawerHeader <DrawerHeader
paddingY="auto" paddingY="auto"
as="button" as="button"
@ -271,31 +204,54 @@ export const TopBar = ({ title, children }: TopBarProps) => {
color={useColorModeValue('brand.900', 'brand.50')} color={useColorModeValue('brand.900', 'brand.50')}
textTransform="uppercase" textTransform="uppercase"
> >
<HStack {...BUTTON_TOP_BAR_PROPERTY} cursor="pointer"> <HStack
{...BUTTON_TOP_BAR_PROPERTY} cursor="pointer">
<LuArrowBigLeft /> <LuArrowBigLeft />
<Span paddingLeft="3px">Karusic</Span> <Span paddingLeft="3px">
Karusic
</Span>
</HStack> </HStack>
</DrawerHeader> </DrawerHeader>
<DrawerBody paddingX="0px"> <DrawerBody paddingX="0px">
<Box marginY="3" /> <Box marginY="3" />
<ButtonMenuLeft <Button
onClickEnd={drawerDisclose.onClose} background="#00000000"
dest="/" borderRadius="0px"
title="Home" onClick={onSelectHome}
icon={<MdHome />} width="full"
/> {...BUTTON_TOP_BAR_PROPERTY}
<ButtonMenuLeft >
onClickEnd={drawerDisclose.onClose} <MdHome style={{ width: "45px", height: "45px" }} />
dest="/on-air" <Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
title="On air" Home
icon={<MdOutlinePlaylistPlay />} </Text>
/> </Button>
<ButtonMenuLeft <Box marginY="5" marginX="10" height="2px" background="brand.600" />
onClickEnd={drawerDisclose.onClose} <Button
dest="/add" background="#00000000"
title="Add Media" borderRadius="0px"
icon={<MdOutlineUploadFile />} onClick={onSelectOnAir}
/> width="full"
{...BUTTON_TOP_BAR_PROPERTY}
>
<MdOutlinePlaylistPlay style={{ width: "45px", height: "45px" }} />
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
On air
</Text>
</Button>
<Box marginY="5" marginX="10" height="2px" background="brand.600" />
<Button
background="#00000000"
borderRadius="0px"
onClick={onSelectAdd}
width="full"
{...BUTTON_TOP_BAR_PROPERTY}
>
<MdOutlineUploadFile style={{ width: "45px", height: "45px" }} />
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
Add Media
</Text>
</Button>
</DrawerBody> </DrawerBody>
</DrawerContent> </DrawerContent>
</DrawerRoot> </DrawerRoot>

View File

@ -1,10 +1,10 @@
import { Flex, Span } from '@chakra-ui/react'; import { Flex, Span, Text } 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 { BASE_WRAP_ICON_SIZE } from '@/constants/genericSpacing';
import { useCountTracksWithAlbumId } from '@/service/Track'; import { useCountTracksWithAlbumId } from '@/service/Track';
import { BASE_WRAP_ICON_SIZE } from '@/constants/genericSpacing';
export type DisplayAlbumProps = { export type DisplayAlbumProps = {
dataAlbum?: Album; dataAlbum?: Album;
@ -19,12 +19,8 @@ export const DisplayAlbum = ({ dataAlbum }: DisplayAlbumProps) => {
); );
} }
return ( return (
<Flex <Flex direction="row" width="full" height="full"
direction="row" data-testid="display-album_flex">
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}
@ -47,7 +43,7 @@ export const DisplayAlbum = ({ dataAlbum }: DisplayAlbumProps) => {
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
// noOfLines={[1, 2]} // noOfLines={[1, 2]}
> >
{dataAlbum?.name} {dataAlbum?.name}
</Span> </Span>
@ -57,7 +53,7 @@ export const DisplayAlbum = ({ dataAlbum }: DisplayAlbumProps) => {
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'}
</Span> </Span>

View File

@ -6,5 +6,6 @@ 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} data-testid="display-album-id" />; return <DisplayAlbum dataAlbum={dataAlbum}
data-testid="display-album-id" />;
}; };

View File

@ -1,9 +1,9 @@
import { ReactNode } from 'react'; import { ReactNode, useState } from 'react';
import { LuMenu } from 'react-icons/lu'; import { LuMenu } from 'react-icons/lu';
import { Button } from '../ui/button';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '../ui/menu'; import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '../ui/menu';
import { Button } from '../ui/button';
export type MenuElement = { export type MenuElement = {
icon?: ReactNode; icon?: ReactNode;
@ -20,33 +20,27 @@ export const ContextMenu = ({ elements }: ContextMenuProps) => {
return <></>; return <></>;
} }
return ( return (
<MenuRoot data-testid="context-menu"> <MenuRoot
<MenuTrigger data-testid="context-menu">
asChild <MenuTrigger asChild
marginY="auto" marginY="auto"
marginRight="4px" marginRight="4px"
data-testid="context-menu_trigger" data-testid="context-menu_trigger">
>
{/* This is very stupid, we need to set as span to prevent a button in button... WTF */} {/* This is very stupid, we need to set as span to prevent a button in button... WTF */}
<Button variant="ghost" color="brand.500"> <Button variant="ghost" color="brand.500">
<LuMenu /> <LuMenu />
</Button> </Button>
</MenuTrigger> </MenuTrigger>
<MenuContent data-testid="context-menu_content"> <MenuContent
data-testid="context-menu_content">
{elements?.map((data) => ( {elements?.map((data) => (
<MenuItem <MenuItem key={data.name} value={data.name} onClick={data.onClick} height="65px" fontSize="25px"
key={data.name} data-test-id="context-menu_item" >
value={data.name}
onClick={data.onClick}
height="65px"
fontSize="25px"
data-test-id="context-menu_item"
>
{data.icon} {data.icon}
{data.name} {data.name}
</MenuItem> </MenuItem>
))} ))}
</MenuContent> </MenuContent>
</MenuRoot> </MenuRoot >
); );
}; };

View File

@ -1,13 +1,25 @@
import { DragEventHandler, ReactNode, RefObject } from 'react'; import {
DragEventHandler,
RefObject,
} from 'react';
import { Box, BoxProps, Center, Flex, HStack, Image } from '@chakra-ui/react'; import {
import { MdHighlightOff, MdUploadFile } from 'react-icons/md'; Box,
BoxProps,
Center,
Flex,
HStack,
Image,
} 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;
@ -16,8 +28,8 @@ export type DragNdropProps = {
}; };
export const DragNdrop = ({ export const DragNdrop = ({
onFilesSelected = () => {}, onFilesSelected = () => { },
onUriSelected = () => {}, onUriSelected = () => { },
width = '100px', width = '100px',
height = '100px', height = '100px',
}: DragNdropProps) => { }: DragNdropProps) => {
@ -80,12 +92,12 @@ export const DragNdrop = ({
}; };
export type CenterIconProps = BoxProps & { export type CenterIconProps = BoxProps & {
children: ReactNode; icon: any;
sizeIcon?: string; sizeIcon?: string;
}; };
export const CenterIcon = ({ export const CenterIcon = ({
children, icon: IconEl,
sizeIcon = '15px', sizeIcon = '15px',
...rest ...rest
}: CenterIconProps) => { }: CenterIconProps) => {
@ -98,15 +110,14 @@ export const CenterIcon = ({
top="50%" top="50%"
left="50%" left="50%"
transform="translate(-50%, -50%)" transform="translate(-50%, -50%)"
> >{IconEl}</Box>
{children}
</Box>
</Box> </Box>
); );
}; };
export type FormCoversProps = { export type FormCoversProps = {
name: string; form: UseFormidableReturn;
variableName: string;
ref?: RefObject<any>; ref?: RefObject<any>;
label?: string; label?: string;
isRequired?: boolean; isRequired?: boolean;
@ -115,19 +126,23 @@ 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 = ({
name, form,
variableName,
ref, ref,
onFilesSelected = () => {}, onFilesSelected = () => { },
onUriSelected = () => {}, onUriSelected = () => { },
onRemove = () => {}, onRemove = () => { },
...rest ...rest
}: FormCoversProps) => { }: FormCoversProps) => {
const { value } = useFormidableContextElement(name); const urls =
const urls = DataUrlAccess.getListThumbnailUrl(value) ?? []; DataUrlAccess.getListThumbnailUrl(form.values[variableName]) ?? [];
return ( return (
<FormGroup name={name} {...rest}> <FormGroup
isModify={form.isModify[variableName]}
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<HStack wrap="wrap" width="full"> <HStack wrap="wrap" width="full">
{urls.map((data, index) => ( {urls.map((data, index) => (
<Flex align="flex-start" key={data}> <Flex align="flex-start" key={data}>
@ -140,9 +155,7 @@ export const FormCovers = ({
color="#00000020" color="#00000020"
_hover={{ color: 'red' }} _hover={{ color: 'red' }}
onClick={() => onRemove && onRemove(index)} onClick={() => onRemove && onRemove(index)}
> ><MdHighlightOff /></CenterIcon>
<MdHighlightOff />
</CenterIcon>
</Box> </Box>
<Image loading="lazy" src={data} boxSize="full" /> <Image loading="lazy" src={data} boxSize="full" />
</Box> </Box>

View File

@ -1,128 +1,62 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Box, 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';
import { useFormidableContextElement } from '../formidable';
const DisplayLabel = ({
label,
isRequired,
}: {
label?: ReactNode;
isRequired: boolean;
}) => {
if (!label) {
return <></>;
}
return (
<Text marginRight="auto" fontWeight="bold">
{label}{' '}
{isRequired && (
<Text as="span" color="red.600">
*
</Text>
)}
</Text>
);
};
const DisplayHelp = ({ help }: { help?: ReactNode }) => {
if (!help) {
return <></>;
}
return (
<Flex direction="row">
<MdHelpOutline />
<Text alignContent="center">{help}</Text>
</Flex>
);
};
const DisplayError = ({ error }: { error?: ReactNode }) => {
if (!error) {
return <></>;
}
return (
<Flex direction="row" color="red.600">
<MdErrorOutline />
<Text alignContent="center">{error}</Text>
</Flex>
);
};
export type FormGroupProps = { export type FormGroupProps = {
children: ReactNode;
name: string;
error?: ReactNode; error?: ReactNode;
help?: ReactNode; help?: ReactNode;
label?: ReactNode; label?: ReactNode;
isModify?: boolean;
onRestore?: () => void;
isRequired?: boolean; isRequired?: boolean;
disableSingleLine?: boolean; children: ReactNode;
}; };
export const FormGroup = ({ export const FormGroup = ({
children, children,
name, error,
help, help,
label, label,
isModify = false,
isRequired = false, isRequired = false,
disableSingleLine, onRestore,
}: FormGroupProps) => { }: FormGroupProps) => (
const { form, error, isModify, onRestore } = <Flex
useFormidableContextElement(name); borderLeftWidth="3px"
const enableModifyNotification = borderLeftColor={error ? 'red' : isModify ? 'blue' : '#00000000'}
form.configuration.enableModifyNotification ?? false; paddingLeft="7px"
const enableReset = form.configuration.enableReset ?? false; paddingY="4px"
const singleLine = disableSingleLine direction="column"
? false >
: form.configuration.singleLineForm; <Flex direction="row" width="full" gap="52px">
return ( {!!label && (
<Flex <Text marginRight="auto" fontWeight="bold">
borderLeftWidth="3px" {label}{' '}
borderLeftColor={ {isRequired && (
error <Text as="span" color="red.600">
? 'red.600' *
: enableModifyNotification && isModify </Text>
? 'blue.600' )}
: '#00000000' </Text>
}
paddingLeft="7px"
paddingY="4px"
width="full"
direction="column"
>
{singleLine && (
<>
<Flex direction="row" width="full" gap="52px">
<Box width="10%">
<DisplayLabel label={label} isRequired={isRequired} />
{!!onRestore && isModify && enableReset && (
<MdRefresh size="15px" onClick={onRestore} cursor="pointer" />
)}
</Box>
<Flex direction="column" width={'90%'} gap="5px">
{children}
<DisplayHelp help={help} />
<DisplayError error={error} />
</Flex>
</Flex>
</>
)} )}
{!singleLine && ( {!!onRestore && isModify && (
<> <MdRefresh size="15px" onClick={onRestore} cursor="pointer" />
<Flex direction="row" width="full" gap="52px">
<Box width="full">
<DisplayLabel label={label} isRequired={isRequired} />
{!!onRestore && isModify && enableReset && (
<MdRefresh size="15px" onClick={onRestore} cursor="pointer" />
)}
</Box>
</Flex>
{children}
<DisplayHelp help={help} />
<DisplayError error={error} />
</>
)} )}
</Flex> </Flex>
); {children}
}; {!!help && (
<Flex direction="row">
<MdHelpOutline />
<Text>{help}</Text>
</Flex>
)}
{!!error && (
<Flex direction="row">
<MdErrorOutline />
<Text>{error}</Text>
</Flex>
)}
</Flex>
);

View File

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

View File

@ -1,19 +1,15 @@
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 { useFormidableContextElement } from '../formidable'; import { NumberInputField, NumberInputProps, NumberInputRoot } from '../ui/number-input';
import {
NumberInputField,
NumberInputProps,
NumberInputRoot,
} from '../ui/number-input';
export type FormNumberProps = Pick< export type FormNumberProps = Pick<
NumberInputProps, NumberInputProps,
'step' | 'defaultValue' | 'min' | 'max' 'step' | 'defaultValue' | 'min' | 'max'
> & { > & {
name: string; form: UseFormidableReturn;
variableName: string;
ref?: RefObject<any>; ref?: RefObject<any>;
label?: string; label?: string;
placeholder?: string; placeholder?: string;
@ -21,7 +17,8 @@ export type FormNumberProps = Pick<
}; };
export const FormNumber = ({ export const FormNumber = ({
name, form,
variableName,
ref, ref,
placeholder, placeholder,
step, step,
@ -30,14 +27,19 @@ export const FormNumber = ({
defaultValue, defaultValue,
...rest ...rest
}: FormNumberProps) => { }: FormNumberProps) => {
const { form, value, isModify, onChange, onRestore } = const onEvent = (value) => {
useFormidableContextElement(name); form.setValues({ [variableName]: value.value });
}
return ( return (
<FormGroup name={name} {...rest}> <FormGroup
isModify={form.isModify[variableName]}
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<NumberInputRoot <NumberInputRoot
ref={ref} ref={ref}
value={value} value={form.values[variableName]}
onValueChange={(e) => onChange(e.value)} onValueChange={onEvent}
step={step} step={step}
defaultValue={defaultValue} defaultValue={defaultValue}
min={min} min={min}

View File

@ -1,50 +0,0 @@
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,9 +3,7 @@ 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/formidable/FormidableConfig'; import { useFormidable } from '@/components/form/Formidable';
import { Formidable } from '../formidable';
export default { export default {
title: 'Components/FormSelect', title: 'Components/FormSelect',
@ -18,49 +16,46 @@ 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={[
{ key: 111, plop: 'first Item' }, { key: 111, plop: 'first Item' },
{ key: 222, plop: 'Second Item' }, { key: 222, plop: 'Second Item' },
{ 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 = () => {
@ -71,28 +66,27 @@ 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;
setData((previous) => { setData((previous) => {
previous.forEach((element) => { previous.forEach((element) => {
if (element['id'] > upperId) { if (element['id'] > upperId) {
upperId = element['id']; upperId = element['id'];
} }
});
upperId++;
return [...previous, { id: upperId, name: data }];
}); });
resolve({ id: upperId, name: data }); upperId++;
return [...previous, { id: upperId, name: data }];
}); });
}} resolve({ id: upperId, name: data });
options={data} });
/> }}
</Formidable.From> options={data}
/>
); );
}; };
@ -100,19 +94,18 @@ 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' },
{ id: 333, name: 'third item' }, { id: 333, name: 'third item' },
]} ]}
/> />
</Box> </Box>
</Formidable.From>
); );
}, },

View File

@ -1,13 +1,16 @@
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
name: string; variableName: string;
// Forward object reference // Forward object reference
ref?: RefObject<any>; ref?: RefObject<any>;
// Form: Label of the input // Form: Label of the input
@ -29,7 +32,8 @@ export type FormSelectProps = {
}; };
export const FormSelect = ({ export const FormSelect = ({
name, form,
variableName,
ref, ref,
placeholder, placeholder,
options, options,
@ -39,23 +43,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) => addNewItem(data).then((data: object) => form.setValues({ [variableName]: data[keyInputKey] }));
form.setValues({ [name]: data[keyInputKey] }) };
);
};
return ( return (
<FormGroup name={name} {...rest}> <FormGroup
isModify={form.isModify[variableName]}
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<SelectSingle <SelectSingle
ref={ref} ref={ref}
value={value} value={form.values[variableName]}
options={options} options={options}
onChange={(value) => onChange(value)} onChange={(value) => form.setValues({ [variableName]: value })}
keyKey={keyInputKey} keyKey={keyInputKey}
keyValue={keyInputValue} keyValue={keyInputValue}
onCreate={onCreate} onCreate={onCreate}

View File

@ -3,9 +3,7 @@ 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/formidable/FormidableConfig'; import { useFormidable } from '@/components/form/Formidable';
import { Formidable } from '../formidable';
export default { export default {
title: 'Components/FormSelectMultipleMultiple', title: 'Components/FormSelectMultipleMultiple',
@ -18,49 +16,46 @@ 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={[
{ key: 111, plop: 'first Item' }, { key: 111, plop: 'first Item' },
{ key: 222, plop: 'Second Item' }, { key: 222, plop: 'Second Item' },
{ 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 = () => {
@ -71,28 +66,27 @@ 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;
setData((previous) => { setData((previous) => {
previous.forEach((element) => { previous.forEach((element) => {
if (element['id'] > upperId) { if (element['id'] > upperId) {
upperId = element['id']; upperId = element['id'];
} }
});
upperId++;
return [...previous, { id: upperId, name: data }];
}); });
resolve({ id: upperId, name: data }); upperId++;
return [...previous, { id: upperId, name: data }];
}); });
}} resolve({ id: upperId, name: data });
options={data} });
/> }}
</Formidable.From> options={data}
/>
); );
}; };
@ -100,19 +94,18 @@ 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' },
{ id: 333, name: 'third item' }, { id: 333, name: 'third item' },
]} ]}
/> />
</Box> </Box>
</Formidable.From>
); );
}, },

View File

@ -1,16 +1,14 @@
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
name: string; variableName: string;
// Forward object reference // Forward object reference
ref?: RefObject<any>; ref?: RefObject<any>;
// Form: Label of the input // Form: Label of the input
@ -30,7 +28,8 @@ export type FormSelectMultipleProps = {
}; };
export const FormSelectMultiple = ({ export const FormSelectMultiple = ({
name, form,
variableName,
ref, ref,
placeholder, placeholder,
options, options,
@ -39,25 +38,23 @@ 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) => addNewItem(data).then((data: object) => form.setValues({ [variableName]: [...(form.values[variableName] ?? []), data[keyInputKey]] }));
form.setValues({ };
[name]: [...(form.values[name] ?? []), data[keyInputKey]],
})
);
};
return ( return (
<FormGroup name={name} {...rest}> <FormGroup
isModify={form.isModify[variableName]}
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<SelectMultiple <SelectMultiple
//ref={ref} //ref={ref}
values={value} values={form.values[variableName]}
options={options} options={options}
onChange={(value) => onChange(value)} onChange={(value) => form.setValues({ [variableName]: 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 = {
name: string; form: UseFormidableReturn;
variableName: string;
ref?: RefObject<any>; ref?: RefObject<any>;
label?: string; label?: string;
placeholder?: string; placeholder?: string;
@ -15,21 +15,22 @@ export type FormTextareaProps = {
}; };
export const FormTextarea = ({ export const FormTextarea = ({
name, form,
variableName,
ref, ref,
placeholder, placeholder,
...rest ...rest
}: FormTextareaProps) => { }: FormTextareaProps) => {
const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
return ( return (
<FormGroup name={name} {...rest}> <FormGroup
isModify={form.isModify[variableName]}
onRestore={() => form.restoreValue({ [variableName]: true })}
{...rest}
>
<Textarea <Textarea
name={name}
ref={ref} ref={ref}
autoComplete={name} value={form.values[variableName]}
value={value} onChange={(e) => form.setValues({ [variableName]: e.target.value })}
onChange={(e) => onChange(e.target.value)}
/> />
</FormGroup> </FormGroup>
); );

View File

@ -1,39 +1,66 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { z as zod } from 'zod';
import { isArray, isNullOrUndefined, isObject } from '@/utils/validator'; import { isArray, isNullOrUndefined, isObject } from '@/utils/validator';
import { getDifferences, hasAnyTrue } from './utils'; const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => {
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] === true) {
return true;
}
}
return false;
};
export type FormidableConfig = { function getDifferences(
enableReset?: boolean; obj1: object,
enableModifyNotification?: boolean; obj2: object
singleLineForm?: boolean; ): { [key: string]: boolean } {
}; // Create an empty object to store the differences
const initialFormConfig: Required<FormidableConfig> = { const result: { [key: string]: boolean } = {};
enableReset: true, // Recursive function to compare values
enableModifyNotification: true, function compareValues(value1: any, value2: any): boolean {
singleLineForm: false, // 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 useFormidable = <TYPE extends object = object>({ export const useFormidable = <TYPE extends object = object>({
initialValues = {} as TYPE, initialValues = {} as TYPE,
configuration: inputConfiguration = initialFormConfig,
resolver = (_data: TYPE) => {
return {};
},
}: { }: {
initialValues?: TYPE; initialValues?: TYPE;
configuration?: FormidableConfig;
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);
@ -70,11 +97,10 @@ 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, setErrors, setIsFormModified, setIsModify] [setValues, initialData]
); );
const restoreValue = useCallback( const restoreValue = useCallback(
(data: object) => { (data: object) => {
@ -107,7 +133,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 }: { omit?: string[]; only?: string[] }) => {
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))) {
@ -135,8 +161,6 @@ export const useFormidable = <TYPE extends object = object>({
restoreValue, restoreValue,
setValues: setValuesExternal, setValues: setValuesExternal,
values, values,
errors,
configuration,
}; };
}; };

View File

@ -1,90 +0,0 @@
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,
},
},
});
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(() => {
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

@ -1,43 +0,0 @@
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());
}
}
};
return (
<FormidableContext form={form}>
<Box as="form" onSubmit={handleSubmit}>
{children}
</Box>
</FormidableContext>
);
};

View File

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

View File

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

View File

@ -1,87 +0,0 @@
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

@ -41,7 +41,7 @@ export const DisplayGender = ({ dataGender }: DisplayGenderProps) => {
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
//TODO: noOfLines={[1, 2]} //TODO: noOfLines={[1, 2]}
> >
{dataGender?.name} {dataGender?.name}
</Text> </Text>
@ -52,7 +52,7 @@ export const DisplayGender = ({ dataGender }: DisplayGenderProps) => {
userSelect="none" userSelect="none"
marginRight="auto" marginRight="auto"
overflow="hidden" overflow="hidden"
//TODO: noOfLines={1} //TODO: noOfLines={1}
> >
{countTracksOnAGender} track{countTracksOnAGender >= 1 && 's'} {countTracksOnAGender} track{countTracksOnAGender >= 1 && 's'}
</Text> </Text>

View File

@ -1,6 +1,10 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { Flex, Text, useDisclosure } from '@chakra-ui/react'; import {
Flex,
Text,
useDisclosure,
} from '@chakra-ui/react';
import { import {
MdAdminPanelSettings, MdAdminPanelSettings,
MdDeleteForever, MdDeleteForever,
@ -16,23 +20,17 @@ 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 { 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 { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
export type AlbumEditPopUpProps = {}; export type AlbumEditPopUpProps = {};
export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => { export const AlbumEditPopUp = ({ }: AlbumEditPopUpProps) => {
const { albumId } = useParams(); const { albumId } = useParams();
const albumIdInt = isNullOrUndefined(albumId) const albumIdInt = isNullOrUndefined(albumId)
? undefined ? undefined

View File

@ -1,6 +1,12 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { Button, Flex, Text, useDisclosure } from '@chakra-ui/react'; import {
Flex,
Text,
useDisclosure,
Button,
} from '@chakra-ui/react';
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
import { import {
MdAdminPanelSettings, MdAdminPanelSettings,
MdDeleteForever, MdDeleteForever,
@ -16,13 +22,6 @@ 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 { 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';
@ -30,7 +29,7 @@ import { isNullOrUndefined } from '@/utils/validator';
export type ArtistEditPopUpProps = {}; export type ArtistEditPopUpProps = {};
export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => { export const ArtistEditPopUp = ({ }: ArtistEditPopUpProps) => {
const { artistId } = useParams(); const { artistId } = useParams();
const artistIdInt = isNullOrUndefined(artistId) const artistIdInt = isNullOrUndefined(artistId)
? undefined ? undefined
@ -218,7 +217,7 @@ export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => {
<Button <Button
onClick={() => setAdmin((value) => !value)} onClick={() => setAdmin((value) => !value)}
marginRight="auto" marginRight="auto"
colorPalette={admin ? undefined : '@danger'} colorPalette={admin ? undefined : "@danger"}
> >
{admin ? ( {admin ? (
<> <>

View File

@ -1,15 +1,11 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { Button, UseDisclosureReturn } from '@chakra-ui/react';
import { import {
DialogBody, Button,
DialogContent, UseDisclosureReturn,
DialogFooter, } from '@chakra-ui/react';
DialogHeader,
DialogRoot,
} from '@/components/ui/dialog';
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
export type ConfirmPopUpProps = { export type ConfirmPopUpProps = {
title: string; title: string;
body: string; body: string;
@ -31,8 +27,7 @@ export const ConfirmPopUp = ({
}; };
const cancelRef = useRef<HTMLButtonElement>(null); const cancelRef = useRef<HTMLButtonElement>(null);
return ( return (
<DialogRoot <DialogRoot role="alertdialog"
role="alertdialog"
open={disclosure.open} open={disclosure.open}
//leastDestructiveRef={cancelRef} //leastDestructiveRef={cancelRef}
onOpenChange={disclosure.onClose} onOpenChange={disclosure.onClose}

View File

@ -1,12 +1,19 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { Button, Flex, Text, useDisclosure } from '@chakra-ui/react'; import {
Flex,
Text,
useDisclosure,
Button
} from '@chakra-ui/react';
import { import {
MdAdminPanelSettings, MdAdminPanelSettings,
MdDeleteForever, MdDeleteForever,
MdEdit, MdEdit,
MdWarning, MdWarning,
} from 'react-icons/md'; } from 'react-icons/md';
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Gender, GenderResource } from '@/back-api'; import { Gender, GenderResource } from '@/back-api';
@ -16,13 +23,6 @@ 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 { 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';
@ -30,7 +30,7 @@ import { isNullOrUndefined } from '@/utils/validator';
export type GenderEditPopUpProps = {}; export type GenderEditPopUpProps = {};
export const GenderEditPopUp = ({}: GenderEditPopUpProps) => { export const GenderEditPopUp = ({ }: GenderEditPopUpProps) => {
const { genderId } = useParams(); const { genderId } = useParams();
const genderIdInt = isNullOrUndefined(genderId) const genderIdInt = isNullOrUndefined(genderId)
? undefined ? undefined

View File

@ -1,14 +1,14 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { Button, Flex, Progress, Text } from '@chakra-ui/react';
import { import {
DialogBody, Flex,
DialogContent, Progress,
DialogFooter, Text,
DialogHeader, Button,
DialogRoot, } from '@chakra-ui/react';
} from '@/components/ui/dialog';
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
export type PopUpUploadProgressProps = { export type PopUpUploadProgressProps = {
title: string; title: string;

View File

@ -1,6 +1,13 @@
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { Button, Text, useDisclosure } from '@chakra-ui/react'; import {
Text,
useDisclosure,
Button,
} from '@chakra-ui/react';
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
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';
@ -13,13 +20,6 @@ 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 { 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';
@ -29,7 +29,7 @@ import { isNullOrUndefined } from '@/utils/validator';
export type TrackEditPopUpProps = {}; export type TrackEditPopUpProps = {};
export const TrackEditPopUp = ({}: TrackEditPopUpProps) => { export const TrackEditPopUp = ({ }: TrackEditPopUpProps) => {
const { trackId } = useParams(); const { trackId } = useParams();
const trackIdInt = isNullOrUndefined(trackId) const trackIdInt = isNullOrUndefined(trackId)
? undefined ? undefined

View File

@ -1,6 +1,14 @@
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'; import { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { Button, Flex, HStack, Input, Spinner, Tag } from '@chakra-ui/react'; import {
Button,
Flex,
HStack,
Input,
Spinner,
Tag,
TagLabel,
} 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';
@ -94,20 +102,14 @@ export const SelectMultiple = ({
const createNewItem = !onCreate const createNewItem = !onCreate
? undefined ? undefined
: (data: string) => { : (data: string) => {
onCreate(data); onCreate(data);
setCurrentSearch(undefined); setCurrentSearch(undefined);
}; };
return ( return (
<Flex direction="column" width="full" gap="0px"> <Flex direction="column" width="full" gap="0px">
{selectedOptions && ( {selectedOptions && (
<HStack <HStack wrap="wrap" gap="5px" justify="left" width="full" marginBottom="2px">
wrap="wrap"
gap="5px"
justify="left"
width="full"
marginBottom="2px"
>
{selectedOptions.map((data) => ( {selectedOptions.map((data) => (
<Flex align="flex-start" key={data[keyKey]}> <Flex align="flex-start" key={data[keyKey]}>
<Tag.Root <Tag.Root
@ -117,10 +119,7 @@ export const SelectMultiple = ({
backgroundColor="green.800" backgroundColor="green.800"
> >
<Tag.Label>{data[keyValue] ?? `id=${data[keyKey]}`}</Tag.Label> <Tag.Label>{data[keyValue] ?? `id=${data[keyKey]}`}</Tag.Label>
<Tag.CloseTrigger <Tag.CloseTrigger boxSize="5" onClick={() => selectValue(data)} />
boxSize="5"
onClick={() => selectValue(data)}
/>
</Tag.Root> </Tag.Root>
</Flex> </Flex>
))} ))}
@ -135,13 +134,7 @@ export const SelectMultiple = ({
//onSubmit={onSubmit} //onSubmit={onSubmit}
onFocus={() => setShowList(true)} onFocus={() => setShowList(true)}
onBlur={() => setTimeout(() => setShowList(false), 200)} onBlur={() => setTimeout(() => setShowList(false), 200)}
value={ value={showList ? (currentSearch ?? '') : hasSuggestion ? `suggest: ${currentSearch}` : ''}
showList
? (currentSearch ?? '')
: hasSuggestion
? `suggest: ${currentSearch}`
: ''
}
borderRadius="5px 0 0 5px" borderRadius="5px 0 0 5px"
/> />
<Button <Button

View File

@ -50,9 +50,7 @@ export const SelectSingle = ({
onCreate ? suggestion : undefined onCreate ? suggestion : undefined
); );
useEffect(() => { useEffect(() => {
console.log( console.log(`Update suggestion : ${onCreate} ${suggestion} ==> ${onCreate ? suggestion : undefined} .. ${onCreate && !isNullOrUndefined(suggestion) ? true : false}`);
`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]);
@ -97,10 +95,10 @@ export const SelectSingle = ({
const createNewItem = !onCreate const createNewItem = !onCreate
? undefined ? undefined
: (data: string) => { : (data: string) => {
onCreate(data); onCreate(data);
setCurrentSearch(undefined); setCurrentSearch(undefined);
setHasSuggestion(false); setHasSuggestion(false);
}; };
return ( return (
<Flex direction="column" width="full" gap="0px"> <Flex direction="column" width="full" gap="0px">
@ -112,10 +110,7 @@ export const SelectSingle = ({
onFocus={() => setShowList(true)} onFocus={() => setShowList(true)}
onBlur={() => setTimeout(() => setShowList(false), 200)} onBlur={() => setTimeout(() => setShowList(false), 200)}
value={ value={
showList showList ? (currentSearch ?? '') : (selectedOptions?.name ?? (hasSuggestion ? `suggest: ${currentSearch}` : ''))
? (currentSearch ?? '')
: (selectedOptions?.name ??
(hasSuggestion ? `suggest: ${currentSearch}` : ''))
} }
backgroundColor={ backgroundColor={
showList || !selectedOptions ? undefined : 'green.800' showList || !selectedOptions ? undefined : 'green.800'

View File

@ -23,7 +23,9 @@ export const DisplayTrack = ({
data={track?.covers} data={track?.covers}
size="50" size="50"
height="full" height="full"
iconEmpty={trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />} iconEmpty={
trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />
}
onClick={onClick} onClick={onClick}
/> />
<Flex <Flex

View File

@ -1,3 +1,5 @@
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';
@ -24,17 +26,15 @@ 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 <Flex direction="row" width="full" height="full"
direction="row" data-testid="display-track-full">
width="full"
height="full"
data-testid="display-track-full"
>
<Covers <Covers
data={track?.covers} data={track?.covers}
size="60px" size="60px"
marginY="auto" marginY="auto"
iconEmpty={trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />} iconEmpty={
trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />
}
onClick={onClick} onClick={onClick}
/> />
<Flex <Flex
@ -71,10 +71,7 @@ export const DisplayTrackFull = ({
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"> <Text as="span" fontWeight="normal">Album:</Text> {dataAlbum.name}
Album:
</Text>{' '}
{dataAlbum.name}
</Text> </Text>
)} )}
{dataArtists && ( {dataArtists && (
@ -90,10 +87,7 @@ export const DisplayTrackFull = ({
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"> <Text as="span" fontWeight="normal">Artist(s):</Text> {dataArtists.map((data) => data.name).join(', ')}
Artist(s):
</Text>{' '}
{dataArtists.map((data) => data.name).join(', ')}
</Text> </Text>
)} )}
{dataGender && ( {dataGender && (
@ -109,17 +103,12 @@ export const DisplayTrackFull = ({
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"> <Text as="span" fontWeight="normal">Gender:</Text> {dataGender.name}
Gender:
</Text>{' '}
{dataGender.name}
</Text> </Text>
)} )}
</Flex> </Flex>
<ContextMenu <ContextMenu elements={contextMenu}
elements={contextMenu} data-testid="display-track-full_context-menu" />
data-testid="display-track-full_context-menu"
/>
</Flex> </Flex>
); );
}; };

View File

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

View File

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

View File

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

View File

@ -1,16 +1,15 @@
import * as React from 'react'; import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
import * as React from "react"
import { Checkbox as ChakraCheckbox } from '@chakra-ui/react';
export interface CheckboxProps extends ChakraCheckbox.RootProps { export interface CheckboxProps extends ChakraCheckbox.RootProps {
icon?: React.ReactNode; icon?: React.ReactNode
inputProps?: React.InputHTMLAttributes<HTMLInputElement>; inputProps?: React.InputHTMLAttributes<HTMLInputElement>
rootRef?: React.Ref<HTMLLabelElement>; rootRef?: React.Ref<HTMLLabelElement>
} }
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>( export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
function Checkbox(props, ref) { function Checkbox(props, ref) {
const { icon, children, inputProps, rootRef, ...rest } = props; const { icon, children, inputProps, rootRef, ...rest } = props
return ( return (
<ChakraCheckbox.Root ref={rootRef} {...rest}> <ChakraCheckbox.Root ref={rootRef} {...rest}>
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} /> <ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
@ -21,6 +20,6 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label> <ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
)} )}
</ChakraCheckbox.Root> </ChakraCheckbox.Root>
); )
} },
); )

View File

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

View File

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

View File

@ -1,13 +1,11 @@
import * as React from 'react'; import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
import { CloseButton } from "./close-button"
import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react'; import * as React from "react"
import { CloseButton } from './close-button';
interface DialogContentProps extends ChakraDialog.ContentProps { interface DialogContentProps extends ChakraDialog.ContentProps {
portalled?: boolean; portalled?: boolean
portalRef?: React.RefObject<HTMLElement>; portalRef?: React.RefObject<HTMLElement>
backdrop?: boolean; backdrop?: boolean
} }
export const DialogContent = React.forwardRef< export const DialogContent = React.forwardRef<
@ -20,7 +18,7 @@ export const DialogContent = React.forwardRef<
portalRef, portalRef,
backdrop = true, backdrop = true,
...rest ...rest
} = props; } = props
return ( return (
<Portal disabled={!portalled} container={portalRef}> <Portal disabled={!portalled} container={portalRef}>
@ -31,8 +29,8 @@ export const DialogContent = React.forwardRef<
</ChakraDialog.Content> </ChakraDialog.Content>
</ChakraDialog.Positioner> </ChakraDialog.Positioner>
</Portal> </Portal>
); )
}); })
export const DialogCloseTrigger = React.forwardRef< export const DialogCloseTrigger = React.forwardRef<
HTMLButtonElement, HTMLButtonElement,
@ -50,15 +48,15 @@ export const DialogCloseTrigger = React.forwardRef<
{props.children} {props.children}
</CloseButton> </CloseButton>
</ChakraDialog.CloseTrigger> </ChakraDialog.CloseTrigger>
); )
}); })
export const DialogRoot = ChakraDialog.Root; export const DialogRoot = ChakraDialog.Root
export const DialogFooter = ChakraDialog.Footer; export const DialogFooter = ChakraDialog.Footer
export const DialogHeader = ChakraDialog.Header; export const DialogHeader = ChakraDialog.Header
export const DialogBody = ChakraDialog.Body; export const DialogBody = ChakraDialog.Body
export const DialogBackdrop = ChakraDialog.Backdrop; export const DialogBackdrop = ChakraDialog.Backdrop
export const DialogTitle = ChakraDialog.Title; export const DialogTitle = ChakraDialog.Title
export const DialogDescription = ChakraDialog.Description; export const DialogDescription = ChakraDialog.Description
export const DialogTrigger = ChakraDialog.Trigger; export const DialogTrigger = ChakraDialog.Trigger
export const DialogActionTrigger = ChakraDialog.ActionTrigger; export const DialogActionTrigger = ChakraDialog.ActionTrigger

View File

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

View File

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

View File

View File

@ -1,16 +1,15 @@
import * as React from 'react'; import type { BoxProps, InputElementProps } from "@chakra-ui/react"
import { Group, InputElement } from "@chakra-ui/react"
import type { BoxProps, InputElementProps } from '@chakra-ui/react'; import * as React from "react"
import { Group, InputElement } from '@chakra-ui/react';
export interface InputGroupProps extends BoxProps { export interface InputGroupProps extends BoxProps {
startElementProps?: InputElementProps; startElementProps?: InputElementProps
endElementProps?: InputElementProps; endElementProps?: InputElementProps
startElement?: React.ReactNode; startElement?: React.ReactNode
endElement?: React.ReactNode; endElement?: React.ReactNode
children: React.ReactElement<InputElementProps>; children: React.ReactElement<InputElementProps>
startOffset?: InputElementProps['paddingStart']; startOffset?: InputElementProps["paddingStart"]
endOffset?: InputElementProps['paddingEnd']; endOffset?: InputElementProps["paddingEnd"]
} }
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>( export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
@ -21,13 +20,13 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
endElement, endElement,
endElementProps, endElementProps,
children, children,
startOffset = '6px', startOffset = "6px",
endOffset = '6px', endOffset = "6px",
...rest ...rest
} = props; } = props
const child = const child =
React.Children.only<React.ReactElement<InputElementProps>>(children); React.Children.only<React.ReactElement<InputElementProps>>(children)
return ( return (
<Group ref={ref} {...rest}> <Group ref={ref} {...rest}>
@ -49,6 +48,6 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
</InputElement> </InputElement>
)} )}
</Group> </Group>
); )
} },
); )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +1,23 @@
import * as React from 'react'; import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react"
import * as React from "react"
import { Slider as ChakraSlider, For, HStack } from '@chakra-ui/react';
export interface SliderProps extends ChakraSlider.RootProps { export interface SliderProps extends ChakraSlider.RootProps {
marks?: Array<number | { value: number; label: React.ReactNode }>; marks?: Array<number | { value: number; label: React.ReactNode }>
label?: React.ReactNode; label?: React.ReactNode
showValue?: boolean; showValue?: boolean
} }
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>( export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
function Slider(props, ref) { function Slider(props, ref) {
const { marks: marksProp, label, showValue, ...rest } = props; const { marks: marksProp, label, showValue, ...rest } = props
const value = props.defaultValue ?? props.value; const value = props.defaultValue ?? props.value
const marks = marksProp?.map((mark) => { const marks = marksProp?.map((mark) => {
if (typeof mark === 'number') return { value: mark, label: undefined }; if (typeof mark === "number") return { value: mark, label: undefined }
return mark; return mark
}); })
const hasMarkLabel = !!marks?.some((mark) => mark.label); const hasMarkLabel = !!marks?.some((mark) => mark.label)
return ( return (
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}> <ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
@ -39,12 +38,12 @@ export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
<SliderMarks marks={marks} /> <SliderMarks marks={marks} />
</ChakraSlider.Control> </ChakraSlider.Control>
</ChakraSlider.Root> </ChakraSlider.Root>
); )
} },
); )
function SliderThumbs(props: { value?: number[] }) { function SliderThumbs(props: { value?: number[] }) {
const { value } = props; const { value } = props
return ( return (
<For each={value}> <For each={value}>
{(_, index) => ( {(_, index) => (
@ -53,31 +52,31 @@ function SliderThumbs(props: { value?: number[] }) {
</ChakraSlider.Thumb> </ChakraSlider.Thumb>
)} )}
</For> </For>
); )
} }
interface SliderMarksProps { interface SliderMarksProps {
marks?: Array<number | { value: number; label: React.ReactNode }>; marks?: Array<number | { value: number; label: React.ReactNode }>
} }
const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>( const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>(
function SliderMarks(props, ref) { function SliderMarks(props, ref) {
const { marks } = props; const { marks } = props
if (!marks?.length) return null; if (!marks?.length) return null
return ( return (
<ChakraSlider.MarkerGroup ref={ref}> <ChakraSlider.MarkerGroup ref={ref}>
{marks.map((mark, index) => { {marks.map((mark, index) => {
const value = typeof mark === 'number' ? mark : mark.value; const value = typeof mark === "number" ? mark : mark.value
const label = typeof mark === 'number' ? undefined : mark.label; const label = typeof mark === "number" ? undefined : mark.label
return ( return (
<ChakraSlider.Marker key={index} value={value}> <ChakraSlider.Marker key={index} value={value}>
<ChakraSlider.MarkerIndicator /> <ChakraSlider.MarkerIndicator />
{label} {label}
</ChakraSlider.Marker> </ChakraSlider.Marker>
); )
})} })}
</ChakraSlider.MarkerGroup> </ChakraSlider.MarkerGroup>
); )
} },
); )

View File

@ -1,5 +1,6 @@
'use client'; "use client"
import { RestErrorResponse } from "@/back-api";
import { import {
Toaster as ChakraToaster, Toaster as ChakraToaster,
Portal, Portal,
@ -7,14 +8,12 @@ import {
Stack, Stack,
Toast, Toast,
createToaster, createToaster,
} from '@chakra-ui/react'; } from "@chakra-ui/react"
import { RestErrorResponse } from '@/back-api';
export const toaster = createToaster({ export const toaster = createToaster({
placement: 'bottom-end', placement: "bottom-end",
pauseOnPageIdle: true, pauseOnPageIdle: true,
}); })
export const toasterAPIError = (error: RestErrorResponse) => { export const toasterAPIError = (error: RestErrorResponse) => {
toaster.create({ toaster.create({
@ -26,10 +25,10 @@ export const toasterAPIError = (error: RestErrorResponse) => {
export const Toaster = () => { export const Toaster = () => {
return ( return (
<Portal> <Portal>
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}> <ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
{(toast) => ( {(toast) => (
<Toast.Root width={{ md: 'sm' }}> <Toast.Root width={{ md: "sm" }}>
{toast.type === 'loading' ? ( {toast.type === "loading" ? (
<Spinner size="sm" color="blue.solid" /> <Spinner size="sm" color="blue.solid" />
) : ( ) : (
<Toast.Indicator /> <Toast.Indicator />
@ -48,5 +47,5 @@ export const Toaster = () => {
)} )}
</ChakraToaster> </ChakraToaster>
</Portal> </Portal>
); )
}; }

View File

@ -1,14 +1,13 @@
import * as React from 'react'; import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
import * as React from "react"
import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react';
export interface TooltipProps extends ChakraTooltip.RootProps { export interface TooltipProps extends ChakraTooltip.RootProps {
showArrow?: boolean; showArrow?: boolean
portalled?: boolean; portalled?: boolean
portalRef?: React.RefObject<HTMLElement>; portalRef?: React.RefObject<HTMLElement>
content: React.ReactNode; content: React.ReactNode
contentProps?: ChakraTooltip.ContentProps; contentProps?: ChakraTooltip.ContentProps
disabled?: boolean; disabled?: boolean
} }
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>( export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
@ -22,9 +21,9 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
contentProps, contentProps,
portalRef, portalRef,
...rest ...rest
} = props; } = props
if (disabled) return children; if (disabled) return children
return ( return (
<ChakraTooltip.Root {...rest}> <ChakraTooltip.Root {...rest}>
@ -42,6 +41,6 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
</ChakraTooltip.Positioner> </ChakraTooltip.Positioner>
</Portal> </Portal>
</ChakraTooltip.Root> </ChakraTooltip.Root>
); )
} },
); )

28
front/src/config/dayjs.ts Normal file
View File

@ -0,0 +1,28 @@
import dayjs from 'dayjs';
import 'dayjs/locale/fr';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import dayOfYear from 'dayjs/plugin/dayOfYear';
import duration from 'dayjs/plugin/duration';
import isBetween from 'dayjs/plugin/isBetween';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isToday from 'dayjs/plugin/isToday';
import isTomorrow from 'dayjs/plugin/isTomorrow';
import isYesterday from 'dayjs/plugin/isYesterday';
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
import relativeTime from 'dayjs/plugin/relativeTime';
import weekOfYear from 'dayjs/plugin/weekOfYear';
dayjs.locale('fr');
dayjs.extend(relativeTime);
dayjs.extend(customParseFormat);
dayjs.extend(weekOfYear);
dayjs.extend(isSameOrAfter);
dayjs.extend(isToday);
dayjs.extend(isTomorrow);
dayjs.extend(isYesterday);
dayjs.extend(dayOfYear);
dayjs.extend(isBetween);
dayjs.extend(advancedFormat);
dayjs.extend(quarterOfYear);
dayjs.extend(duration);

View File

@ -0,0 +1,2 @@
import './axios';
import './dayjs';

View File

@ -0,0 +1,2 @@
export const DATE_FORMAT = 'YYYY-MM-DD';
export const DATE_FORMAT_FULL = 'dddd DD MMMM HH:mm';

View File

@ -1,4 +1,4 @@
export const BASE_WRAP_SPACING = { base: '5px', md: '10px', lg: '20px' }; export const BASE_WRAP_SPACING = { base: "5px", md: "10px", lg: "20px" };
export const BASE_WRAP_WIDTH = { base: '90%', md: '45%', lg: '270px' }; export const BASE_WRAP_WIDTH = { base: "90%", md: "45%", lg: "270px" };
export const BASE_WRAP_HEIGHT = { base: '75px', lg: '120px' }; export const BASE_WRAP_HEIGHT = { base: "75px", lg: "120px" };
export const BASE_WRAP_ICON_SIZE = { base: '50px', lg: '100px' }; export const BASE_WRAP_ICON_SIZE = { base: "50px", lg: "100px" };

View File

@ -0,0 +1 @@
export * from './date'

View File

@ -89,9 +89,7 @@ export const isDevelopmentEnvironment = () => {
return import.meta.env.MODE === 'development'; return import.meta.env.MODE === 'development';
}; };
export const environment = isDevelopmentEnvironment() export const environment = isDevelopmentEnvironment() ? environment_local : environment_back_prod;
? environment_local
: environment_back_prod;
/** /**

View File

@ -1,4 +1,4 @@
import { Box, Center, Heading, Link, Text } from '@chakra-ui/react'; import { Box, Button, Center, Heading, Link, Text } from '@chakra-ui/react';
import { MdControlCamera } from 'react-icons/md'; import { MdControlCamera } from 'react-icons/md';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter'; import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
@ -10,17 +10,15 @@ export const Error401 = () => {
<TopBar /> <TopBar />
<PageLayoutInfoCenter padding="25px"> <PageLayoutInfoCenter padding="25px">
<Center> <Center>
<MdControlCamera <MdControlCamera style={{ width: "250px", height: "250px", color: "orange" }} />
style={{ width: '250px', height: '250px', color: 'orange' }}
/>
</Center> </Center>
<Box textAlign="center"> <Box textAlign="center">
<Heading>Error 401</Heading> <Heading>Erreur 401</Heading>
<Text color="red.600"> <Text color="red.600">
You are not authorized to access this content. Vous n'êtes pas autorisé a accéder a ce contenu.
</Text> </Text>
<Link as="a" href="/"> <Link as="a" href="/">
Back to Homepage Retour à l'accueil
</Link> </Link>
</Box> </Box>
</PageLayoutInfoCenter> </PageLayoutInfoCenter>

View File

@ -1,4 +1,4 @@
import { Box, Center, Heading, Link, Text } from '@chakra-ui/react'; import { Box, Button, Center, Heading, Link, Text } from '@chakra-ui/react';
import { MdDangerous } from 'react-icons/md'; import { MdDangerous } from 'react-icons/md';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter'; import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
@ -10,14 +10,14 @@ export const Error403 = () => {
<TopBar /> <TopBar />
<PageLayoutInfoCenter padding="25px"> <PageLayoutInfoCenter padding="25px">
<Center> <Center>
<MdDangerous <MdDangerous style={{ width: "250px", height: "250px", color: "red" }} />
style={{ width: '250px', height: '250px', color: 'red' }}
/>
</Center> </Center>
<Box textAlign="center"> <Box textAlign="center">
<Heading>Error 403</Heading> <Heading>Erreur 403</Heading>
<Text color="orange.600">This page is forbidden to you.</Text> <Text color="orange.600">Cette page vous est interdite</Text>
<Link href="/">Back to Homepage</Link> <Link href="/">
Retour à l'accueil
</Link>
</Box> </Box>
</PageLayoutInfoCenter> </PageLayoutInfoCenter>
</> </>

View File

@ -1,4 +1,4 @@
import { Box, Center, Heading, Link, Text } from '@chakra-ui/react'; import { Box, Button, Center, Heading, Link, Text } from '@chakra-ui/react';
import { MdSignpost } from 'react-icons/md'; import { MdSignpost } from 'react-icons/md';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter'; import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
@ -10,16 +10,16 @@ export const Error404 = () => {
<TopBar /> <TopBar />
<PageLayoutInfoCenter padding="25px"> <PageLayoutInfoCenter padding="25px">
<Center> <Center>
<MdSignpost <MdSignpost style={{ width: "250px", height: "250px", color: "aqua" }} />
style={{ width: '250px', height: '250px', color: 'aqua' }}
/>
</Center> </Center>
<Box textAlign="center"> <Box textAlign="center">
<Heading>Error 404</Heading> <Heading>Erreur 404</Heading>
<Text color="gray.600"> <Text color="gray.600">
This page no longer exists or the URL has changed. Cette page n'existe plus ou l'URL a changé
</Text> </Text>
<Link href="/">Back to Homepage</Link> <Link href="/">
Retour à l'accueil
</Link>
</Box> </Box>
</PageLayoutInfoCenter> </PageLayoutInfoCenter>
</> </>

View File

@ -1,6 +1,11 @@
import { ReactNode } from 'react'; import React, { FC } from 'react';
import { Alert, AlertDescription, AlertTitle, Box } from '@chakra-ui/react'; import {
AlertDescription,
AlertTitle,
Box,
Alert,
} from '@chakra-ui/react';
import { import {
FallbackProps, FallbackProps,
ErrorBoundary as ReactErrorBoundary, ErrorBoundary as ReactErrorBoundary,
@ -12,15 +17,8 @@ const ErrorFallback = ({ error }: FallbackProps) => {
<Alert.Root status="error" borderRadius="md"> <Alert.Root status="error" borderRadius="md">
<Alert.Indicator height="75px" width="75px" /> <Alert.Indicator height="75px" width="75px" />
<Box flex="1"> <Box flex="1">
<AlertTitle fontWeight="bold" fontSize="35px"> <AlertTitle fontWeight="bold" fontSize="35px">An unexpected error has occurred.</AlertTitle>
An unexpected error has occurred. <AlertDescription padding="5" marginTop="3" fontSize="20px" lineHeight="1.4">
</AlertTitle>
<AlertDescription
padding="5"
marginTop="3"
fontSize="20px"
lineHeight="1.4"
>
<br /> <br />
{error.message} {error.message}
</AlertDescription> </AlertDescription>
@ -30,10 +28,6 @@ const ErrorFallback = ({ error }: FallbackProps) => {
); );
}; };
export const ErrorBoundary = ({ children }: { children: ReactNode }) => { export const ErrorBoundary: FC<React.PropsWithChildren<unknown>> = (props) => {
return ( return <ReactErrorBoundary FallbackComponent={ErrorFallback} {...props} />;
<ReactErrorBoundary FallbackComponent={ErrorFallback}>
{children}
</ReactErrorBoundary>
);
}; };

View File

@ -1,13 +1,9 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { ChakraProvider } from '@chakra-ui/react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import App from '@/App'; import App from '@/App';
import { ColorModeProvider } from './components/ui/color-mode'; import { ColorModeProvider } from './components/ui/color-mode';
import { Toaster } from './components/ui/toaster';
import { systemTheme } from './theme/theme';
// Render the app // Render the app
const rootElement = document.getElementById('root'); const rootElement = document.getElementById('root');
@ -16,10 +12,7 @@ if (rootElement && !rootElement.innerHTML) {
root.render( root.render(
<StrictMode> <StrictMode>
<ColorModeProvider> <ColorModeProvider>
<ChakraProvider value={systemTheme}> <App />
<App />
<Toaster />
</ChakraProvider>
</ColorModeProvider> </ColorModeProvider>
</StrictMode> </StrictMode>
); );

15
front/src/scene/App.tsx Normal file
View File

@ -0,0 +1,15 @@
import { AudioPlayer } from '@/components/AudioPlayer';
import { ErrorBoundary } from '@/errors/ErrorBoundary';
import { AppRoutes } from '@/scene/AppRoutes';
import { ServiceContextProvider } from '@/service/ServiceContext';
export const App = () => {
return (
<ServiceContextProvider>
<ErrorBoundary>
<AppRoutes />
</ErrorBoundary>
<AudioPlayer />
</ServiceContextProvider>
);
};

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