[DEV] update from karso
This commit is contained in:
parent
4d18438914
commit
de61cc156f
1
.gitignore
vendored
1
.gitignore
vendored
@ -65,3 +65,4 @@ __pycache__
|
||||
.design/
|
||||
.vscode/
|
||||
front/storybook-static
|
||||
back/bin
|
||||
|
75
README.md
75
README.md
@ -6,38 +6,71 @@ Karideo
|
||||
Run in local:
|
||||
=============
|
||||
|
||||
so simple...
|
||||
Start tools
|
||||
-----------
|
||||
|
||||
Start the server basic interfaces: (DB(mySQL), Adminer)
|
||||
|
||||
```{.bash}
|
||||
# start the Bdd interface (no big data > 50Mo)
|
||||
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
|
||||
docker compose -f env_dev/docker-compose.yaml up -d
|
||||
```
|
||||
|
||||
Start the Back-end:
|
||||
-------------------
|
||||
|
||||
convert in an angular application:
|
||||
https://betterprogramming.pub/how-to-convert-your-angular-application-to-a-native-mobile-app-android-and-ios-c212b38976df
|
||||
backend is developed in JAVA
|
||||
|
||||
|
||||
Link with the external sub-library (front)
|
||||
------------------------------------------
|
||||
|
||||
Link:
|
||||
The first step is configuring your JAVA version (or select the JVM with the OS)
|
||||
```bash
|
||||
cd front
|
||||
pnpm run link_kar_cw
|
||||
export PATH=$(ls -d --color=never /usr/lib/jvm/java-2*-openjdk)/bin:$PATH
|
||||
```
|
||||
|
||||
un-link:
|
||||
Install the dependency:
|
||||
```bash
|
||||
mvn install
|
||||
```
|
||||
|
||||
Run the test
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
Install it for external use
|
||||
```bash
|
||||
mvn install
|
||||
```
|
||||
|
||||
Execute the local server:
|
||||
```bash
|
||||
mvn exec:java@dev-mode
|
||||
```
|
||||
|
||||
Start the Front-end:
|
||||
--------------------
|
||||
|
||||
backend is developed in JAVA
|
||||
```bash
|
||||
cd front
|
||||
pnpm run unlink_kar_cw
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Display the result:
|
||||
-------------------
|
||||
|
||||
[show the webpage: http://localhost:4203](http://localhost:4203)
|
||||
|
||||
Some other dev tools:
|
||||
=====================
|
||||
|
||||
Format code:
|
||||
------------
|
||||
|
||||
```bash
|
||||
export PATH=$(ls -d --color=never /usr/lib/jvm/java-2*-openjdk)/bin:$PATH
|
||||
mvn formatter:format
|
||||
mvn test
|
||||
```
|
||||
|
||||
Tools in production mode
|
||||
@ -89,4 +122,4 @@ export TAG_DOCKER=latest
|
||||
export REGISTRY_ADDRESS=gitea.atria-soft.org
|
||||
docker build -t ${REGISTRY_ADDRESS}/kangaroo-and-rabbit/karusic:${TAG_DOCKER} .
|
||||
docker push ${REGISTRY_ADDRESS}/kangaroo-and-rabbit/karusic:${TAG_DOCKER}
|
||||
```
|
||||
```
|
||||
|
33
back/pom.xml
33
back/pom.xml
@ -104,9 +104,38 @@
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<version>3.2.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prod-mode</id>
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<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>
|
||||
<mainClass>org.kar.karusic.WebLauncher</mainClass>
|
||||
<mainClass/>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- Create the source bundle -->
|
||||
|
17
back/src/org/kar/karusic/GenerateApi.java
Normal file
17
back/src/org/kar/karusic/GenerateApi.java
Normal file
@ -0,0 +1,17 @@
|
||||
package org.kar.karusic;
|
||||
|
||||
import org.kar.karusic.migration.Initialization;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class GenerateApi {
|
||||
private final static Logger LOGGER = LoggerFactory.getLogger(GenerateApi.class);
|
||||
|
||||
private GenerateApi() {}
|
||||
|
||||
public static void main(final String[] args) throws Exception {
|
||||
LOGGER.info("Generate API");
|
||||
Initialization.generateObjects();
|
||||
LOGGER.info("STOP the REST server.");
|
||||
}
|
||||
}
|
@ -1,22 +1,9 @@
|
||||
package org.kar.karusic;
|
||||
|
||||
import java.util.List;
|
||||
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.externalRestApi.AnalyzeApi;
|
||||
import org.kar.archidata.externalRestApi.TsGenerateApi;
|
||||
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.LoggerFactory;
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
@ -26,22 +13,12 @@ public class WebLauncherLocal extends WebLauncher {
|
||||
|
||||
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 {
|
||||
// Loop-back of logger JDK logging API to SLF4J
|
||||
LogManager.getLogManager().reset();
|
||||
SLF4JBridgeHandler.install();
|
||||
// Generate the APIs in type-script
|
||||
generateObjects();
|
||||
Initialization.generateObjects();
|
||||
final WebLauncherLocal launcher = new WebLauncherLocal();
|
||||
launcher.process();
|
||||
launcher.LOGGER.info("end-configure the server & wait finish process:");
|
||||
|
@ -2,10 +2,22 @@ package org.kar.karusic.migration;
|
||||
|
||||
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.externalRestApi.AnalyzeApi;
|
||||
import org.kar.archidata.externalRestApi.TsGenerateApi;
|
||||
import org.kar.archidata.migration.MigrationSqlStep;
|
||||
import org.kar.archidata.model.Data;
|
||||
import org.kar.archidata.model.User;
|
||||
import org.kar.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.Artist;
|
||||
import org.kar.karusic.model.Gender;
|
||||
@ -25,6 +37,16 @@ public class Initialization extends MigrationSqlStep {
|
||||
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
|
||||
public void generateStep() throws Exception {
|
||||
for (final Class<?> clazz : CLASSES_BASE) {
|
||||
|
@ -4,23 +4,11 @@ import { Box } from '@chakra-ui/react';
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import theme from '../src/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,
|
||||
},
|
||||
};
|
||||
import { ColorModeProvider } from '../src/components/ui/color-mode';
|
||||
import { Toaster } from '../src/components/ui/toaster';
|
||||
import { systemTheme } from '../src/theme/theme';
|
||||
|
||||
// .
|
||||
const DocumentationWrapper = ({ children }) => {
|
||||
return (
|
||||
<Box id="start-ui-storybook-wrapper" p="4" pb="8" flex="1">
|
||||
@ -31,13 +19,16 @@ const DocumentationWrapper = ({ children }) => {
|
||||
|
||||
export const decorators = [
|
||||
(Story, context) => (
|
||||
<ChakraProvider theme={theme}>
|
||||
{/* Using MemoryRouter to avoid route clashing with Storybook */}
|
||||
<MemoryRouter>
|
||||
<DocumentationWrapper>
|
||||
<Story {...context} />
|
||||
</DocumentationWrapper>
|
||||
</MemoryRouter>
|
||||
</ChakraProvider>
|
||||
<ColorModeProvider>
|
||||
<ChakraProvider value={systemTheme}>
|
||||
{/* Using MemoryRouter to avoid route clashing with Storybook */}
|
||||
<MemoryRouter>
|
||||
<DocumentationWrapper>
|
||||
<Story {...context} />
|
||||
</DocumentationWrapper>
|
||||
</MemoryRouter>
|
||||
<Toaster />
|
||||
</ChakraProvider>
|
||||
</ColorModeProvider>
|
||||
),
|
||||
];
|
||||
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
const dayjs = require('dayjs');
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const generateAppBuild = () => {
|
||||
const getVersion = () => fs.readFileSync('version.txt', 'utf8');
|
||||
|
||||
const commit = process.env.VERCEL_GIT_COMMIT_SHA
|
||||
? process.env.VERCEL_GIT_COMMIT_SHA
|
||||
: getVersion();
|
||||
|
||||
const appBuildContent = {
|
||||
display: `${dayjs().format('YYYY-MM-DD')}`,
|
||||
version: `${commit} - ${dayjs().format()}`,
|
||||
commit,
|
||||
date: dayjs().format(),
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
'./app-build.json',
|
||||
JSON.stringify(appBuildContent, null, 2)
|
||||
);
|
||||
};
|
||||
|
||||
generateAppBuild();
|
10888
front/config sample.ts
Normal file
10888
front/config sample.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,16 +3,7 @@ import type { KnipConfig } from 'knip';
|
||||
const config: KnipConfig = {
|
||||
// Ignoring mostly shell binaries
|
||||
ignoreBinaries: ['export', 'sleep'],
|
||||
ignore: [
|
||||
// Related to tests
|
||||
'tests/**',
|
||||
'**.conf.js',
|
||||
'steps.d.ts',
|
||||
'steps_file.js',
|
||||
'env_ci/codecept.conf.js',
|
||||
// Generic components are useful.
|
||||
'src/components/**',
|
||||
],
|
||||
ignore: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -18,7 +18,7 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch",
|
||||
"build": "tsc && vite build",
|
||||
"static:build": "node build.js && pnpm build",
|
||||
"static:build": "pnpm build",
|
||||
"dev": "vite",
|
||||
"pretty": "prettier -w .",
|
||||
"lint": "pnpm tsc --noEmit",
|
||||
@ -29,46 +29,47 @@
|
||||
"*.{ts,tsx,js,jsx,json}": "prettier --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/cli": "3.3.1",
|
||||
"@chakra-ui/react": "3.3.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
||||
"@chakra-ui/cli": "3.7.0",
|
||||
"@chakra-ui/react": "3.7.0",
|
||||
"@emotion/react": "11.14.0",
|
||||
"allotment": "1.20.2",
|
||||
"css-mediaquery": "0.1.2",
|
||||
"dayjs": "1.11.13",
|
||||
"history": "5.3.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-error-boundary": "5.0.0",
|
||||
"react-icons": "5.4.0",
|
||||
"react-router-dom": "7.1.1",
|
||||
"react-select": "5.9.0",
|
||||
"react-router-dom": "7.1.5",
|
||||
"react-select": "5.10.0",
|
||||
"react-use": "17.6.0",
|
||||
"zod": "3.24.1",
|
||||
"zustand": "5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/styled-system": "^2.12.0",
|
||||
"@playwright/test": "1.49.1",
|
||||
"@storybook/addon-actions": "8.4.7",
|
||||
"@storybook/addon-essentials": "8.4.7",
|
||||
"@storybook/addon-links": "8.4.7",
|
||||
"@storybook/addon-mdx-gfm": "8.4.7",
|
||||
"@storybook/react": "8.4.7",
|
||||
"@storybook/react-vite": "8.4.7",
|
||||
"@storybook/theming": "8.4.7",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-actions": "8.5.3",
|
||||
"@storybook/addon-essentials": "8.5.3",
|
||||
"@storybook/addon-links": "8.5.3",
|
||||
"@storybook/addon-mdx-gfm": "8.5.3",
|
||||
"@storybook/react": "8.5.3",
|
||||
"@storybook/react-vite": "8.5.3",
|
||||
"@storybook/theming": "8.5.3",
|
||||
"@testing-library/jest-dom": "6.6.3",
|
||||
"@testing-library/react": "16.1.0",
|
||||
"@testing-library/user-event": "14.5.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "5.2.1",
|
||||
"@testing-library/react": "16.2.0",
|
||||
"@testing-library/user-event": "14.6.1",
|
||||
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/node": "22.10.6",
|
||||
"@types/react": "18.3.8",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.20.0",
|
||||
"@typescript-eslint/parser": "8.20.0",
|
||||
"@types/node": "22.13.1",
|
||||
"@types/react": "19.0.8",
|
||||
"@types/react-dom": "19.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "8.23.0",
|
||||
"@typescript-eslint/parser": "8.23.0",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"eslint": "9.18.0",
|
||||
"eslint": "9.20.0",
|
||||
"eslint-plugin-codeceptjs": "1.3.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-react": "7.37.4",
|
||||
@ -76,16 +77,16 @@
|
||||
"eslint-plugin-storybook": "0.11.2",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"knip": "5.42.0",
|
||||
"lint-staged": "15.3.0",
|
||||
"npm-check-updates": "^17.1.13",
|
||||
"knip": "5.43.6",
|
||||
"lint-staged": "15.4.3",
|
||||
"npm-check-updates": "^17.1.14",
|
||||
"prettier": "3.4.2",
|
||||
"puppeteer": "24.0.0",
|
||||
"puppeteer": "24.2.0",
|
||||
"react-is": "19.0.0",
|
||||
"storybook": "8.4.7",
|
||||
"storybook": "8.5.3",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.7.3",
|
||||
"vite": "6.0.7",
|
||||
"vitest": "2.1.8"
|
||||
"vite": "6.1.0",
|
||||
"vitest": "3.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,128 +1,19 @@
|
||||
import { useState } from 'react';
|
||||
import { ErrorBoundary } from '@/errors/ErrorBoundary';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
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]}`;
|
||||
}
|
||||
};
|
||||
import { AudioPlayer } from './components';
|
||||
import { EnvDevelopment } from './components/EnvDevelopment/EnvDevelopment';
|
||||
import { AppRoutes } from './scene/AppRoutes';
|
||||
import { ServiceContextProvider } from './service/ServiceContext';
|
||||
|
||||
export const App = () => {
|
||||
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 >
|
||||
<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>
|
||||
<ServiceContextProvider>
|
||||
<EnvDevelopment />
|
||||
<ErrorBoundary>
|
||||
<AppRoutes />
|
||||
</ErrorBoundary>
|
||||
<AudioPlayer />
|
||||
</ServiceContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,16 +1,9 @@
|
||||
import { SyntheticEvent, useEffect, useRef, useState } from 'react';
|
||||
import { 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 {
|
||||
MdFastForward,
|
||||
MdFastRewind,
|
||||
MdGraphicEq,
|
||||
MdLooksOne,
|
||||
MdNavigateBefore,
|
||||
MdNavigateNext,
|
||||
@ -22,15 +15,15 @@ import {
|
||||
MdTrendingFlat,
|
||||
} from 'react-icons/md';
|
||||
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||
import { useSpecificAlbum } from '@/service/Album';
|
||||
import { useSpecificArtists } from '@/service/Artist';
|
||||
import { useSpecificGender } from '@/service/Gender';
|
||||
import { useSpecificTrack } from '@/service/Track';
|
||||
import { DataUrlAccess } from '@/utils/data-url-access';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { isNullOrUndefined } from '@/utils/validator';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
import { Slider } from './ui/slider';
|
||||
|
||||
export enum PlayMode {
|
||||
@ -41,10 +34,16 @@ export enum PlayMode {
|
||||
}
|
||||
|
||||
const playModeIcon = {
|
||||
[PlayMode.PLAY_ONE]: <MdLooksOne style={{ width: "100%", height: "100%" }} />,
|
||||
[PlayMode.PLAY_ALL]: <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]: <MdLooksOne style={{ width: '100%', height: '100%' }} />,
|
||||
[PlayMode.PLAY_ALL]: (
|
||||
<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%' }} />
|
||||
),
|
||||
};
|
||||
|
||||
export type AudioPlayerProps = {};
|
||||
@ -60,7 +59,7 @@ const formatTime = (time) => {
|
||||
return '00:00';
|
||||
};
|
||||
|
||||
export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
export const AudioPlayer = ({}: AudioPlayerProps) => {
|
||||
const { playTrackList, trackOffset, previous, next, first } =
|
||||
useActivePlaylistService();
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
@ -90,9 +89,9 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
_hover: {
|
||||
bgColor: 'brand.500',
|
||||
},
|
||||
width: "50px",
|
||||
height: "50px",
|
||||
padding: "5px",
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
padding: '5px',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -198,7 +197,7 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
console.log(`onTimeUpdate ${audioRef.current.currentTime}`);
|
||||
setTimeProgress(audioRef.current.currentTime);
|
||||
};
|
||||
const onDurationChange = (event) => { };
|
||||
const onDurationChange = (event) => {};
|
||||
const onChangeStateToPlay = () => {
|
||||
setIsPlaying(true);
|
||||
};
|
||||
@ -212,7 +211,7 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
result.push(60 * i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{!isNullOrUndefined(trackOffset) && (
|
||||
@ -240,7 +239,7 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
userSelect="none"
|
||||
marginRight="auto"
|
||||
overflow="hidden"
|
||||
// noOfLines={1}
|
||||
// noOfLines={1}
|
||||
>
|
||||
{dataTrack?.name ?? '???'}
|
||||
</Text>
|
||||
@ -250,7 +249,7 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
userSelect="none"
|
||||
marginRight="auto"
|
||||
overflow="hidden"
|
||||
// noOfLines={1}
|
||||
// noOfLines={1}
|
||||
>
|
||||
{dataArtists.map((data) => data.name).join(', ')} /{' '}
|
||||
{dataAlbum && dataAlbum?.name}
|
||||
@ -267,10 +266,13 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
variant="outline"
|
||||
colorPalette="brand"
|
||||
marks={marks()}
|
||||
//focusCapture={false}
|
||||
//focusCapture={false}
|
||||
>
|
||||
<SliderTrack bg="brand.200" height="10px" borderRadius="full">
|
||||
</SliderTrack>
|
||||
<SliderTrack
|
||||
bg="brand.200"
|
||||
height="10px"
|
||||
borderRadius="full"
|
||||
></SliderTrack>
|
||||
</Slider>
|
||||
</Box>
|
||||
<Flex>
|
||||
@ -280,7 +282,7 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
userSelect="none"
|
||||
marginRight="auto"
|
||||
overflow="hidden"
|
||||
// noOfLines={1}
|
||||
// noOfLines={1}
|
||||
>
|
||||
{formatTime(timeProgress)}
|
||||
</Text>
|
||||
@ -296,9 +298,9 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
variant="ghost"
|
||||
>
|
||||
{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
|
||||
@ -306,39 +308,51 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
|
||||
aria-label={'Stop'}
|
||||
onClick={onStop}
|
||||
variant="ghost"
|
||||
><MdStop style={{ width: "100%", height: "100%" }} /></IconButton>
|
||||
>
|
||||
<MdStop style={{ width: '100%', height: '100%' }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
{...configButton}
|
||||
aria-label={'Previous track'}
|
||||
onClick={onNavigatePrevious}
|
||||
marginLeft="auto"
|
||||
variant="ghost"
|
||||
><MdNavigateBefore style={{ width: "100%", height: "100%" }} /> </IconButton>
|
||||
>
|
||||
<MdNavigateBefore style={{ width: '100%', height: '100%' }} />{' '}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
{...configButton}
|
||||
aria-label={'jump 15sec in past'}
|
||||
onClick={onFastRewind}
|
||||
variant="ghost"
|
||||
><MdFastRewind style={{ width: "100%", height: "100%" }} /></IconButton>
|
||||
>
|
||||
<MdFastRewind style={{ width: '100%', height: '100%' }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
{...configButton}
|
||||
aria-label={'jump 15sec in future'}
|
||||
onClick={onFastForward}
|
||||
variant="ghost"
|
||||
><MdFastForward style={{ width: "100%", height: "100%" }} /></IconButton>
|
||||
>
|
||||
<MdFastForward style={{ width: '100%', height: '100%' }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
{...configButton}
|
||||
aria-label={'Next track'}
|
||||
marginRight="auto"
|
||||
onClick={onNavigateNext}
|
||||
variant="ghost"
|
||||
><MdNavigateNext style={{ width: "100%", height: "100%" }} /></IconButton>
|
||||
>
|
||||
<MdNavigateNext style={{ width: '100%', height: '100%' }} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
{...configButton}
|
||||
aria-label={'continue to the end'}
|
||||
onClick={onTypePlay}
|
||||
variant="ghost"
|
||||
>{playModeIcon[playingMode]}</IconButton>
|
||||
>
|
||||
{playModeIcon[playingMode]}
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
|
@ -3,13 +3,14 @@ import { ReactElement, useEffect, useState } from 'react';
|
||||
import { Box, BoxProps, Flex, FlexProps } from '@chakra-ui/react';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
|
||||
import { DataUrlAccess } from '@/utils/data-url-access';
|
||||
import { Icon } from './Icon';
|
||||
import { ObjectId } from '@/back-api';
|
||||
import { DataUrlAccess } from '@/utils/data-url-access';
|
||||
|
||||
export type CoversProps = Omit<BoxProps, "iconEmpty"> & {
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export type CoversProps = Omit<BoxProps, 'iconEmpty'> & {
|
||||
data?: ObjectId[];
|
||||
size?: BoxProps["width"];
|
||||
size?: BoxProps['width'];
|
||||
iconEmpty?: ReactElement;
|
||||
slideshow?: boolean;
|
||||
};
|
||||
@ -33,7 +34,9 @@ export const Covers = ({
|
||||
setPreviousImageIndex(currentImageIndex);
|
||||
setTopOpacity(0.0);
|
||||
setTimeout(() => {
|
||||
setCurrentImageIndex((prevIndex) => (prevIndex + 1) % (data?.length ?? 1));
|
||||
setCurrentImageIndex(
|
||||
(prevIndex) => (prevIndex + 1) % (data?.length ?? 1)
|
||||
);
|
||||
setTopOpacity(1.0);
|
||||
}, 1500);
|
||||
}, 3000);
|
||||
@ -42,7 +45,7 @@ export const Covers = ({
|
||||
|
||||
if (!data || data.length < 1) {
|
||||
if (iconEmpty) {
|
||||
return <Icon icon={iconEmpty} sizeIcon={size} />;
|
||||
return <Icon children={iconEmpty} sizeIcon={size} />;
|
||||
} else {
|
||||
return (
|
||||
<Box
|
||||
@ -60,40 +63,50 @@ export const Covers = ({
|
||||
}
|
||||
if (slideshow === false || data.length === 1) {
|
||||
const url = DataUrlAccess.getThumbnailUrl(data[0]);
|
||||
return <Image loading="lazy" src={url} maxWidth={size} boxSize={size} /*{...rest}*/ />;
|
||||
return (
|
||||
<Image
|
||||
loading="lazy"
|
||||
src={url}
|
||||
maxWidth={size}
|
||||
boxSize={size} /*{...rest}*/
|
||||
/>
|
||||
);
|
||||
}
|
||||
const urlCurrent = DataUrlAccess.getThumbnailUrl(data[currentImageIndex]);
|
||||
const urlPrevious = DataUrlAccess.getThumbnailUrl(data[previousImageIndex]);
|
||||
return <Flex
|
||||
position="relative"
|
||||
// {...rest}
|
||||
maxWidth={size}
|
||||
width={size}
|
||||
height={size}
|
||||
overflow="hidden">
|
||||
<Image
|
||||
src={urlPrevious}
|
||||
loading="lazy"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
zIndex={1}
|
||||
boxSize={size}
|
||||
/>
|
||||
<Image
|
||||
src={urlCurrent}
|
||||
loading="lazy"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
boxSize={size}
|
||||
transition="opacity 0.5s ease-in-out"
|
||||
opacity={topOpacity}
|
||||
zIndex={2}
|
||||
/>
|
||||
</Flex>
|
||||
return (
|
||||
<Flex
|
||||
position="relative"
|
||||
// {...rest}
|
||||
maxWidth={size}
|
||||
width={size}
|
||||
height={size}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Image
|
||||
src={urlPrevious}
|
||||
loading="lazy"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
zIndex={1}
|
||||
boxSize={size}
|
||||
/>
|
||||
<Image
|
||||
src={urlCurrent}
|
||||
loading="lazy"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
boxSize={size}
|
||||
transition="opacity 0.5s ease-in-out"
|
||||
opacity={topOpacity}
|
||||
zIndex={2}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -6,8 +6,8 @@ export const EmptyEnd = () => {
|
||||
width="full"
|
||||
height="25%"
|
||||
minHeight="250px"
|
||||
// borderWidth="1px"
|
||||
// borderColor="red"
|
||||
// borderWidth="1px"
|
||||
// borderColor="red"
|
||||
></Box>
|
||||
);
|
||||
};
|
||||
|
117
front/src/components/EnvDevelopment/EnvDevelopment.tsx
Normal file
117
front/src/components/EnvDevelopment/EnvDevelopment.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
createListCollection,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { environment } from '@/environment';
|
||||
import { USERS } from '@/service/session';
|
||||
import { hashLocalData } from '@/utils/sso';
|
||||
|
||||
export const USERS_COLLECTION = createListCollection({
|
||||
items: [
|
||||
{ label: 'karadmin', value: 'adminA@666' },
|
||||
{ label: 'karuser', value: 'userA@666' },
|
||||
{ label: 'NO_USER', value: '' },
|
||||
],
|
||||
});
|
||||
|
||||
export const EnvDevelopment = () => {
|
||||
const dialog = useDisclosure();
|
||||
const [selectUserTest, setSelectUserTest] = useState<string>('NO_USER');
|
||||
//const setUser = useRightsStore((store) => store.setUser);
|
||||
const buildEnv =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? 'Development'
|
||||
: import.meta.env.VITE_DEV_ENV_NAME;
|
||||
const envName: Array<string> = [];
|
||||
!!buildEnv && envName.push(buildEnv);
|
||||
if (!envName.length) {
|
||||
return null;
|
||||
}
|
||||
const handleChange = (selectedOption) => {
|
||||
console.log(`SELECT: [${selectedOption.target.value}]`);
|
||||
setSelectUserTest(selectedOption.target.value);
|
||||
};
|
||||
const onClose = () => {
|
||||
dialog.onClose();
|
||||
if (selectUserTest == 'NO_USER') {
|
||||
window.location.href = `/${environment.applName}/sso/${hashLocalData()}/false/__LOGOUT__`;
|
||||
} else {
|
||||
window.location.href = `/${environment.applName}/sso/${hashLocalData()}/true/${USERS[selectUserTest]}`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
as="button"
|
||||
zIndex="100000"
|
||||
position="fixed"
|
||||
top="0"
|
||||
insetStart="0"
|
||||
insetEnd="0"
|
||||
h="2px"
|
||||
bg="warning.400"
|
||||
cursor="pointer"
|
||||
data-test-id="devtools"
|
||||
onClick={dialog.onOpen}
|
||||
>
|
||||
<Text
|
||||
position="fixed"
|
||||
top="0"
|
||||
insetStart="4"
|
||||
bg="warning.400"
|
||||
color="warning.900"
|
||||
fontSize="0.6rem"
|
||||
fontWeight="bold"
|
||||
px="10px"
|
||||
marginLeft="25%"
|
||||
borderBottomStartRadius="sm"
|
||||
borderBottomEndRadius="sm"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{envName.join(' : ')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Dialog.Root open={dialog.open} onOpenChange={dialog.onClose}>
|
||||
<Dialog.Positioner>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>Outils développeurs</Dialog.Header>
|
||||
<Dialog.Body>
|
||||
<Stack>
|
||||
<Text>User</Text>
|
||||
<Select.Root
|
||||
onChange={handleChange}
|
||||
collection={USERS_COLLECTION}
|
||||
>
|
||||
<Select.Trigger>
|
||||
<Select.ValueText placeholder="Select test user" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{USERS_COLLECTION.items.map((value) => (
|
||||
<Select.Item item={value} key={value.value}>
|
||||
{value.label}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Stack>
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Positioner>
|
||||
</Dialog.Root>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,41 +1,40 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
FlexProps,
|
||||
} from '@chakra-ui/react';
|
||||
import { forwardRef, ReactNode } from 'react';
|
||||
import { ReactNode, forwardRef } from 'react';
|
||||
|
||||
import { Box, Flex, FlexProps } from '@chakra-ui/react';
|
||||
|
||||
export type IconProps = FlexProps & {
|
||||
icon: ReactNode;
|
||||
color?: string;
|
||||
sizeIcon?: FlexProps['width'];
|
||||
children: ReactNode;
|
||||
color?: string;
|
||||
sizeIcon?: FlexProps['width'];
|
||||
};
|
||||
|
||||
export const Icon = forwardRef<HTMLDivElement, IconProps>(
|
||||
({ icon: IconEl, color, sizeIcon = '1em', ...rest }, ref) => {
|
||||
return (
|
||||
<Flex flex="none"
|
||||
minWidth={sizeIcon}
|
||||
minHeight={sizeIcon}
|
||||
maxWidth={sizeIcon}
|
||||
maxHeight={sizeIcon}
|
||||
align="center"
|
||||
padding="1px"
|
||||
ref={ref}
|
||||
{...rest}>
|
||||
<Box
|
||||
marginX="auto"
|
||||
width="100%"
|
||||
minWidth="100%"
|
||||
height="100%"
|
||||
color={color}
|
||||
asChild
|
||||
>
|
||||
{IconEl}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
({ children, color, sizeIcon = '1em', ...rest }, ref) => {
|
||||
return (
|
||||
<Flex
|
||||
flex="none"
|
||||
minWidth={sizeIcon}
|
||||
minHeight={sizeIcon}
|
||||
maxWidth={sizeIcon}
|
||||
maxHeight={sizeIcon}
|
||||
align="center"
|
||||
padding="1px"
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
<Box
|
||||
marginX="auto"
|
||||
width="100%"
|
||||
minWidth="100%"
|
||||
height="100%"
|
||||
color={color}
|
||||
asChild
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Icon.displayName = 'Icon';
|
||||
|
@ -3,7 +3,7 @@ import React, { ReactNode, useEffect } from 'react';
|
||||
import { Flex, Image } from '@chakra-ui/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import background from '@/assets/images/ikon.svg';
|
||||
import ikon from '@/assets/images/ikon.svg';
|
||||
import { TOP_BAR_HEIGHT } from '@/components/TopBar/TopBar';
|
||||
|
||||
export type LayoutProps = React.PropsWithChildren<unknown> & {
|
||||
@ -28,9 +28,9 @@ export const PageLayout = ({ children }: LayoutProps) => {
|
||||
left={0}
|
||||
right={0}
|
||||
minWidth="300px"
|
||||
zIndex={-1}
|
||||
zIndex={0}
|
||||
>
|
||||
<Image src={background} boxSize="90%" margin="auto" opacity="30%" />
|
||||
<Image src={ikon} boxSize="90vh" margin="auto" />
|
||||
</Flex>
|
||||
<Flex
|
||||
direction="column"
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
|
||||
import { Flex, FlexProps } from '@chakra-ui/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||
import { colors } from '@/theme/colors';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { colors } from '@/theme/colors';
|
||||
|
||||
export type LayoutProps = FlexProps & {
|
||||
children: ReactNode;
|
||||
|
20
front/src/components/ParameterLayout/ParameterLayout.ts
Normal file
20
front/src/components/ParameterLayout/ParameterLayout.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export {
|
||||
ParameterLayoutContent as Content,
|
||||
type ParameterLayoutContentProps as ContentProps,
|
||||
} from './ParameterLayoutContent';
|
||||
export {
|
||||
ParameterLayoutFooter as Footer,
|
||||
type ParameterLayoutFooterProps as FooterProps,
|
||||
} from './ParameterLayoutFooter';
|
||||
export {
|
||||
ParameterLayoutHeader as Header,
|
||||
type ParameterLayoutHeaderProps as HeaderProps,
|
||||
} from './ParameterLayoutHeader';
|
||||
export {
|
||||
ParameterLayoutHeaderBase as HeaderBase,
|
||||
type ParameterLayoutHeaderBaseProps as HeaderBaseProps,
|
||||
} from './ParameterLayoutHeaderBase';
|
||||
export {
|
||||
ParameterLayoutRoot as Root,
|
||||
type ParameterLayoutRootProps as RootProps,
|
||||
} from './ParameterLayoutRoot';
|
@ -0,0 +1,25 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
export type ParameterLayoutContentProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ParameterLayoutContent = ({
|
||||
children,
|
||||
}: ParameterLayoutContentProps) => {
|
||||
return (
|
||||
<Flex
|
||||
direction="column"
|
||||
width="full"
|
||||
borderY="1px solid black"
|
||||
paddingY="15px"
|
||||
paddingX="25px"
|
||||
minHeight="10px"
|
||||
background="gray.700"
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
export type ParameterLayoutFooterProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ParameterLayoutFooter = ({
|
||||
children,
|
||||
}: ParameterLayoutFooterProps) => {
|
||||
return (
|
||||
<Flex width="full" paddingY="15px" paddingX="25px" minHeight="10px">
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
export type ParameterLayoutHeaderProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ParameterLayoutHeader = ({
|
||||
children,
|
||||
}: ParameterLayoutHeaderProps) => {
|
||||
return (
|
||||
<Flex width="full" paddingY="15px" paddingX="25px" minHeight="10px">
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
|
||||
import { ParameterLayoutHeader } from './ParameterLayoutHeader';
|
||||
|
||||
export type ParameterLayoutHeaderBaseProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export const ParameterLayoutHeaderBase = ({
|
||||
title,
|
||||
description,
|
||||
}: ParameterLayoutHeaderBaseProps) => {
|
||||
return (
|
||||
<ParameterLayoutHeader>
|
||||
<Flex direction="column">
|
||||
<Text fontSize="25px" fontWeight="bold">
|
||||
{title}
|
||||
</Text>
|
||||
{description && <Text>{description}</Text>}
|
||||
</Flex>
|
||||
</ParameterLayoutHeader>
|
||||
);
|
||||
};
|
24
front/src/components/ParameterLayout/ParameterLayoutRoot.tsx
Normal file
24
front/src/components/ParameterLayout/ParameterLayoutRoot.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
|
||||
export type ParameterLayoutRootProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ParameterLayoutRoot = ({ children }: ParameterLayoutRootProps) => {
|
||||
return (
|
||||
<VStack
|
||||
gap="0px"
|
||||
marginX="15%"
|
||||
marginY="20px"
|
||||
justify="center"
|
||||
//borderRadius="20px"
|
||||
borderRadius="0px"
|
||||
border="1px solid black"
|
||||
background="gray.500"
|
||||
>
|
||||
{children}
|
||||
</VStack>
|
||||
);
|
||||
};
|
1
front/src/components/ParameterLayout/index.ts
Normal file
1
front/src/components/ParameterLayout/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * as ParameterLayout from './ParameterLayout';
|
@ -1,9 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
Group,
|
||||
Input,
|
||||
} from '@chakra-ui/react';
|
||||
import { Group, Input } from '@chakra-ui/react';
|
||||
import { MdSearch } from 'react-icons/md';
|
||||
|
||||
export type SearchInputProps = {
|
||||
|
@ -2,14 +2,14 @@ import { ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ConditionalValue,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Span,
|
||||
Text,
|
||||
useDisclosure,
|
||||
Button,
|
||||
ConditionalValue,
|
||||
Span,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
LuAlignJustify,
|
||||
@ -21,15 +21,16 @@ import {
|
||||
LuSettings,
|
||||
LuSun,
|
||||
} from 'react-icons/lu';
|
||||
import {
|
||||
MdHelp,
|
||||
MdHome,
|
||||
MdMore,
|
||||
MdOutlinePlaylistPlay,
|
||||
MdOutlineUploadFile,
|
||||
MdSupervisedUserCircle,
|
||||
} from 'react-icons/md';
|
||||
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 {
|
||||
DrawerBody,
|
||||
@ -37,11 +38,29 @@ import {
|
||||
DrawerHeader,
|
||||
DrawerRoot,
|
||||
} from '@/components/ui/drawer';
|
||||
import {
|
||||
MenuContent,
|
||||
MenuItem,
|
||||
MenuRoot,
|
||||
MenuTrigger,
|
||||
} from '@/components/ui/menu';
|
||||
import { useServiceContext } from '@/service/ServiceContext';
|
||||
import { SessionState } from '@/service/SessionState';
|
||||
import { useSessionService } from '@/service/session';
|
||||
import { colors } from '@/theme/colors';
|
||||
import {
|
||||
requestOpenSite,
|
||||
requestSignIn,
|
||||
requestSignOut,
|
||||
requestSignUp,
|
||||
} from '@/utils/sso';
|
||||
|
||||
export const TOP_BAR_HEIGHT = '50px';
|
||||
|
||||
export const BUTTON_TOP_BAR_PROPERTY = {
|
||||
variant: "ghost" as ConditionalValue<"ghost" | "outline" | "solid" | "subtle" | "surface" | "plain" | undefined>,
|
||||
variant: 'ghost' as ConditionalValue<
|
||||
'ghost' | 'outline' | 'solid' | 'subtle' | 'surface' | 'plain' | undefined
|
||||
>,
|
||||
//colorPalette: "brand",
|
||||
fontSize: '20px',
|
||||
textTransform: 'uppercase',
|
||||
@ -53,6 +72,41 @@ export type TopBarProps = {
|
||||
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) => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { clearToken } = useSessionService();
|
||||
@ -79,21 +133,6 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
||||
const onKarso = (): void => {
|
||||
requestOpenSite();
|
||||
};
|
||||
const onSelectAdd = () => {
|
||||
navigate('/add');
|
||||
};
|
||||
const onSelectHome = () => {
|
||||
navigate('/');
|
||||
};
|
||||
const onSelectOnAir = () => {
|
||||
navigate('/on-air');
|
||||
};
|
||||
const onHelp = () => {
|
||||
navigate('/help');
|
||||
};
|
||||
const onSettings = () => {
|
||||
navigate('/settings');
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
position="absolute"
|
||||
@ -155,31 +194,60 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
||||
<MenuRoot>
|
||||
<MenuTrigger asChild>
|
||||
<IconButton
|
||||
asChild
|
||||
aria-label="Options"
|
||||
{...BUTTON_TOP_BAR_PROPERTY}
|
||||
width={TOP_BAR_HEIGHT}
|
||||
><MdSupervisedUserCircle /></IconButton>
|
||||
>
|
||||
<LuCircleUserRound />
|
||||
</IconButton>
|
||||
</MenuTrigger>
|
||||
<MenuContent>
|
||||
<MenuItem value="user" valueText="user" color={useColorModeValue('brand.800', 'brand.200')}>
|
||||
<MenuItem
|
||||
value="user"
|
||||
valueText="user"
|
||||
color={useColorModeValue('brand.800', 'brand.200')}
|
||||
>
|
||||
<MdSupervisedUserCircle />
|
||||
<Box flex="1">Sign in as {session?.login ?? 'Fail'}</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="Settings" valueText="Settings" onClick={onSettings}><LuSettings />Settings</MenuItem>
|
||||
<MenuItem value="Help" valueText="Help" onClick={onHelp}><MdHelp /> Help</MenuItem>
|
||||
<MenuItem value="Sign-out" valueText="Sign-out" onClick={onSignOut}>
|
||||
<MenuItem
|
||||
value="Settings"
|
||||
valueText="Settings"
|
||||
onClick={() => navigate('/settings')}
|
||||
>
|
||||
<LuSettings />
|
||||
Settings
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="Help"
|
||||
valueText="Help"
|
||||
onClick={() => navigate('/help')}
|
||||
>
|
||||
<MdHelp /> Help
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="Sign-out"
|
||||
valueText="Sign-out"
|
||||
onClick={onSignOut}
|
||||
>
|
||||
<LuLogOut /> Sign-out
|
||||
</MenuItem>
|
||||
<MenuItem value="karso" valueText="Karso" onClick={onKarso}>
|
||||
<LuKeySquare /> Karso (SSO)
|
||||
</MenuItem>
|
||||
{colorMode === 'light' ? (
|
||||
<MenuItem value="set-dark" valueText="set-dark" onClick={toggleColorMode}>
|
||||
<MenuItem
|
||||
value="set-dark"
|
||||
valueText="set-dark"
|
||||
onClick={toggleColorMode}
|
||||
>
|
||||
<LuMoon /> Set dark mode
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem value="set-light" valueText="set-light" onClick={toggleColorMode}>
|
||||
<MenuItem
|
||||
value="set-light"
|
||||
valueText="set-light"
|
||||
onClick={toggleColorMode}
|
||||
>
|
||||
<LuSun /> Set light mode
|
||||
</MenuItem>
|
||||
)}
|
||||
@ -193,8 +261,7 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
||||
open={drawerDisclose.open}
|
||||
data-testid="top-bar_drawer-root"
|
||||
>
|
||||
<DrawerContent
|
||||
data-testid="top-bar_drawer-content">
|
||||
<DrawerContent data-testid="top-bar_drawer-content">
|
||||
<DrawerHeader
|
||||
paddingY="auto"
|
||||
as="button"
|
||||
@ -204,54 +271,31 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
||||
color={useColorModeValue('brand.900', 'brand.50')}
|
||||
textTransform="uppercase"
|
||||
>
|
||||
<HStack
|
||||
{...BUTTON_TOP_BAR_PROPERTY} cursor="pointer">
|
||||
<HStack {...BUTTON_TOP_BAR_PROPERTY} cursor="pointer">
|
||||
<LuArrowBigLeft />
|
||||
<Span paddingLeft="3px">
|
||||
Karusic
|
||||
</Span>
|
||||
<Span paddingLeft="3px">Karusic</Span>
|
||||
</HStack>
|
||||
</DrawerHeader>
|
||||
<DrawerBody paddingX="0px">
|
||||
<Box marginY="3" />
|
||||
<Button
|
||||
background="#00000000"
|
||||
borderRadius="0px"
|
||||
onClick={onSelectHome}
|
||||
width="full"
|
||||
{...BUTTON_TOP_BAR_PROPERTY}
|
||||
>
|
||||
<MdHome style={{ width: "45px", height: "45px" }} />
|
||||
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
|
||||
Home
|
||||
</Text>
|
||||
</Button>
|
||||
<Box marginY="5" marginX="10" height="2px" background="brand.600" />
|
||||
<Button
|
||||
background="#00000000"
|
||||
borderRadius="0px"
|
||||
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>
|
||||
<ButtonMenuLeft
|
||||
onClickEnd={drawerDisclose.onClose}
|
||||
dest="/"
|
||||
title="Home"
|
||||
icon={<MdHome />}
|
||||
/>
|
||||
<ButtonMenuLeft
|
||||
onClickEnd={drawerDisclose.onClose}
|
||||
dest="/on-air"
|
||||
title="On air"
|
||||
icon={<MdOutlinePlaylistPlay />}
|
||||
/>
|
||||
<ButtonMenuLeft
|
||||
onClickEnd={drawerDisclose.onClose}
|
||||
dest="/add"
|
||||
title="Add Media"
|
||||
icon={<MdOutlineUploadFile />}
|
||||
/>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</DrawerRoot>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Flex, Span, Text } from '@chakra-ui/react';
|
||||
import { Flex, Span } from '@chakra-ui/react';
|
||||
import { LuDisc3 } from 'react-icons/lu';
|
||||
|
||||
import { Album } from '@/back-api';
|
||||
import { Covers } from '@/components/Cover';
|
||||
import { useCountTracksWithAlbumId } from '@/service/Track';
|
||||
import { BASE_WRAP_ICON_SIZE } from '@/constants/genericSpacing';
|
||||
import { useCountTracksWithAlbumId } from '@/service/Track';
|
||||
|
||||
export type DisplayAlbumProps = {
|
||||
dataAlbum?: Album;
|
||||
@ -19,8 +19,12 @@ export const DisplayAlbum = ({ dataAlbum }: DisplayAlbumProps) => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Flex direction="row" width="full" height="full"
|
||||
data-testid="display-album_flex">
|
||||
<Flex
|
||||
direction="row"
|
||||
width="full"
|
||||
height="full"
|
||||
data-testid="display-album_flex"
|
||||
>
|
||||
<Covers
|
||||
data={dataAlbum?.covers}
|
||||
size={BASE_WRAP_ICON_SIZE}
|
||||
@ -43,7 +47,7 @@ export const DisplayAlbum = ({ dataAlbum }: DisplayAlbumProps) => {
|
||||
userSelect="none"
|
||||
marginRight="auto"
|
||||
overflow="hidden"
|
||||
// noOfLines={[1, 2]}
|
||||
// noOfLines={[1, 2]}
|
||||
>
|
||||
{dataAlbum?.name}
|
||||
</Span>
|
||||
@ -53,7 +57,7 @@ export const DisplayAlbum = ({ dataAlbum }: DisplayAlbumProps) => {
|
||||
userSelect="none"
|
||||
marginRight="auto"
|
||||
overflow="hidden"
|
||||
// noOfLines={1}
|
||||
// noOfLines={1}
|
||||
>
|
||||
{countTracksOfAnAlbum} track{countTracksOfAnAlbum >= 1 && 's'}
|
||||
</Span>
|
||||
|
@ -6,6 +6,5 @@ export type DisplayAlbumIdProps = {
|
||||
};
|
||||
export const DisplayAlbumId = ({ id }: DisplayAlbumIdProps) => {
|
||||
const { dataAlbum } = useSpecificAlbum(id);
|
||||
return <DisplayAlbum dataAlbum={dataAlbum}
|
||||
data-testid="display-album-id" />;
|
||||
return <DisplayAlbum dataAlbum={dataAlbum} data-testid="display-album-id" />;
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { LuMenu } from 'react-icons/lu';
|
||||
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '../ui/menu';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
import { Button } from '../ui/button';
|
||||
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '../ui/menu';
|
||||
|
||||
export type MenuElement = {
|
||||
icon?: ReactNode;
|
||||
@ -20,27 +20,33 @@ export const ContextMenu = ({ elements }: ContextMenuProps) => {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<MenuRoot
|
||||
data-testid="context-menu">
|
||||
<MenuTrigger asChild
|
||||
<MenuRoot data-testid="context-menu">
|
||||
<MenuTrigger
|
||||
asChild
|
||||
marginY="auto"
|
||||
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 */}
|
||||
<Button variant="ghost" color="brand.500">
|
||||
<LuMenu />
|
||||
</Button>
|
||||
</MenuTrigger>
|
||||
<MenuContent
|
||||
data-testid="context-menu_content">
|
||||
<MenuContent data-testid="context-menu_content">
|
||||
{elements?.map((data) => (
|
||||
<MenuItem key={data.name} value={data.name} onClick={data.onClick} height="65px" fontSize="25px"
|
||||
data-test-id="context-menu_item" >
|
||||
<MenuItem
|
||||
key={data.name}
|
||||
value={data.name}
|
||||
onClick={data.onClick}
|
||||
height="65px"
|
||||
fontSize="25px"
|
||||
data-test-id="context-menu_item"
|
||||
>
|
||||
{data.icon}
|
||||
{data.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuContent>
|
||||
</MenuRoot >
|
||||
</MenuRoot>
|
||||
);
|
||||
};
|
||||
|
@ -1,25 +1,13 @@
|
||||
import {
|
||||
DragEventHandler,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
import { DragEventHandler, ReactNode, RefObject } from 'react';
|
||||
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
Center,
|
||||
Flex,
|
||||
HStack,
|
||||
Image,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
MdHighlightOff,
|
||||
MdUploadFile,
|
||||
} from 'react-icons/md';
|
||||
import { Box, BoxProps, Center, Flex, HStack, Image } from '@chakra-ui/react';
|
||||
import { MdHighlightOff, MdUploadFile } from 'react-icons/md';
|
||||
|
||||
import { FormGroup } from '@/components/form/FormGroup';
|
||||
import { UseFormidableReturn } from '@/components/form/Formidable';
|
||||
import { DataUrlAccess } from '@/utils/data-url-access';
|
||||
|
||||
import { useFormidableContextElement } from '../formidable';
|
||||
|
||||
export type DragNdropProps = {
|
||||
onFilesSelected?: (file: File[]) => void;
|
||||
onUriSelected?: (uri: string) => void;
|
||||
@ -28,8 +16,8 @@ export type DragNdropProps = {
|
||||
};
|
||||
|
||||
export const DragNdrop = ({
|
||||
onFilesSelected = () => { },
|
||||
onUriSelected = () => { },
|
||||
onFilesSelected = () => {},
|
||||
onUriSelected = () => {},
|
||||
width = '100px',
|
||||
height = '100px',
|
||||
}: DragNdropProps) => {
|
||||
@ -92,7 +80,7 @@ export const DragNdrop = ({
|
||||
};
|
||||
|
||||
export type CenterIconProps = BoxProps & {
|
||||
children: any;
|
||||
children: ReactNode;
|
||||
sizeIcon?: string;
|
||||
};
|
||||
|
||||
@ -110,14 +98,15 @@ export const CenterIcon = ({
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translate(-50%, -50%)"
|
||||
>{children}</Box>
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export type FormCoversProps = {
|
||||
form: UseFormidableReturn;
|
||||
variableName: string;
|
||||
name: string;
|
||||
ref?: RefObject<any>;
|
||||
label?: string;
|
||||
isRequired?: boolean;
|
||||
@ -126,23 +115,19 @@ export type FormCoversProps = {
|
||||
onRemove?: (index: number) => void;
|
||||
};
|
||||
|
||||
/** This field component is a direct insertion component ==> not manage with formidable */
|
||||
export const FormCovers = ({
|
||||
form,
|
||||
variableName,
|
||||
name,
|
||||
ref,
|
||||
onFilesSelected = () => { },
|
||||
onUriSelected = () => { },
|
||||
onRemove = () => { },
|
||||
onFilesSelected = () => {},
|
||||
onUriSelected = () => {},
|
||||
onRemove = () => {},
|
||||
...rest
|
||||
}: FormCoversProps) => {
|
||||
const urls =
|
||||
DataUrlAccess.getListThumbnailUrl(form.values[variableName]) ?? [];
|
||||
const { value } = useFormidableContextElement(name);
|
||||
const urls = DataUrlAccess.getListThumbnailUrl(value) ?? [];
|
||||
return (
|
||||
<FormGroup
|
||||
isModify={form.isModify[variableName]}
|
||||
onRestore={() => form.restoreValue({ [variableName]: true })}
|
||||
{...rest}
|
||||
>
|
||||
<FormGroup name={name} {...rest}>
|
||||
<HStack wrap="wrap" width="full">
|
||||
{urls.map((data, index) => (
|
||||
<Flex align="flex-start" key={data}>
|
||||
@ -155,7 +140,9 @@ export const FormCovers = ({
|
||||
color="#00000020"
|
||||
_hover={{ color: 'red' }}
|
||||
onClick={() => onRemove && onRemove(index)}
|
||||
><MdHighlightOff /></CenterIcon>
|
||||
>
|
||||
<MdHighlightOff />
|
||||
</CenterIcon>
|
||||
</Box>
|
||||
<Image loading="lazy" src={data} boxSize="full" />
|
||||
</Box>
|
||||
|
@ -1,62 +1,128 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
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 = {
|
||||
children: ReactNode;
|
||||
name: string;
|
||||
error?: ReactNode;
|
||||
help?: ReactNode;
|
||||
label?: ReactNode;
|
||||
isModify?: boolean;
|
||||
onRestore?: () => void;
|
||||
isRequired?: boolean;
|
||||
children: ReactNode;
|
||||
disableSingleLine?: boolean;
|
||||
};
|
||||
|
||||
export const FormGroup = ({
|
||||
children,
|
||||
error,
|
||||
name,
|
||||
help,
|
||||
label,
|
||||
isModify = false,
|
||||
isRequired = false,
|
||||
onRestore,
|
||||
}: FormGroupProps) => (
|
||||
<Flex
|
||||
borderLeftWidth="3px"
|
||||
borderLeftColor={error ? 'red' : isModify ? 'blue' : '#00000000'}
|
||||
paddingLeft="7px"
|
||||
paddingY="4px"
|
||||
direction="column"
|
||||
>
|
||||
<Flex direction="row" width="full" gap="52px">
|
||||
{!!label && (
|
||||
<Text marginRight="auto" fontWeight="bold">
|
||||
{label}{' '}
|
||||
{isRequired && (
|
||||
<Text as="span" color="red.600">
|
||||
*
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
disableSingleLine,
|
||||
}: FormGroupProps) => {
|
||||
const { form, error, isModify, onRestore } =
|
||||
useFormidableContextElement(name);
|
||||
const enableModifyNotification =
|
||||
form.configuration.enableModifyNotification ?? false;
|
||||
const enableReset = form.configuration.enableReset ?? false;
|
||||
const singleLine = disableSingleLine
|
||||
? false
|
||||
: form.configuration.singleLineForm;
|
||||
return (
|
||||
<Flex
|
||||
borderLeftWidth="3px"
|
||||
borderLeftColor={
|
||||
error
|
||||
? 'red.600'
|
||||
: enableModifyNotification && isModify
|
||||
? 'blue.600'
|
||||
: '#00000000'
|
||||
}
|
||||
paddingLeft="7px"
|
||||
paddingY="4px"
|
||||
width="full"
|
||||
direction="column"
|
||||
>
|
||||
{singleLine && (
|
||||
<>
|
||||
<Flex direction="row" width="full" gap="52px">
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{!!onRestore && isModify && (
|
||||
<MdRefresh size="15px" onClick={onRestore} cursor="pointer" />
|
||||
{!singleLine && (
|
||||
<>
|
||||
<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>
|
||||
{children}
|
||||
{!!help && (
|
||||
<Flex direction="row">
|
||||
<MdHelpOutline />
|
||||
<Text>{help}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!!error && (
|
||||
<Flex direction="row">
|
||||
<MdErrorOutline />
|
||||
<Text>{error}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
@ -2,35 +2,34 @@ import { RefObject } from 'react';
|
||||
|
||||
import { Input } from '@chakra-ui/react';
|
||||
|
||||
import { FormGroup } from '@/components/form/FormGroup';
|
||||
import { UseFormidableReturn } from '@/components/form/Formidable';
|
||||
import { FormGroup, FormGroupProps } from '@/components/form/FormGroup';
|
||||
|
||||
import { useFormidableContextElement } from '../formidable';
|
||||
|
||||
export type FormInputProps = {
|
||||
form: UseFormidableReturn;
|
||||
variableName: string;
|
||||
name: string;
|
||||
ref?: RefObject<any>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
isRequired?: boolean;
|
||||
};
|
||||
} & Omit<FormGroupProps, 'children'>;
|
||||
|
||||
export const FormInput = ({
|
||||
form,
|
||||
variableName,
|
||||
name,
|
||||
ref,
|
||||
placeholder,
|
||||
...rest
|
||||
}: FormInputProps) => {
|
||||
const { value, onChange } = useFormidableContextElement(name);
|
||||
return (
|
||||
<FormGroup
|
||||
isModify={form.isModify[variableName]}
|
||||
onRestore={() => form.restoreValue({ [variableName]: true })}
|
||||
{...rest}
|
||||
>
|
||||
<FormGroup name={name} {...rest}>
|
||||
<Input
|
||||
ref={ref}
|
||||
value={form.values[variableName]}
|
||||
onChange={(e) => form.setValues({ [variableName]: e.target.value })}
|
||||
type="text"
|
||||
name={name}
|
||||
autoComplete={name}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
@ -1,15 +1,19 @@
|
||||
import { RefObject } from 'react';
|
||||
|
||||
import { FormGroup } from '@/components/form/FormGroup';
|
||||
import { UseFormidableReturn } from '@/components/form/Formidable';
|
||||
import { NumberInputField, NumberInputProps, NumberInputRoot } from '../ui/number-input';
|
||||
|
||||
import { useFormidableContextElement } from '../formidable';
|
||||
import {
|
||||
NumberInputField,
|
||||
NumberInputProps,
|
||||
NumberInputRoot,
|
||||
} from '../ui/number-input';
|
||||
|
||||
export type FormNumberProps = Pick<
|
||||
NumberInputProps,
|
||||
'step' | 'defaultValue' | 'min' | 'max'
|
||||
> & {
|
||||
form: UseFormidableReturn;
|
||||
variableName: string;
|
||||
name: string;
|
||||
ref?: RefObject<any>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
@ -17,8 +21,7 @@ export type FormNumberProps = Pick<
|
||||
};
|
||||
|
||||
export const FormNumber = ({
|
||||
form,
|
||||
variableName,
|
||||
name,
|
||||
ref,
|
||||
placeholder,
|
||||
step,
|
||||
@ -27,19 +30,14 @@ export const FormNumber = ({
|
||||
defaultValue,
|
||||
...rest
|
||||
}: FormNumberProps) => {
|
||||
const onEvent = (value) => {
|
||||
form.setValues({ [variableName]: value.value });
|
||||
}
|
||||
const { form, value, isModify, onChange, onRestore } =
|
||||
useFormidableContextElement(name);
|
||||
return (
|
||||
<FormGroup
|
||||
isModify={form.isModify[variableName]}
|
||||
onRestore={() => form.restoreValue({ [variableName]: true })}
|
||||
{...rest}
|
||||
>
|
||||
<FormGroup name={name} {...rest}>
|
||||
<NumberInputRoot
|
||||
ref={ref}
|
||||
value={form.values[variableName]}
|
||||
onValueChange={onEvent}
|
||||
value={value}
|
||||
onValueChange={(e) => onChange(e.value)}
|
||||
step={step}
|
||||
defaultValue={defaultValue}
|
||||
min={min}
|
||||
|
50
front/src/components/form/FormPassword.tsx
Normal file
50
front/src/components/form/FormPassword.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { RefObject, useState } from 'react';
|
||||
|
||||
import { chakra, Group, Input } from '@chakra-ui/react';
|
||||
|
||||
import { FormGroup, FormGroupProps } from '@/components/form/FormGroup';
|
||||
import { Button } from '../ui/button';
|
||||
import { LuEye, LuEyeOff } from 'react-icons/lu';
|
||||
import { useFormidableContextElement } from '../formidable';
|
||||
|
||||
export type FormInputProps = {
|
||||
name: string;
|
||||
ref?: RefObject<any>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
isRequired?: boolean;
|
||||
} & Omit<FormGroupProps, 'children'>;
|
||||
|
||||
export const FormPassword = ({
|
||||
name,
|
||||
ref,
|
||||
placeholder,
|
||||
...rest
|
||||
}: FormInputProps) => {
|
||||
const {value, onChange} = useFormidableContextElement(name);
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
function toggleVisible(): void {
|
||||
setShowPassword((value) => ! value)
|
||||
}
|
||||
return (
|
||||
<FormGroup
|
||||
name={name}
|
||||
{...rest}
|
||||
>
|
||||
<chakra.div position="relative" width="full">
|
||||
<Input
|
||||
ref={ref}
|
||||
type={showPassword? "text" : "password"}
|
||||
name={name}
|
||||
autoComplete={name}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
paddingRight="47px"
|
||||
/>
|
||||
<Button variant="ghost" onClick={toggleVisible} position="absolute" top="0" right="0" _hover={{bg:"#0000", shadow:"none", color:"black"}}>
|
||||
{showPassword? <LuEye/> : <LuEyeOff/>}
|
||||
</Button>
|
||||
</chakra.div>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
@ -3,7 +3,9 @@ import { useState } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
import { FormSelect } from '@/components/form/FormSelect';
|
||||
import { useFormidable } from '@/components/form/Formidable';
|
||||
import { useFormidable } from '@/components/formidable/FormidableConfig';
|
||||
|
||||
import { Formidable } from '../formidable';
|
||||
|
||||
export default {
|
||||
title: 'Components/FormSelect',
|
||||
@ -16,46 +18,49 @@ type BasicFormData = {
|
||||
export const Default = () => {
|
||||
const form = useFormidable<BasicFormData>({});
|
||||
return (
|
||||
<FormSelect
|
||||
label="Simple Title"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
keyInputValue="id"
|
||||
options={[{ id: 111 }, { id: 222 }, { id: 333 }, { id: 123 }]}
|
||||
/>
|
||||
<Formidable.From form={form}>
|
||||
<FormSelect
|
||||
label="Simple Title"
|
||||
name="data"
|
||||
keyInputValue="id"
|
||||
options={[{ id: 111 }, { id: 222 }, { id: 333 }, { id: 123 }]}
|
||||
/>
|
||||
</Formidable.From>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChangeKeys = () => {
|
||||
const form = useFormidable<BasicFormData>({});
|
||||
return (
|
||||
<FormSelect
|
||||
label="Simple Title for (ChangeKeys)"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
keyInputKey="key"
|
||||
keyInputValue="plop"
|
||||
options={[
|
||||
{ key: 111, plop: 'first Item' },
|
||||
{ key: 222, plop: 'Second Item' },
|
||||
{ key: 333, plop: 'third item' },
|
||||
]}
|
||||
/>
|
||||
<Formidable.From form={form}>
|
||||
<FormSelect
|
||||
label="Simple Title for (ChangeKeys)"
|
||||
name="data"
|
||||
keyInputKey="key"
|
||||
keyInputValue="plop"
|
||||
options={[
|
||||
{ key: 111, plop: 'first Item' },
|
||||
{ key: 222, plop: 'Second Item' },
|
||||
{ key: 333, plop: 'third item' },
|
||||
]}
|
||||
/>
|
||||
</Formidable.From>
|
||||
);
|
||||
};
|
||||
export const ChangeName = () => {
|
||||
const form = useFormidable<BasicFormData>({});
|
||||
return (
|
||||
<FormSelect
|
||||
label="Simple Title for (ChangeName)"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
options={[
|
||||
{ id: 111, name: 'first Item' },
|
||||
{ id: 222, name: 'Second Item' },
|
||||
{ id: 333, name: 'third item' },
|
||||
]}
|
||||
/>
|
||||
<Formidable.From form={form}>
|
||||
<FormSelect
|
||||
label="Simple Title for (ChangeName)"
|
||||
name="data"
|
||||
options={[
|
||||
{ id: 111, name: 'first Item' },
|
||||
{ id: 222, name: 'Second Item' },
|
||||
{ id: 333, name: 'third item' },
|
||||
]}
|
||||
/>
|
||||
</Formidable.From>
|
||||
);
|
||||
};
|
||||
export const AddableItem = () => {
|
||||
@ -66,27 +71,28 @@ export const AddableItem = () => {
|
||||
{ id: 333, name: 'third item' },
|
||||
]);
|
||||
return (
|
||||
<FormSelect
|
||||
label="Simple Title for (ChangeName)"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
addNewItem={(data: string) => {
|
||||
return new Promise((resolve, _rejects) => {
|
||||
let upperId = 0;
|
||||
setData((previous) => {
|
||||
previous.forEach((element) => {
|
||||
if (element['id'] > upperId) {
|
||||
upperId = element['id'];
|
||||
}
|
||||
<Formidable.From form={form}>
|
||||
<FormSelect
|
||||
label="Simple Title for (ChangeName)"
|
||||
name="data"
|
||||
addNewItem={(data: string) => {
|
||||
return new Promise((resolve, _rejects) => {
|
||||
let upperId = 0;
|
||||
setData((previous) => {
|
||||
previous.forEach((element) => {
|
||||
if (element['id'] > upperId) {
|
||||
upperId = element['id'];
|
||||
}
|
||||
});
|
||||
upperId++;
|
||||
return [...previous, { id: upperId, name: data }];
|
||||
});
|
||||
upperId++;
|
||||
return [...previous, { id: upperId, name: data }];
|
||||
resolve({ id: upperId, name: data });
|
||||
});
|
||||
resolve({ id: upperId, name: data });
|
||||
});
|
||||
}}
|
||||
options={data}
|
||||
/>
|
||||
}}
|
||||
options={data}
|
||||
/>
|
||||
</Formidable.From>
|
||||
);
|
||||
};
|
||||
|
||||
@ -94,18 +100,19 @@ export const DarkBackground = {
|
||||
render: () => {
|
||||
const form = useFormidable<BasicFormData>({});
|
||||
return (
|
||||
<Box p="4" color="white" bg="gray.800">
|
||||
<FormSelect
|
||||
label="Simple Title for (DarkBackground)"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
options={[
|
||||
{ id: 111, name: 'first Item' },
|
||||
{ id: 222, name: 'Second Item' },
|
||||
{ id: 333, name: 'third item' },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
<Formidable.From form={form}>
|
||||
<Box p="4" color="white" bg="gray.800">
|
||||
<FormSelect
|
||||
label="Simple Title for (DarkBackground)"
|
||||
name="data"
|
||||
options={[
|
||||
{ id: 111, name: 'first Item' },
|
||||
{ id: 222, name: 'Second Item' },
|
||||
{ id: 333, name: 'third item' },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Formidable.From>
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { RefObject } from 'react';
|
||||
|
||||
import { Text } from '@chakra-ui/react';
|
||||
|
||||
import { FormGroup } from '@/components/form/FormGroup';
|
||||
import { UseFormidableReturn } from '@/components/form/Formidable';
|
||||
import { SelectSingle } from '@/components/select/SelectSingle';
|
||||
|
||||
import { useFormidableContextElement } from '../formidable';
|
||||
|
||||
export type FormSelectProps = {
|
||||
// Generic Form input
|
||||
form: UseFormidableReturn;
|
||||
// Form: Name of the variable
|
||||
variableName: string;
|
||||
name: string;
|
||||
// Forward object reference
|
||||
ref?: RefObject<any>;
|
||||
// Form: Label of the input
|
||||
@ -32,8 +29,7 @@ export type FormSelectProps = {
|
||||
};
|
||||
|
||||
export const FormSelect = ({
|
||||
form,
|
||||
variableName,
|
||||
name,
|
||||
ref,
|
||||
placeholder,
|
||||
options,
|
||||
@ -43,23 +39,23 @@ export const FormSelect = ({
|
||||
addNewItem,
|
||||
...rest
|
||||
}: FormSelectProps) => {
|
||||
const { form, value, isModify, onChange, onRestore } =
|
||||
useFormidableContextElement(name);
|
||||
// if set add capability to add the search item
|
||||
const onCreate = !addNewItem
|
||||
? undefined
|
||||
: (data: string) => {
|
||||
addNewItem(data).then((data: object) => form.setValues({ [variableName]: data[keyInputKey] }));
|
||||
};
|
||||
addNewItem(data).then((data: object) =>
|
||||
form.setValues({ [name]: data[keyInputKey] })
|
||||
);
|
||||
};
|
||||
return (
|
||||
<FormGroup
|
||||
isModify={form.isModify[variableName]}
|
||||
onRestore={() => form.restoreValue({ [variableName]: true })}
|
||||
{...rest}
|
||||
>
|
||||
<FormGroup name={name} {...rest}>
|
||||
<SelectSingle
|
||||
ref={ref}
|
||||
value={form.values[variableName]}
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={(value) => form.setValues({ [variableName]: value })}
|
||||
onChange={(value) => onChange(value)}
|
||||
keyKey={keyInputKey}
|
||||
keyValue={keyInputValue}
|
||||
onCreate={onCreate}
|
||||
|
@ -3,7 +3,9 @@ import { useState } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
import { FormSelectMultiple } from '@/components/form/FormSelectMultiple';
|
||||
import { useFormidable } from '@/components/form/Formidable';
|
||||
import { useFormidable } from '@/components/formidable/FormidableConfig';
|
||||
|
||||
import { Formidable } from '../formidable';
|
||||
|
||||
export default {
|
||||
title: 'Components/FormSelectMultipleMultiple',
|
||||
@ -16,46 +18,49 @@ type BasicFormData = {
|
||||
export const Default = () => {
|
||||
const form = useFormidable<BasicFormData>({});
|
||||
return (
|
||||
<FormSelectMultiple
|
||||
label="Simple Title"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
keyInputValue="id"
|
||||
options={[{ id: 111 }, { id: 222 }, { id: 333 }, { id: 123 }]}
|
||||
/>
|
||||
<Formidable.From form={form}>
|
||||
<FormSelectMultiple
|
||||
label="Simple Title"
|
||||
name={'data'}
|
||||
keyInputValue="id"
|
||||
options={[{ id: 111 }, { id: 222 }, { id: 333 }, { id: 123 }]}
|
||||
/>
|
||||
</Formidable.From>
|
||||
);
|
||||
};
|
||||
|
||||
export const ChangeKeys = () => {
|
||||
const form = useFormidable<BasicFormData>({});
|
||||
return (
|
||||
<FormSelectMultiple
|
||||
label="Simple Title for (ChangeKeys)"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
keyInputKey="key"
|
||||
keyInputValue="plop"
|
||||
options={[
|
||||
{ key: 111, plop: 'first Item' },
|
||||
{ key: 222, plop: 'Second Item' },
|
||||
{ key: 333, plop: 'third item' },
|
||||
]}
|
||||
/>
|
||||
<Formidable.From form={form}>
|
||||
<FormSelectMultiple
|
||||
label="Simple Title for (ChangeKeys)"
|
||||
name="data"
|
||||
keyInputKey="key"
|
||||
keyInputValue="plop"
|
||||
options={[
|
||||
{ key: 111, plop: 'first Item' },
|
||||
{ key: 222, plop: 'Second Item' },
|
||||
{ key: 333, plop: 'third item' },
|
||||
]}
|
||||
/>
|
||||
</Formidable.From>
|
||||
);
|
||||
};
|
||||
export const ChangeName = () => {
|
||||
const form = useFormidable<BasicFormData>({});
|
||||
return (
|
||||
<FormSelectMultiple
|
||||
label="Simple Title for (ChangeName)"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
options={[
|
||||
{ id: 111, name: 'first Item' },
|
||||
{ id: 222, name: 'Second Item' },
|
||||
{ id: 333, name: 'third item' },
|
||||
]}
|
||||
/>
|
||||
<Formidable.From form={form}>
|
||||
<FormSelectMultiple
|
||||
label="Simple Title for (ChangeName)"
|
||||
name="data"
|
||||
options={[
|
||||
{ id: 111, name: 'first Item' },
|
||||
{ id: 222, name: 'Second Item' },
|
||||
{ id: 333, name: 'third item' },
|
||||
]}
|
||||
/>
|
||||
</Formidable.From>
|
||||
);
|
||||
};
|
||||
export const AddableItem = () => {
|
||||
@ -66,27 +71,28 @@ export const AddableItem = () => {
|
||||
{ id: 333, name: 'third item' },
|
||||
]);
|
||||
return (
|
||||
<FormSelectMultiple
|
||||
label="Simple Title for (ChangeName)"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
addNewItem={(data: string) => {
|
||||
return new Promise((resolve, _rejects) => {
|
||||
let upperId = 0;
|
||||
setData((previous) => {
|
||||
previous.forEach((element) => {
|
||||
if (element['id'] > upperId) {
|
||||
upperId = element['id'];
|
||||
}
|
||||
<Formidable.From form={form}>
|
||||
<FormSelectMultiple
|
||||
label="Simple Title for (ChangeName)"
|
||||
name="data"
|
||||
addNewItem={(data: string) => {
|
||||
return new Promise((resolve, _rejects) => {
|
||||
let upperId = 0;
|
||||
setData((previous) => {
|
||||
previous.forEach((element) => {
|
||||
if (element['id'] > upperId) {
|
||||
upperId = element['id'];
|
||||
}
|
||||
});
|
||||
upperId++;
|
||||
return [...previous, { id: upperId, name: data }];
|
||||
});
|
||||
upperId++;
|
||||
return [...previous, { id: upperId, name: data }];
|
||||
resolve({ id: upperId, name: data });
|
||||
});
|
||||
resolve({ id: upperId, name: data });
|
||||
});
|
||||
}}
|
||||
options={data}
|
||||
/>
|
||||
}}
|
||||
options={data}
|
||||
/>
|
||||
</Formidable.From>
|
||||
);
|
||||
};
|
||||
|
||||
@ -94,18 +100,19 @@ export const DarkBackground = {
|
||||
render: () => {
|
||||
const form = useFormidable<BasicFormData>({});
|
||||
return (
|
||||
<Box p="4" color="white" bg="gray.800">
|
||||
<FormSelectMultiple
|
||||
label="Simple Title for (DarkBackground)"
|
||||
form={form}
|
||||
variableName={'data'}
|
||||
options={[
|
||||
{ id: 111, name: 'first Item' },
|
||||
{ id: 222, name: 'Second Item' },
|
||||
{ id: 333, name: 'third item' },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
<Formidable.From form={form}>
|
||||
<Box p="4" color="white" bg="gray.800">
|
||||
<FormSelectMultiple
|
||||
label="Simple Title for (DarkBackground)"
|
||||
name="data"
|
||||
options={[
|
||||
{ id: 111, name: 'first Item' },
|
||||
{ id: 222, name: 'Second Item' },
|
||||
{ id: 333, name: 'third item' },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Formidable.From>
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { RefObject } from 'react';
|
||||
|
||||
import { FormGroup } from '@/components/form/FormGroup';
|
||||
import { UseFormidableReturn } from '@/components/form/Formidable';
|
||||
import { SelectMultiple } from '@/components/select/SelectMultiple';
|
||||
|
||||
import {
|
||||
useFormidableContext,
|
||||
useFormidableContextElement,
|
||||
} from '../formidable';
|
||||
|
||||
export type FormSelectMultipleProps = {
|
||||
// Generic Form input
|
||||
form: UseFormidableReturn;
|
||||
// Form: Name of the variable
|
||||
variableName: string;
|
||||
name: string;
|
||||
// Forward object reference
|
||||
ref?: RefObject<any>;
|
||||
// Form: Label of the input
|
||||
@ -28,8 +30,7 @@ export type FormSelectMultipleProps = {
|
||||
};
|
||||
|
||||
export const FormSelectMultiple = ({
|
||||
form,
|
||||
variableName,
|
||||
name,
|
||||
ref,
|
||||
placeholder,
|
||||
options,
|
||||
@ -38,23 +39,25 @@ export const FormSelectMultiple = ({
|
||||
addNewItem,
|
||||
...rest
|
||||
}: FormSelectMultipleProps) => {
|
||||
const { form, value, isModify, onChange, onRestore } =
|
||||
useFormidableContextElement(name);
|
||||
// if set add capability to add the search item
|
||||
const onCreate = !addNewItem
|
||||
? undefined
|
||||
: (data: string) => {
|
||||
addNewItem(data).then((data: object) => form.setValues({ [variableName]: [...(form.values[variableName] ?? []), data[keyInputKey]] }));
|
||||
};
|
||||
addNewItem(data).then((data: object) =>
|
||||
form.setValues({
|
||||
[name]: [...(form.values[name] ?? []), data[keyInputKey]],
|
||||
})
|
||||
);
|
||||
};
|
||||
return (
|
||||
<FormGroup
|
||||
isModify={form.isModify[variableName]}
|
||||
onRestore={() => form.restoreValue({ [variableName]: true })}
|
||||
{...rest}
|
||||
>
|
||||
<FormGroup name={name} {...rest}>
|
||||
<SelectMultiple
|
||||
//ref={ref}
|
||||
values={form.values[variableName]}
|
||||
values={value}
|
||||
options={options}
|
||||
onChange={(value) => form.setValues({ [variableName]: value })}
|
||||
onChange={(value) => onChange(value)}
|
||||
keyKey={keyInputKey}
|
||||
keyValue={keyInputValue}
|
||||
onCreate={onCreate}
|
||||
|
@ -3,11 +3,11 @@ import { RefObject } from 'react';
|
||||
import { Textarea } from '@chakra-ui/react';
|
||||
|
||||
import { FormGroup } from '@/components/form/FormGroup';
|
||||
import { UseFormidableReturn } from '@/components/form/Formidable';
|
||||
|
||||
import { useFormidableContextElement } from '../formidable';
|
||||
|
||||
export type FormTextareaProps = {
|
||||
form: UseFormidableReturn;
|
||||
variableName: string;
|
||||
name: string;
|
||||
ref?: RefObject<any>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
@ -15,22 +15,21 @@ export type FormTextareaProps = {
|
||||
};
|
||||
|
||||
export const FormTextarea = ({
|
||||
form,
|
||||
variableName,
|
||||
name,
|
||||
ref,
|
||||
placeholder,
|
||||
...rest
|
||||
}: FormTextareaProps) => {
|
||||
const { form, value, isModify, onChange, onRestore } =
|
||||
useFormidableContextElement(name);
|
||||
return (
|
||||
<FormGroup
|
||||
isModify={form.isModify[variableName]}
|
||||
onRestore={() => form.restoreValue({ [variableName]: true })}
|
||||
{...rest}
|
||||
>
|
||||
<FormGroup name={name} {...rest}>
|
||||
<Textarea
|
||||
name={name}
|
||||
ref={ref}
|
||||
value={form.values[variableName]}
|
||||
onChange={(e) => form.setValues({ [variableName]: e.target.value })}
|
||||
autoComplete={name}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
@ -1,66 +1,39 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
import { isArray, isNullOrUndefined, isObject } from '@/utils/validator';
|
||||
|
||||
const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key) && obj[key] === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
import { getDifferences, hasAnyTrue } from './utils';
|
||||
|
||||
export type FormidableConfig = {
|
||||
enableReset?: boolean;
|
||||
enableModifyNotification?: boolean;
|
||||
singleLineForm?: boolean;
|
||||
};
|
||||
const initialFormConfig: Required<FormidableConfig> = {
|
||||
enableReset: true,
|
||||
enableModifyNotification: true,
|
||||
singleLineForm: false,
|
||||
};
|
||||
|
||||
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 useFormidable = <TYPE extends object = object>({
|
||||
initialValues = {} as TYPE,
|
||||
configuration: inputConfiguration = initialFormConfig,
|
||||
resolver = (_data: TYPE) => {
|
||||
return {};
|
||||
},
|
||||
}: {
|
||||
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 [errors, setErrors] = useState<object>({});
|
||||
const [initialData, setInitialData] = useState<TYPE>(initialValues);
|
||||
const [isModify, setIsModify] = useState<{ [key: string]: boolean }>({});
|
||||
const [isFormModified, setIsFormModified] = useState<boolean>(false);
|
||||
@ -97,10 +70,11 @@ export const useFormidable = <TYPE extends object = object>({
|
||||
const ret = getDifferences(initialData, newValues);
|
||||
setIsModify(ret);
|
||||
setIsFormModified(hasAnyTrue(ret));
|
||||
setErrors(resolver(newValues));
|
||||
return newValues;
|
||||
});
|
||||
},
|
||||
[setValues, initialData]
|
||||
[setValues, initialData, setErrors, setIsFormModified, setIsModify]
|
||||
);
|
||||
const restoreValue = useCallback(
|
||||
(data: object) => {
|
||||
@ -133,7 +107,7 @@ export const useFormidable = <TYPE extends object = object>({
|
||||
[setValues, initialData, setIsFormModified, setIsModify]
|
||||
);
|
||||
const getDeltaData = useCallback(
|
||||
({ omit = [], only }: { omit?: string[]; only?: string[] }) => {
|
||||
({ omit = [], only }: { omit?: string[]; only?: string[] } = {}) => {
|
||||
const out = {};
|
||||
Object.keys(isModify).forEach((key) => {
|
||||
if (omit.includes(key) || (only && !only.includes(key))) {
|
||||
@ -161,6 +135,8 @@ export const useFormidable = <TYPE extends object = object>({
|
||||
restoreValue,
|
||||
setValues: setValuesExternal,
|
||||
values,
|
||||
errors,
|
||||
configuration,
|
||||
};
|
||||
};
|
||||
|
90
front/src/components/formidable/FormidableContext.tsx
Normal file
90
front/src/components/formidable/FormidableContext.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
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>
|
||||
);
|
||||
};
|
43
front/src/components/formidable/FormidableForm.tsx
Normal file
43
front/src/components/formidable/FormidableForm.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
import { useFormidable } from './FormidableConfig';
|
||||
import { FormidableContext } from './FormidableContext';
|
||||
|
||||
export interface FormidableFormProps<TYPE extends object = object> {
|
||||
form: ReturnType<typeof useFormidable<TYPE>>;
|
||||
children: ReactNode;
|
||||
onSubmit?: (data: TYPE) => void;
|
||||
onSubmitDelta?: (data: Partial<TYPE>) => void;
|
||||
}
|
||||
|
||||
export const FormidableForm = <TYPE extends object = object>({
|
||||
onSubmit,
|
||||
onSubmitDelta,
|
||||
form,
|
||||
children,
|
||||
}: FormidableFormProps<TYPE>) => {
|
||||
const handleSubmit = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const hasErrors = false; //Object.values(errors).some((err) => err);
|
||||
if (!hasErrors) {
|
||||
console.log(
|
||||
`request From submit !!! ${JSON.stringify(form.values, null, 2)}`
|
||||
);
|
||||
if (onSubmit) {
|
||||
onSubmit(form.values);
|
||||
}
|
||||
if (onSubmitDelta) {
|
||||
onSubmitDelta(form.getDeltaData());
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<FormidableContext form={form}>
|
||||
<Box as="form" onSubmit={handleSubmit}>
|
||||
{children}
|
||||
</Box>
|
||||
</FormidableContext>
|
||||
);
|
||||
};
|
8
front/src/components/formidable/Fromidable.tsx
Normal file
8
front/src/components/formidable/Fromidable.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export {
|
||||
type FormidableConfig as config,
|
||||
useFormidable,
|
||||
} from './FormidableConfig';
|
||||
export {
|
||||
FormidableForm as From,
|
||||
type FormidableFormProps as FormProps,
|
||||
} from './FormidableForm';
|
7
front/src/components/formidable/index.ts
Normal file
7
front/src/components/formidable/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export * as Formidable from './Fromidable';
|
||||
export {
|
||||
useFormidableContext,
|
||||
useFormidableContextElement,
|
||||
} from './FormidableContext';
|
||||
export { useFormidable } from './FormidableConfig';
|
||||
export { zodResolver } from './utils';
|
87
front/src/components/formidable/utils.ts
Normal file
87
front/src/components/formidable/utils.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import { isArray, isNullOrUndefined, isObject } from '@/utils/validator';
|
||||
|
||||
export const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key) && obj[key] === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export function getDifferences(
|
||||
obj1: object,
|
||||
obj2: object
|
||||
): { [key: string]: boolean } {
|
||||
// Create an empty object to store the differences
|
||||
const result: { [key: string]: boolean } = {};
|
||||
// Recursive function to compare values
|
||||
function compareValues(value1: any, value2: any): boolean {
|
||||
// If both values are objects, compare their properties recursively
|
||||
if (isObject(value1) && isObject(value2)) {
|
||||
return hasAnyTrue(getDifferences(value1, value2));
|
||||
}
|
||||
// If both values are arrays, compare their elements
|
||||
if (isArray(value1) && isArray(value2)) {
|
||||
//console.log(`Check is array: ${JSON.stringify(value1)} =?= ${JSON.stringify(value2)}`);
|
||||
if (value1.length !== value2.length) {
|
||||
return true;
|
||||
}
|
||||
for (let i = 0; i < value1.length; i++) {
|
||||
if (compareValues(value1[i], value2[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Otherwise, compare the values directly
|
||||
//console.log(`compare : ${value1} =?= ${value2}`);
|
||||
return value1 !== value2;
|
||||
}
|
||||
|
||||
// Get all keys from both objects
|
||||
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
|
||||
|
||||
// Iterate over all keys
|
||||
for (const key of allKeys) {
|
||||
if (compareValues(obj1[key], obj2[key])) {
|
||||
result[key] = true;
|
||||
} else {
|
||||
result[key] = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const zodResolver = (zodModel) => {
|
||||
return (data: any) => {
|
||||
try {
|
||||
console.log(`check resolver of: ${JSON.stringify(data, null, 2)}`);
|
||||
zodModel.parse(data);
|
||||
return {};
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
console.log(
|
||||
`catch error with resolver: ${JSON.stringify(error, null, 2)}`
|
||||
);
|
||||
const formattedErrors = error.issues.reduce(
|
||||
(acc, issue) => {
|
||||
if (issue.path.length > 0) {
|
||||
acc[issue.path[0]] = issue.message;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
console.log(`get errors: ${JSON.stringify(formattedErrors, null, 2)}`);
|
||||
return formattedErrors;
|
||||
}
|
||||
// prevent zod error
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
@ -41,7 +41,7 @@ export const DisplayGender = ({ dataGender }: DisplayGenderProps) => {
|
||||
userSelect="none"
|
||||
marginRight="auto"
|
||||
overflow="hidden"
|
||||
//TODO: noOfLines={[1, 2]}
|
||||
//TODO: noOfLines={[1, 2]}
|
||||
>
|
||||
{dataGender?.name}
|
||||
</Text>
|
||||
@ -52,7 +52,7 @@ export const DisplayGender = ({ dataGender }: DisplayGenderProps) => {
|
||||
userSelect="none"
|
||||
marginRight="auto"
|
||||
overflow="hidden"
|
||||
//TODO: noOfLines={1}
|
||||
//TODO: noOfLines={1}
|
||||
>
|
||||
{countTracksOnAGender} track{countTracksOnAGender >= 1 && 's'}
|
||||
</Text>
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
Flex,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { Flex, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import {
|
||||
MdAdminPanelSettings,
|
||||
MdDeleteForever,
|
||||
@ -20,17 +16,23 @@ import { FormInput } from '@/components/form/FormInput';
|
||||
import { FormTextarea } from '@/components/form/FormTextarea';
|
||||
import { useFormidable } from '@/components/form/Formidable';
|
||||
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useAlbumService, useSpecificAlbum } from '@/service/Album';
|
||||
import { useServiceContext } from '@/service/ServiceContext';
|
||||
import { useCountTracksWithAlbumId } from '@/service/Track';
|
||||
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 const AlbumEditPopUp = ({ }: AlbumEditPopUpProps) => {
|
||||
export const AlbumEditPopUp = ({}: AlbumEditPopUpProps) => {
|
||||
const { albumId } = useParams();
|
||||
const albumIdInt = isNullOrUndefined(albumId)
|
||||
? undefined
|
||||
|
@ -1,12 +1,6 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
Flex,
|
||||
Text,
|
||||
useDisclosure,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
|
||||
import { Button, Flex, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import {
|
||||
MdAdminPanelSettings,
|
||||
MdDeleteForever,
|
||||
@ -22,6 +16,13 @@ import { FormInput } from '@/components/form/FormInput';
|
||||
import { FormTextarea } from '@/components/form/FormTextarea';
|
||||
import { useFormidable } from '@/components/form/Formidable';
|
||||
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useArtistService, useSpecificArtist } from '@/service/Artist';
|
||||
import { useServiceContext } from '@/service/ServiceContext';
|
||||
import { useCountTracksOfAnArtist } from '@/service/Track';
|
||||
@ -29,7 +30,7 @@ import { isNullOrUndefined } from '@/utils/validator';
|
||||
|
||||
export type ArtistEditPopUpProps = {};
|
||||
|
||||
export const ArtistEditPopUp = ({ }: ArtistEditPopUpProps) => {
|
||||
export const ArtistEditPopUp = ({}: ArtistEditPopUpProps) => {
|
||||
const { artistId } = useParams();
|
||||
const artistIdInt = isNullOrUndefined(artistId)
|
||||
? undefined
|
||||
@ -217,7 +218,7 @@ export const ArtistEditPopUp = ({ }: ArtistEditPopUpProps) => {
|
||||
<Button
|
||||
onClick={() => setAdmin((value) => !value)}
|
||||
marginRight="auto"
|
||||
colorPalette={admin ? undefined : "@danger"}
|
||||
colorPalette={admin ? undefined : '@danger'}
|
||||
>
|
||||
{admin ? (
|
||||
<>
|
||||
|
@ -1,11 +1,15 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
UseDisclosureReturn,
|
||||
} from '@chakra-ui/react';
|
||||
import { Button, UseDisclosureReturn } from '@chakra-ui/react';
|
||||
|
||||
import {
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
|
||||
export type ConfirmPopUpProps = {
|
||||
title: string;
|
||||
body: string;
|
||||
@ -27,7 +31,8 @@ export const ConfirmPopUp = ({
|
||||
};
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
return (
|
||||
<DialogRoot role="alertdialog"
|
||||
<DialogRoot
|
||||
role="alertdialog"
|
||||
open={disclosure.open}
|
||||
//leastDestructiveRef={cancelRef}
|
||||
onOpenChange={disclosure.onClose}
|
||||
|
@ -1,19 +1,12 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
Flex,
|
||||
Text,
|
||||
useDisclosure,
|
||||
Button
|
||||
} from '@chakra-ui/react';
|
||||
import { Button, Flex, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import {
|
||||
MdAdminPanelSettings,
|
||||
MdDeleteForever,
|
||||
MdEdit,
|
||||
MdWarning,
|
||||
} from 'react-icons/md';
|
||||
|
||||
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Gender, GenderResource } from '@/back-api';
|
||||
@ -23,6 +16,13 @@ import { FormInput } from '@/components/form/FormInput';
|
||||
import { FormTextarea } from '@/components/form/FormTextarea';
|
||||
import { useFormidable } from '@/components/form/Formidable';
|
||||
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useGenderService, useSpecificGender } from '@/service/Gender';
|
||||
import { useServiceContext } from '@/service/ServiceContext';
|
||||
import { useCountTracksOfAGender } from '@/service/Track';
|
||||
@ -30,7 +30,7 @@ import { isNullOrUndefined } from '@/utils/validator';
|
||||
|
||||
export type GenderEditPopUpProps = {};
|
||||
|
||||
export const GenderEditPopUp = ({ }: GenderEditPopUpProps) => {
|
||||
export const GenderEditPopUp = ({}: GenderEditPopUpProps) => {
|
||||
const { genderId } = useParams();
|
||||
const genderIdInt = isNullOrUndefined(genderId)
|
||||
? undefined
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { Button, Flex, Progress, Text } from '@chakra-ui/react';
|
||||
|
||||
import {
|
||||
Flex,
|
||||
Progress,
|
||||
Text,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
|
||||
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
export type PopUpUploadProgressProps = {
|
||||
title: string;
|
||||
|
@ -1,13 +1,6 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
Text,
|
||||
useDisclosure,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from '@/components/ui/dialog';
|
||||
|
||||
import { Button, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import { MdAdminPanelSettings, MdDeleteForever, MdEdit } from 'react-icons/md';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
@ -20,6 +13,13 @@ import { FormSelectMultiple } from '@/components/form/FormSelectMultiple';
|
||||
import { FormTextarea } from '@/components/form/FormTextarea';
|
||||
import { useFormidable } from '@/components/form/Formidable';
|
||||
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
|
||||
import {
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useOrderedAlbums } from '@/service/Album';
|
||||
import { useOrderedArtists } from '@/service/Artist';
|
||||
import { useOrderedGenders } from '@/service/Gender';
|
||||
@ -29,7 +29,7 @@ import { isNullOrUndefined } from '@/utils/validator';
|
||||
|
||||
export type TrackEditPopUpProps = {};
|
||||
|
||||
export const TrackEditPopUp = ({ }: TrackEditPopUpProps) => {
|
||||
export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
|
||||
const { trackId } = useParams();
|
||||
const trackIdInt = isNullOrUndefined(trackId)
|
||||
? undefined
|
||||
|
@ -1,14 +1,6 @@
|
||||
import { RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
HStack,
|
||||
Input,
|
||||
Spinner,
|
||||
Tag,
|
||||
TagLabel,
|
||||
} from '@chakra-ui/react';
|
||||
import { Button, Flex, HStack, Input, Spinner, Tag } from '@chakra-ui/react';
|
||||
import { MdEdit, MdKeyboardArrowDown, MdKeyboardArrowUp } from 'react-icons/md';
|
||||
|
||||
import { SelectList, SelectListModel } from '@/components/select/SelectList';
|
||||
@ -102,14 +94,20 @@ export const SelectMultiple = ({
|
||||
const createNewItem = !onCreate
|
||||
? undefined
|
||||
: (data: string) => {
|
||||
onCreate(data);
|
||||
setCurrentSearch(undefined);
|
||||
};
|
||||
onCreate(data);
|
||||
setCurrentSearch(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" width="full" gap="0px">
|
||||
{selectedOptions && (
|
||||
<HStack wrap="wrap" gap="5px" justify="left" width="full" marginBottom="2px">
|
||||
<HStack
|
||||
wrap="wrap"
|
||||
gap="5px"
|
||||
justify="left"
|
||||
width="full"
|
||||
marginBottom="2px"
|
||||
>
|
||||
{selectedOptions.map((data) => (
|
||||
<Flex align="flex-start" key={data[keyKey]}>
|
||||
<Tag.Root
|
||||
@ -119,7 +117,10 @@ export const SelectMultiple = ({
|
||||
backgroundColor="green.800"
|
||||
>
|
||||
<Tag.Label>{data[keyValue] ?? `id=${data[keyKey]}`}</Tag.Label>
|
||||
<Tag.CloseTrigger boxSize="5" onClick={() => selectValue(data)} />
|
||||
<Tag.CloseTrigger
|
||||
boxSize="5"
|
||||
onClick={() => selectValue(data)}
|
||||
/>
|
||||
</Tag.Root>
|
||||
</Flex>
|
||||
))}
|
||||
@ -134,7 +135,13 @@ export const SelectMultiple = ({
|
||||
//onSubmit={onSubmit}
|
||||
onFocus={() => setShowList(true)}
|
||||
onBlur={() => setTimeout(() => setShowList(false), 200)}
|
||||
value={showList ? (currentSearch ?? '') : hasSuggestion ? `suggest: ${currentSearch}` : ''}
|
||||
value={
|
||||
showList
|
||||
? (currentSearch ?? '')
|
||||
: hasSuggestion
|
||||
? `suggest: ${currentSearch}`
|
||||
: ''
|
||||
}
|
||||
borderRadius="5px 0 0 5px"
|
||||
/>
|
||||
<Button
|
||||
|
@ -50,7 +50,9 @@ export const SelectSingle = ({
|
||||
onCreate ? suggestion : undefined
|
||||
);
|
||||
useEffect(() => {
|
||||
console.log(`Update suggestion : ${onCreate} ${suggestion} ==> ${onCreate ? suggestion : undefined} .. ${onCreate && !isNullOrUndefined(suggestion) ? true : false}`);
|
||||
console.log(
|
||||
`Update suggestion : ${onCreate} ${suggestion} ==> ${onCreate ? suggestion : undefined} .. ${onCreate && !isNullOrUndefined(suggestion) ? true : false}`
|
||||
);
|
||||
setCurrentSearch(onCreate ? suggestion : undefined);
|
||||
setHasSuggestion(onCreate && !isNullOrUndefined(suggestion) ? true : false);
|
||||
}, [suggestion]);
|
||||
@ -95,10 +97,10 @@ export const SelectSingle = ({
|
||||
const createNewItem = !onCreate
|
||||
? undefined
|
||||
: (data: string) => {
|
||||
onCreate(data);
|
||||
setCurrentSearch(undefined);
|
||||
setHasSuggestion(false);
|
||||
};
|
||||
onCreate(data);
|
||||
setCurrentSearch(undefined);
|
||||
setHasSuggestion(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" width="full" gap="0px">
|
||||
@ -110,7 +112,10 @@ export const SelectSingle = ({
|
||||
onFocus={() => setShowList(true)}
|
||||
onBlur={() => setTimeout(() => setShowList(false), 200)}
|
||||
value={
|
||||
showList ? (currentSearch ?? '') : (selectedOptions?.name ?? (hasSuggestion ? `suggest: ${currentSearch}` : ''))
|
||||
showList
|
||||
? (currentSearch ?? '')
|
||||
: (selectedOptions?.name ??
|
||||
(hasSuggestion ? `suggest: ${currentSearch}` : ''))
|
||||
}
|
||||
backgroundColor={
|
||||
showList || !selectedOptions ? undefined : 'green.800'
|
||||
|
@ -23,9 +23,7 @@ export const DisplayTrack = ({
|
||||
data={track?.covers}
|
||||
size="50"
|
||||
height="full"
|
||||
iconEmpty={
|
||||
trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />
|
||||
}
|
||||
iconEmpty={trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<Flex
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { LuMusic2, LuPlay } from 'react-icons/lu';
|
||||
|
||||
@ -26,15 +24,17 @@ export const DisplayTrackFull = ({
|
||||
const { dataGender } = useSpecificGender(track?.genderId);
|
||||
const { dataArtists } = useSpecificArtists(track?.artists);
|
||||
return (
|
||||
<Flex direction="row" width="full" height="full"
|
||||
data-testid="display-track-full">
|
||||
<Flex
|
||||
direction="row"
|
||||
width="full"
|
||||
height="full"
|
||||
data-testid="display-track-full"
|
||||
>
|
||||
<Covers
|
||||
data={track?.covers}
|
||||
size="60px"
|
||||
marginY="auto"
|
||||
iconEmpty={
|
||||
trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />
|
||||
}
|
||||
iconEmpty={trackActive?.id === track.id ? <LuPlay /> : <LuMusic2 />}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<Flex
|
||||
@ -71,7 +71,10 @@ export const DisplayTrackFull = ({
|
||||
marginY="auto"
|
||||
color={trackActive?.id === track.id ? 'green.700' : undefined}
|
||||
>
|
||||
<Text as="span" fontWeight="normal">Album:</Text> {dataAlbum.name}
|
||||
<Text as="span" fontWeight="normal">
|
||||
Album:
|
||||
</Text>{' '}
|
||||
{dataAlbum.name}
|
||||
</Text>
|
||||
)}
|
||||
{dataArtists && (
|
||||
@ -87,7 +90,10 @@ export const DisplayTrackFull = ({
|
||||
marginY="auto"
|
||||
color={trackActive?.id === track.id ? 'green.700' : undefined}
|
||||
>
|
||||
<Text as="span" fontWeight="normal">Artist(s):</Text> {dataArtists.map((data) => data.name).join(', ')}
|
||||
<Text as="span" fontWeight="normal">
|
||||
Artist(s):
|
||||
</Text>{' '}
|
||||
{dataArtists.map((data) => data.name).join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
{dataGender && (
|
||||
@ -103,12 +109,17 @@ export const DisplayTrackFull = ({
|
||||
marginY="auto"
|
||||
color={trackActive?.id === track.id ? 'green.700' : undefined}
|
||||
>
|
||||
<Text as="span" fontWeight="normal">Gender:</Text> {dataGender.name}
|
||||
<Text as="span" fontWeight="normal">
|
||||
Gender:
|
||||
</Text>{' '}
|
||||
{dataGender.name}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<ContextMenu elements={contextMenu}
|
||||
data-testid="display-track-full_context-menu" />
|
||||
<ContextMenu
|
||||
elements={contextMenu}
|
||||
data-testid="display-track-full_context-menu"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { Track } from '@/back-api';
|
||||
import { Covers } from '@/components/Cover';
|
||||
import { MenuElement } from '@/components/contextMenu/ContextMenu';
|
||||
import { useSpecificTrack } from '@/service/Track';
|
||||
|
||||
import { DisplayTrackFull } from './DisplayTrackFull';
|
||||
import { DisplayTrackSkeleton } from './DisplayTrackSkeleton';
|
||||
|
||||
export type DisplayTrackProps = {
|
||||
trackId: Track["id"];
|
||||
trackId: Track['id'];
|
||||
onClick?: () => void;
|
||||
contextMenu?: MenuElement[];
|
||||
};
|
||||
@ -18,11 +18,13 @@ export const DisplayTrackFullId = ({
|
||||
const { dataTrack } = useSpecificTrack(trackId);
|
||||
if (dataTrack) {
|
||||
return (
|
||||
<DisplayTrackFull track={dataTrack} onClick={onClick} contextMenu={contextMenu} />
|
||||
<DisplayTrackFull
|
||||
track={dataTrack}
|
||||
onClick={onClick}
|
||||
contextMenu={contextMenu}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DisplayTrackSkeleton />
|
||||
);
|
||||
return <DisplayTrackSkeleton />;
|
||||
}
|
||||
};
|
||||
|
@ -1,24 +1,25 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import type { GroupProps, SlotRecipeProps } from "@chakra-ui/react"
|
||||
import { Avatar as ChakraAvatar, Group } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>
|
||||
import type { GroupProps, SlotRecipeProps } from '@chakra-ui/react';
|
||||
import { Avatar as ChakraAvatar, Group } from '@chakra-ui/react';
|
||||
|
||||
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>;
|
||||
|
||||
export interface AvatarProps extends ChakraAvatar.RootProps {
|
||||
name?: string
|
||||
src?: string
|
||||
srcSet?: string
|
||||
loading?: ImageProps["loading"]
|
||||
icon?: React.ReactElement
|
||||
fallback?: React.ReactNode
|
||||
name?: string;
|
||||
src?: string;
|
||||
srcSet?: string;
|
||||
loading?: ImageProps['loading'];
|
||||
icon?: React.ReactElement;
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
||||
function Avatar(props, ref) {
|
||||
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
|
||||
props
|
||||
props;
|
||||
return (
|
||||
<ChakraAvatar.Root ref={ref} {...rest}>
|
||||
<AvatarFallback name={name} icon={icon}>
|
||||
@ -27,18 +28,18 @@ export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
|
||||
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
|
||||
{children}
|
||||
</ChakraAvatar.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface AvatarFallbackProps extends ChakraAvatar.FallbackProps {
|
||||
name?: string
|
||||
icon?: React.ReactElement
|
||||
name?: string;
|
||||
icon?: React.ReactElement;
|
||||
}
|
||||
|
||||
const AvatarFallback = React.forwardRef<HTMLDivElement, AvatarFallbackProps>(
|
||||
function AvatarFallback(props, ref) {
|
||||
const { name, icon, children, ...rest } = props
|
||||
const { name, icon, children, ...rest } = props;
|
||||
return (
|
||||
<ChakraAvatar.Fallback ref={ref} {...rest}>
|
||||
{children}
|
||||
@ -47,28 +48,28 @@ const AvatarFallback = React.forwardRef<HTMLDivElement, AvatarFallbackProps>(
|
||||
<ChakraAvatar.Icon asChild={!!icon}>{icon}</ChakraAvatar.Icon>
|
||||
)}
|
||||
</ChakraAvatar.Fallback>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function getInitials(name: string) {
|
||||
const names = name.trim().split(" ")
|
||||
const firstName = names[0] != null ? names[0] : ""
|
||||
const lastName = names.length > 1 ? names[names.length - 1] : ""
|
||||
const names = name.trim().split(' ');
|
||||
const firstName = names[0] != null ? names[0] : '';
|
||||
const lastName = names.length > 1 ? names[names.length - 1] : '';
|
||||
return firstName && lastName
|
||||
? `${firstName.charAt(0)}${lastName.charAt(0)}`
|
||||
: firstName.charAt(0)
|
||||
: firstName.charAt(0);
|
||||
}
|
||||
|
||||
interface AvatarGroupProps extends GroupProps, SlotRecipeProps<"avatar"> {}
|
||||
interface AvatarGroupProps extends GroupProps, SlotRecipeProps<'avatar'> {}
|
||||
|
||||
export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
||||
function AvatarGroup(props, ref) {
|
||||
const { size, variant, borderless, ...rest } = props
|
||||
const { size, variant, borderless, ...rest } = props;
|
||||
return (
|
||||
<ChakraAvatar.PropsProvider value={{ size, variant, borderless }}>
|
||||
<Group gap="0" spaceX="-3" ref={ref} {...rest} />
|
||||
</ChakraAvatar.PropsProvider>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,22 +1,23 @@
|
||||
import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react"
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ButtonProps as ChakraButtonProps } from '@chakra-ui/react';
|
||||
import {
|
||||
AbsoluteCenter,
|
||||
Button as ChakraButton,
|
||||
Span,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface ButtonLoadingProps {
|
||||
loading?: boolean
|
||||
loadingText?: React.ReactNode
|
||||
loading?: boolean;
|
||||
loadingText?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function Button(props, ref) {
|
||||
const { loading, disabled, loadingText, children, ...rest } = props
|
||||
const { loading, disabled, loadingText, children, ...rest } = props;
|
||||
return (
|
||||
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}>
|
||||
{loading && !loadingText ? (
|
||||
@ -35,6 +36,6 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
children
|
||||
)}
|
||||
</ChakraButton>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { Checkbox as ChakraCheckbox } from '@chakra-ui/react';
|
||||
|
||||
export interface CheckboxProps extends ChakraCheckbox.RootProps {
|
||||
icon?: React.ReactNode
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
rootRef?: React.Ref<HTMLLabelElement>
|
||||
icon?: React.ReactNode;
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||
rootRef?: React.Ref<HTMLLabelElement>;
|
||||
}
|
||||
|
||||
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
function Checkbox(props, ref) {
|
||||
const { icon, children, inputProps, rootRef, ...rest } = props
|
||||
const { icon, children, inputProps, rootRef, ...rest } = props;
|
||||
return (
|
||||
<ChakraCheckbox.Root ref={rootRef} {...rest}>
|
||||
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
|
||||
@ -20,6 +21,6 @@ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
|
||||
)}
|
||||
</ChakraCheckbox.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,9 +1,10 @@
|
||||
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 * as React from 'react';
|
||||
|
||||
export type CloseButtonProps = ButtonProps
|
||||
import type { ButtonProps } from '@chakra-ui/react';
|
||||
import { IconButton as ChakraIconButton } from '@chakra-ui/react';
|
||||
import { LuX } from 'react-icons/lu';
|
||||
|
||||
export type CloseButtonProps = ButtonProps;
|
||||
|
||||
export const CloseButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
@ -13,5 +14,5 @@ export const CloseButton = React.forwardRef<
|
||||
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
|
||||
{props.children ?? <LuX />}
|
||||
</ChakraIconButton>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -1,57 +1,58 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import type { IconButtonProps } from "@chakra-ui/react"
|
||||
import { ClientOnly, IconButton, Skeleton } from "@chakra-ui/react"
|
||||
import { ThemeProvider, useTheme } from "next-themes"
|
||||
import type { ThemeProviderProps } from "next-themes"
|
||||
import * as React from "react"
|
||||
import { LuMoon, LuSun } from "react-icons/lu"
|
||||
import * as React from 'react';
|
||||
|
||||
import type { IconButtonProps } from '@chakra-ui/react';
|
||||
import { ClientOnly, IconButton, Skeleton } from '@chakra-ui/react';
|
||||
import { ThemeProvider, useTheme } from 'next-themes';
|
||||
import type { ThemeProviderProps } from 'next-themes';
|
||||
import { LuMoon, LuSun } from 'react-icons/lu';
|
||||
|
||||
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||
|
||||
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export type ColorMode = "light" | "dark"
|
||||
export type ColorMode = 'light' | 'dark';
|
||||
|
||||
export interface UseColorModeReturn {
|
||||
colorMode: ColorMode
|
||||
setColorMode: (colorMode: ColorMode) => void
|
||||
toggleColorMode: () => void
|
||||
colorMode: ColorMode;
|
||||
setColorMode: (colorMode: ColorMode) => void;
|
||||
toggleColorMode: () => void;
|
||||
}
|
||||
|
||||
export function useColorMode(): UseColorModeReturn {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const toggleColorMode = () => {
|
||||
setTheme(resolvedTheme === "light" ? "dark" : "light")
|
||||
}
|
||||
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
return {
|
||||
colorMode: resolvedTheme as ColorMode,
|
||||
setColorMode: setTheme,
|
||||
toggleColorMode,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function useColorModeValue<T>(light: T, dark: T) {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "dark" ? dark : light
|
||||
const { colorMode } = useColorMode();
|
||||
return colorMode === 'dark' ? dark : light;
|
||||
}
|
||||
|
||||
export function ColorModeIcon() {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "dark" ? <LuMoon /> : <LuSun />
|
||||
const { colorMode } = useColorMode();
|
||||
return colorMode === 'dark' ? <LuMoon /> : <LuSun />;
|
||||
}
|
||||
|
||||
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||
interface ColorModeButtonProps extends Omit<IconButtonProps, 'aria-label'> {}
|
||||
|
||||
export const ColorModeButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ColorModeButtonProps
|
||||
>(function ColorModeButton(props, ref) {
|
||||
const { toggleColorMode } = useColorMode()
|
||||
const { toggleColorMode } = useColorMode();
|
||||
return (
|
||||
<ClientOnly fallback={<Skeleton boxSize="8" />}>
|
||||
<IconButton
|
||||
@ -63,13 +64,13 @@ export const ColorModeButton = React.forwardRef<
|
||||
{...props}
|
||||
css={{
|
||||
_icon: {
|
||||
width: "5",
|
||||
height: "5",
|
||||
width: '5',
|
||||
height: '5',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ColorModeIcon />
|
||||
</IconButton>
|
||||
</ClientOnly>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react';
|
||||
|
||||
import { CloseButton } from './close-button';
|
||||
|
||||
interface DialogContentProps extends ChakraDialog.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
backdrop?: boolean
|
||||
portalled?: boolean;
|
||||
portalRef?: React.RefObject<HTMLElement>;
|
||||
backdrop?: boolean;
|
||||
}
|
||||
|
||||
export const DialogContent = React.forwardRef<
|
||||
@ -18,7 +20,7 @@ export const DialogContent = React.forwardRef<
|
||||
portalRef,
|
||||
backdrop = true,
|
||||
...rest
|
||||
} = props
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
@ -29,8 +31,8 @@ export const DialogContent = React.forwardRef<
|
||||
</ChakraDialog.Content>
|
||||
</ChakraDialog.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const DialogCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
@ -48,15 +50,15 @@ export const DialogCloseTrigger = React.forwardRef<
|
||||
{props.children}
|
||||
</CloseButton>
|
||||
</ChakraDialog.CloseTrigger>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const DialogRoot = ChakraDialog.Root
|
||||
export const DialogFooter = ChakraDialog.Footer
|
||||
export const DialogHeader = ChakraDialog.Header
|
||||
export const DialogBody = ChakraDialog.Body
|
||||
export const DialogBackdrop = ChakraDialog.Backdrop
|
||||
export const DialogTitle = ChakraDialog.Title
|
||||
export const DialogDescription = ChakraDialog.Description
|
||||
export const DialogTrigger = ChakraDialog.Trigger
|
||||
export const DialogActionTrigger = ChakraDialog.ActionTrigger
|
||||
export const DialogRoot = ChakraDialog.Root;
|
||||
export const DialogFooter = ChakraDialog.Footer;
|
||||
export const DialogHeader = ChakraDialog.Header;
|
||||
export const DialogBody = ChakraDialog.Body;
|
||||
export const DialogBackdrop = ChakraDialog.Backdrop;
|
||||
export const DialogTitle = ChakraDialog.Title;
|
||||
export const DialogDescription = ChakraDialog.Description;
|
||||
export const DialogTrigger = ChakraDialog.Trigger;
|
||||
export const DialogActionTrigger = ChakraDialog.ActionTrigger;
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { Drawer as ChakraDrawer, Portal } from '@chakra-ui/react';
|
||||
|
||||
import { CloseButton } from './close-button';
|
||||
|
||||
interface DrawerContentProps extends ChakraDrawer.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
offset?: ChakraDrawer.ContentProps["padding"]
|
||||
portalled?: boolean;
|
||||
portalRef?: React.RefObject<HTMLElement>;
|
||||
offset?: ChakraDrawer.ContentProps['padding'];
|
||||
}
|
||||
|
||||
export const DrawerContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
DrawerContentProps
|
||||
>(function DrawerContent(props, ref) {
|
||||
const { children, portalled = true, portalRef, offset, ...rest } = props
|
||||
const { children, portalled = true, portalRef, offset, ...rest } = props;
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraDrawer.Positioner padding={offset}>
|
||||
@ -21,8 +23,8 @@ export const DrawerContent = React.forwardRef<
|
||||
</ChakraDrawer.Content>
|
||||
</ChakraDrawer.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const DrawerCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
@ -38,15 +40,15 @@ export const DrawerCloseTrigger = React.forwardRef<
|
||||
>
|
||||
<CloseButton size="sm" ref={ref} />
|
||||
</ChakraDrawer.CloseTrigger>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const DrawerTrigger = ChakraDrawer.Trigger
|
||||
export const DrawerRoot = ChakraDrawer.Root
|
||||
export const DrawerFooter = ChakraDrawer.Footer
|
||||
export const DrawerHeader = ChakraDrawer.Header
|
||||
export const DrawerBody = ChakraDrawer.Body
|
||||
export const DrawerBackdrop = ChakraDrawer.Backdrop
|
||||
export const DrawerDescription = ChakraDrawer.Description
|
||||
export const DrawerTitle = ChakraDrawer.Title
|
||||
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger
|
||||
export const DrawerTrigger = ChakraDrawer.Trigger;
|
||||
export const DrawerRoot = ChakraDrawer.Root;
|
||||
export const DrawerFooter = ChakraDrawer.Footer;
|
||||
export const DrawerHeader = ChakraDrawer.Header;
|
||||
export const DrawerBody = ChakraDrawer.Body;
|
||||
export const DrawerBackdrop = ChakraDrawer.Backdrop;
|
||||
export const DrawerDescription = ChakraDrawer.Description;
|
||||
export const DrawerTitle = ChakraDrawer.Title;
|
||||
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger;
|
||||
|
@ -1,17 +1,18 @@
|
||||
import { Field as ChakraField } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
|
||||
label?: React.ReactNode
|
||||
helperText?: React.ReactNode
|
||||
errorText?: React.ReactNode
|
||||
optionalText?: React.ReactNode
|
||||
import { Field as ChakraField } from '@chakra-ui/react';
|
||||
|
||||
export interface FieldProps extends Omit<ChakraField.RootProps, 'label'> {
|
||||
label?: React.ReactNode;
|
||||
helperText?: React.ReactNode;
|
||||
errorText?: React.ReactNode;
|
||||
optionalText?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
||||
function Field(props, ref) {
|
||||
const { label, children, helperText, errorText, optionalText, ...rest } =
|
||||
props
|
||||
props;
|
||||
return (
|
||||
<ChakraField.Root ref={ref} {...rest}>
|
||||
{label && (
|
||||
@ -28,6 +29,6 @@ export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
|
||||
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
|
||||
)}
|
||||
</ChakraField.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,15 +1,16 @@
|
||||
import type { BoxProps, InputElementProps } from "@chakra-ui/react"
|
||||
import { Group, InputElement } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import type { BoxProps, InputElementProps } from '@chakra-ui/react';
|
||||
import { Group, InputElement } from '@chakra-ui/react';
|
||||
|
||||
export interface InputGroupProps extends BoxProps {
|
||||
startElementProps?: InputElementProps
|
||||
endElementProps?: InputElementProps
|
||||
startElement?: React.ReactNode
|
||||
endElement?: React.ReactNode
|
||||
children: React.ReactElement<InputElementProps>
|
||||
startOffset?: InputElementProps["paddingStart"]
|
||||
endOffset?: InputElementProps["paddingEnd"]
|
||||
startElementProps?: InputElementProps;
|
||||
endElementProps?: InputElementProps;
|
||||
startElement?: React.ReactNode;
|
||||
endElement?: React.ReactNode;
|
||||
children: React.ReactElement<InputElementProps>;
|
||||
startOffset?: InputElementProps['paddingStart'];
|
||||
endOffset?: InputElementProps['paddingEnd'];
|
||||
}
|
||||
|
||||
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
||||
@ -20,13 +21,13 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
||||
endElement,
|
||||
endElementProps,
|
||||
children,
|
||||
startOffset = "6px",
|
||||
endOffset = "6px",
|
||||
startOffset = '6px',
|
||||
endOffset = '6px',
|
||||
...rest
|
||||
} = props
|
||||
} = props;
|
||||
|
||||
const child =
|
||||
React.Children.only<React.ReactElement<InputElementProps>>(children)
|
||||
React.Children.only<React.ReactElement<InputElementProps>>(children);
|
||||
|
||||
return (
|
||||
<Group ref={ref} {...rest}>
|
||||
@ -48,6 +49,6 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
|
||||
</InputElement>
|
||||
)}
|
||||
</Group>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,26 +1,27 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import { AbsoluteCenter, Menu as ChakraMenu, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import { LuCheck, LuChevronRight } from "react-icons/lu"
|
||||
import * as React from 'react';
|
||||
|
||||
import { AbsoluteCenter, Menu as ChakraMenu, Portal } from '@chakra-ui/react';
|
||||
import { LuCheck, LuChevronRight } from 'react-icons/lu';
|
||||
|
||||
interface MenuContentProps extends ChakraMenu.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
portalled?: boolean;
|
||||
portalRef?: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>(
|
||||
function MenuContent(props, ref) {
|
||||
const { portalled = true, portalRef, ...rest } = props
|
||||
const { portalled = true, portalRef, ...rest } = props;
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraMenu.Positioner>
|
||||
<ChakraMenu.Content ref={ref} {...rest} />
|
||||
</ChakraMenu.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const MenuArrow = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -30,8 +31,8 @@ export const MenuArrow = React.forwardRef<
|
||||
<ChakraMenu.Arrow ref={ref} {...props}>
|
||||
<ChakraMenu.ArrowTip />
|
||||
</ChakraMenu.Arrow>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const MenuCheckboxItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -44,14 +45,14 @@ export const MenuCheckboxItem = React.forwardRef<
|
||||
</ChakraMenu.ItemIndicator>
|
||||
{props.children}
|
||||
</ChakraMenu.CheckboxItem>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const MenuRadioItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraMenu.RadioItemProps
|
||||
>(function MenuRadioItem(props, ref) {
|
||||
const { children, ...rest } = props
|
||||
const { children, ...rest } = props;
|
||||
return (
|
||||
<ChakraMenu.RadioItem ps="8" ref={ref} {...rest}>
|
||||
<AbsoluteCenter axis="horizontal" left="4" asChild>
|
||||
@ -61,14 +62,14 @@ export const MenuRadioItem = React.forwardRef<
|
||||
</AbsoluteCenter>
|
||||
<ChakraMenu.ItemText>{children}</ChakraMenu.ItemText>
|
||||
</ChakraMenu.RadioItem>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const MenuItemGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChakraMenu.ItemGroupProps
|
||||
>(function MenuItemGroup(props, ref) {
|
||||
const { title, children, ...rest } = props
|
||||
const { title, children, ...rest } = props;
|
||||
return (
|
||||
<ChakraMenu.ItemGroup ref={ref} {...rest}>
|
||||
{title && (
|
||||
@ -78,33 +79,33 @@ export const MenuItemGroup = React.forwardRef<
|
||||
)}
|
||||
{children}
|
||||
</ChakraMenu.ItemGroup>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export interface MenuTriggerItemProps extends ChakraMenu.ItemProps {
|
||||
startIcon?: React.ReactNode
|
||||
startIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const MenuTriggerItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
MenuTriggerItemProps
|
||||
>(function MenuTriggerItem(props, ref) {
|
||||
const { startIcon, children, ...rest } = props
|
||||
const { startIcon, children, ...rest } = props;
|
||||
return (
|
||||
<ChakraMenu.TriggerItem ref={ref} {...rest}>
|
||||
{startIcon}
|
||||
{children}
|
||||
<LuChevronRight />
|
||||
</ChakraMenu.TriggerItem>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup
|
||||
export const MenuContextTrigger = ChakraMenu.ContextTrigger
|
||||
export const MenuRoot = ChakraMenu.Root
|
||||
export const MenuSeparator = ChakraMenu.Separator
|
||||
export const MenuRadioItemGroup = ChakraMenu.RadioItemGroup;
|
||||
export const MenuContextTrigger = ChakraMenu.ContextTrigger;
|
||||
export const MenuRoot = ChakraMenu.Root;
|
||||
export const MenuSeparator = ChakraMenu.Separator;
|
||||
|
||||
export const MenuItem = ChakraMenu.Item
|
||||
export const MenuItemText = ChakraMenu.ItemText
|
||||
export const MenuItemCommand = ChakraMenu.ItemCommand
|
||||
export const MenuTrigger = ChakraMenu.Trigger
|
||||
export const MenuItem = ChakraMenu.Item;
|
||||
export const MenuItemText = ChakraMenu.ItemText;
|
||||
export const MenuItemCommand = ChakraMenu.ItemCommand;
|
||||
export const MenuTrigger = ChakraMenu.Trigger;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { NumberInput as ChakraNumberInput } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { NumberInput as ChakraNumberInput } from '@chakra-ui/react';
|
||||
|
||||
export interface NumberInputProps extends ChakraNumberInput.RootProps {}
|
||||
|
||||
@ -7,7 +8,7 @@ export const NumberInputRoot = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
NumberInputProps
|
||||
>(function NumberInput(props, ref) {
|
||||
const { children, ...rest } = props
|
||||
const { children, ...rest } = props;
|
||||
return (
|
||||
<ChakraNumberInput.Root ref={ref} variant="outline" {...rest}>
|
||||
{children}
|
||||
@ -16,9 +17,9 @@ export const NumberInputRoot = React.forwardRef<
|
||||
<ChakraNumberInput.DecrementTrigger />
|
||||
</ChakraNumberInput.Control>
|
||||
</ChakraNumberInput.Root>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const NumberInputField = ChakraNumberInput.Input
|
||||
export const NumberInputScrubber = ChakraNumberInput.Scrubber
|
||||
export const NumberInputLabel = ChakraNumberInput.Label
|
||||
export const NumberInputField = ChakraNumberInput.Input;
|
||||
export const NumberInputScrubber = ChakraNumberInput.Scrubber;
|
||||
export const NumberInputLabel = ChakraNumberInput.Label;
|
||||
|
@ -1,25 +1,27 @@
|
||||
import { Popover as ChakraPopover, Portal } from "@chakra-ui/react"
|
||||
import { CloseButton } from "./close-button"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { Popover as ChakraPopover, Portal } from '@chakra-ui/react';
|
||||
|
||||
import { CloseButton } from './close-button';
|
||||
|
||||
interface PopoverContentProps extends ChakraPopover.ContentProps {
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
portalled?: boolean;
|
||||
portalRef?: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export const PopoverContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
PopoverContentProps
|
||||
>(function PopoverContent(props, ref) {
|
||||
const { portalled = true, portalRef, ...rest } = props
|
||||
const { portalled = true, portalRef, ...rest } = props;
|
||||
return (
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraPopover.Positioner>
|
||||
<ChakraPopover.Content ref={ref} {...rest} />
|
||||
</ChakraPopover.Positioner>
|
||||
</Portal>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const PopoverArrow = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@ -29,8 +31,8 @@ export const PopoverArrow = React.forwardRef<
|
||||
<ChakraPopover.Arrow {...props} ref={ref}>
|
||||
<ChakraPopover.ArrowTip />
|
||||
</ChakraPopover.Arrow>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const PopoverCloseTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
@ -47,13 +49,13 @@ export const PopoverCloseTrigger = React.forwardRef<
|
||||
>
|
||||
<CloseButton size="sm" />
|
||||
</ChakraPopover.CloseTrigger>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export const PopoverTitle = ChakraPopover.Title
|
||||
export const PopoverDescription = ChakraPopover.Description
|
||||
export const PopoverFooter = ChakraPopover.Footer
|
||||
export const PopoverHeader = ChakraPopover.Header
|
||||
export const PopoverRoot = ChakraPopover.Root
|
||||
export const PopoverBody = ChakraPopover.Body
|
||||
export const PopoverTrigger = ChakraPopover.Trigger
|
||||
export const PopoverTitle = ChakraPopover.Title;
|
||||
export const PopoverDescription = ChakraPopover.Description;
|
||||
export const PopoverFooter = ChakraPopover.Footer;
|
||||
export const PopoverHeader = ChakraPopover.Header;
|
||||
export const PopoverRoot = ChakraPopover.Root;
|
||||
export const PopoverBody = ChakraPopover.Body;
|
||||
export const PopoverTrigger = ChakraPopover.Trigger;
|
||||
|
@ -1,15 +1,13 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||
import {
|
||||
ColorModeProvider,
|
||||
type ColorModeProviderProps,
|
||||
} from "./color-mode"
|
||||
import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
|
||||
|
||||
import { ColorModeProvider, type ColorModeProviderProps } from './color-mode';
|
||||
|
||||
export function Provider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ChakraProvider value={defaultSystem}>
|
||||
<ColorModeProvider {...props} />
|
||||
</ChakraProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { RadioGroup as ChakraRadioGroup } from '@chakra-ui/react';
|
||||
|
||||
export interface RadioProps extends ChakraRadioGroup.ItemProps {
|
||||
rootRef?: React.Ref<HTMLDivElement>
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
|
||||
rootRef?: React.Ref<HTMLDivElement>;
|
||||
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
|
||||
function Radio(props, ref) {
|
||||
const { children, inputProps, rootRef, ...rest } = props
|
||||
const { children, inputProps, rootRef, ...rest } = props;
|
||||
return (
|
||||
<ChakraRadioGroup.Item ref={rootRef} {...rest}>
|
||||
<ChakraRadioGroup.ItemHiddenInput ref={ref} {...inputProps} />
|
||||
@ -17,8 +18,8 @@ export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
|
||||
<ChakraRadioGroup.ItemText>{children}</ChakraRadioGroup.ItemText>
|
||||
)}
|
||||
</ChakraRadioGroup.Item>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const RadioGroup = ChakraRadioGroup.Root
|
||||
export const RadioGroup = ChakraRadioGroup.Root;
|
||||
|
@ -1,23 +1,24 @@
|
||||
import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { Slider as ChakraSlider, For, HStack } from '@chakra-ui/react';
|
||||
|
||||
export interface SliderProps extends ChakraSlider.RootProps {
|
||||
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||
label?: React.ReactNode
|
||||
showValue?: boolean
|
||||
marks?: Array<number | { value: number; label: React.ReactNode }>;
|
||||
label?: React.ReactNode;
|
||||
showValue?: boolean;
|
||||
}
|
||||
|
||||
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
||||
function Slider(props, ref) {
|
||||
const { marks: marksProp, label, showValue, ...rest } = props
|
||||
const value = props.defaultValue ?? props.value
|
||||
const { marks: marksProp, label, showValue, ...rest } = props;
|
||||
const value = props.defaultValue ?? props.value;
|
||||
|
||||
const marks = marksProp?.map((mark) => {
|
||||
if (typeof mark === "number") return { value: mark, label: undefined }
|
||||
return mark
|
||||
})
|
||||
if (typeof mark === 'number') return { value: mark, label: undefined };
|
||||
return mark;
|
||||
});
|
||||
|
||||
const hasMarkLabel = !!marks?.some((mark) => mark.label)
|
||||
const hasMarkLabel = !!marks?.some((mark) => mark.label);
|
||||
|
||||
return (
|
||||
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
|
||||
@ -38,12 +39,12 @@ export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
|
||||
<SliderMarks marks={marks} />
|
||||
</ChakraSlider.Control>
|
||||
</ChakraSlider.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function SliderThumbs(props: { value?: number[] }) {
|
||||
const { value } = props
|
||||
const { value } = props;
|
||||
return (
|
||||
<For each={value}>
|
||||
{(_, index) => (
|
||||
@ -52,31 +53,31 @@ function SliderThumbs(props: { value?: number[] }) {
|
||||
</ChakraSlider.Thumb>
|
||||
)}
|
||||
</For>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface SliderMarksProps {
|
||||
marks?: Array<number | { value: number; label: React.ReactNode }>
|
||||
marks?: Array<number | { value: number; label: React.ReactNode }>;
|
||||
}
|
||||
|
||||
const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>(
|
||||
function SliderMarks(props, ref) {
|
||||
const { marks } = props
|
||||
if (!marks?.length) return null
|
||||
const { marks } = props;
|
||||
if (!marks?.length) return null;
|
||||
|
||||
return (
|
||||
<ChakraSlider.MarkerGroup ref={ref}>
|
||||
{marks.map((mark, index) => {
|
||||
const value = typeof mark === "number" ? mark : mark.value
|
||||
const label = typeof mark === "number" ? undefined : mark.label
|
||||
const value = typeof mark === 'number' ? mark : mark.value;
|
||||
const label = typeof mark === 'number' ? undefined : mark.label;
|
||||
return (
|
||||
<ChakraSlider.Marker key={index} value={value}>
|
||||
<ChakraSlider.MarkerIndicator />
|
||||
{label}
|
||||
</ChakraSlider.Marker>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</ChakraSlider.MarkerGroup>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import { RestErrorResponse } from "@/back-api";
|
||||
import {
|
||||
Toaster as ChakraToaster,
|
||||
Portal,
|
||||
@ -8,12 +7,14 @@ import {
|
||||
Stack,
|
||||
Toast,
|
||||
createToaster,
|
||||
} from "@chakra-ui/react"
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { RestErrorResponse } from '@/back-api';
|
||||
|
||||
export const toaster = createToaster({
|
||||
placement: "bottom-end",
|
||||
placement: 'bottom-end',
|
||||
pauseOnPageIdle: true,
|
||||
})
|
||||
});
|
||||
|
||||
export const toasterAPIError = (error: RestErrorResponse) => {
|
||||
toaster.create({
|
||||
@ -25,10 +26,10 @@ export const toasterAPIError = (error: RestErrorResponse) => {
|
||||
export const Toaster = () => {
|
||||
return (
|
||||
<Portal>
|
||||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
|
||||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}>
|
||||
{(toast) => (
|
||||
<Toast.Root width={{ md: "sm" }}>
|
||||
{toast.type === "loading" ? (
|
||||
<Toast.Root width={{ md: 'sm' }}>
|
||||
{toast.type === 'loading' ? (
|
||||
<Spinner size="sm" color="blue.solid" />
|
||||
) : (
|
||||
<Toast.Indicator />
|
||||
@ -47,5 +48,5 @@ export const Toaster = () => {
|
||||
)}
|
||||
</ChakraToaster>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
|
||||
import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react';
|
||||
|
||||
export interface TooltipProps extends ChakraTooltip.RootProps {
|
||||
showArrow?: boolean
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
content: React.ReactNode
|
||||
contentProps?: ChakraTooltip.ContentProps
|
||||
disabled?: boolean
|
||||
showArrow?: boolean;
|
||||
portalled?: boolean;
|
||||
portalRef?: React.RefObject<HTMLElement>;
|
||||
content: React.ReactNode;
|
||||
contentProps?: ChakraTooltip.ContentProps;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||
@ -21,9 +22,9 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||
contentProps,
|
||||
portalRef,
|
||||
...rest
|
||||
} = props
|
||||
} = props;
|
||||
|
||||
if (disabled) return children
|
||||
if (disabled) return children;
|
||||
|
||||
return (
|
||||
<ChakraTooltip.Root {...rest}>
|
||||
@ -41,6 +42,6 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||
</ChakraTooltip.Positioner>
|
||||
</Portal>
|
||||
</ChakraTooltip.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@ -1,28 +0,0 @@
|
||||
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);
|
@ -1,2 +0,0 @@
|
||||
import './axios';
|
||||
import './dayjs';
|
@ -1,2 +0,0 @@
|
||||
export const DATE_FORMAT = 'YYYY-MM-DD';
|
||||
export const DATE_FORMAT_FULL = 'dddd DD MMMM HH:mm';
|
@ -1,4 +1,4 @@
|
||||
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_HEIGHT = { base: "75px", lg: "120px" };
|
||||
export const BASE_WRAP_ICON_SIZE = { base: "50px", lg: "100px" };
|
||||
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_HEIGHT = { base: '75px', lg: '120px' };
|
||||
export const BASE_WRAP_ICON_SIZE = { base: '50px', lg: '100px' };
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './date'
|
@ -89,7 +89,9 @@ export const isDevelopmentEnvironment = () => {
|
||||
return import.meta.env.MODE === 'development';
|
||||
};
|
||||
|
||||
export const environment = isDevelopmentEnvironment() ? environment_local : environment_back_prod;
|
||||
export const environment = isDevelopmentEnvironment()
|
||||
? environment_local
|
||||
: environment_back_prod;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Box, Button, Center, Heading, Link, Text } from '@chakra-ui/react';
|
||||
import { Box, Center, Heading, Link, Text } from '@chakra-ui/react';
|
||||
import { MdControlCamera } from 'react-icons/md';
|
||||
|
||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
@ -10,15 +10,17 @@ export const Error401 = () => {
|
||||
<TopBar />
|
||||
<PageLayoutInfoCenter padding="25px">
|
||||
<Center>
|
||||
<MdControlCamera style={{ width: "250px", height: "250px", color: "orange" }} />
|
||||
<MdControlCamera
|
||||
style={{ width: '250px', height: '250px', color: 'orange' }}
|
||||
/>
|
||||
</Center>
|
||||
<Box textAlign="center">
|
||||
<Heading>Erreur 401</Heading>
|
||||
<Heading>Error 401</Heading>
|
||||
<Text color="red.600">
|
||||
Vous n'êtes pas autorisé a accéder a ce contenu.
|
||||
You are not authorized to access this content.
|
||||
</Text>
|
||||
<Link as="a" href="/">
|
||||
Retour à l'accueil
|
||||
Back to Homepage
|
||||
</Link>
|
||||
</Box>
|
||||
</PageLayoutInfoCenter>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Box, Button, Center, Heading, Link, Text } from '@chakra-ui/react';
|
||||
import { Box, Center, Heading, Link, Text } from '@chakra-ui/react';
|
||||
import { MdDangerous } from 'react-icons/md';
|
||||
|
||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
@ -10,14 +10,14 @@ export const Error403 = () => {
|
||||
<TopBar />
|
||||
<PageLayoutInfoCenter padding="25px">
|
||||
<Center>
|
||||
<MdDangerous style={{ width: "250px", height: "250px", color: "red" }} />
|
||||
<MdDangerous
|
||||
style={{ width: '250px', height: '250px', color: 'red' }}
|
||||
/>
|
||||
</Center>
|
||||
<Box textAlign="center">
|
||||
<Heading>Erreur 403</Heading>
|
||||
<Text color="orange.600">Cette page vous est interdite</Text>
|
||||
<Link href="/">
|
||||
Retour à l'accueil
|
||||
</Link>
|
||||
<Heading>Error 403</Heading>
|
||||
<Text color="orange.600">This page is forbidden to you.</Text>
|
||||
<Link href="/">Back to Homepage</Link>
|
||||
</Box>
|
||||
</PageLayoutInfoCenter>
|
||||
</>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Box, Button, Center, Heading, Link, Text } from '@chakra-ui/react';
|
||||
import { Box, Center, Heading, Link, Text } from '@chakra-ui/react';
|
||||
import { MdSignpost } from 'react-icons/md';
|
||||
|
||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
@ -10,16 +10,16 @@ export const Error404 = () => {
|
||||
<TopBar />
|
||||
<PageLayoutInfoCenter padding="25px">
|
||||
<Center>
|
||||
<MdSignpost style={{ width: "250px", height: "250px", color: "aqua" }} />
|
||||
<MdSignpost
|
||||
style={{ width: '250px', height: '250px', color: 'aqua' }}
|
||||
/>
|
||||
</Center>
|
||||
<Box textAlign="center">
|
||||
<Heading>Erreur 404</Heading>
|
||||
<Heading>Error 404</Heading>
|
||||
<Text color="gray.600">
|
||||
Cette page n'existe plus ou l'URL a changé
|
||||
This page no longer exists or the URL has changed.
|
||||
</Text>
|
||||
<Link href="/">
|
||||
Retour à l'accueil
|
||||
</Link>
|
||||
<Link href="/">Back to Homepage</Link>
|
||||
</Box>
|
||||
</PageLayoutInfoCenter>
|
||||
</>
|
||||
|
@ -1,11 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Alert,
|
||||
} from '@chakra-ui/react';
|
||||
import { Alert, AlertDescription, AlertTitle, Box } from '@chakra-ui/react';
|
||||
import {
|
||||
FallbackProps,
|
||||
ErrorBoundary as ReactErrorBoundary,
|
||||
@ -17,8 +12,15 @@ const ErrorFallback = ({ error }: FallbackProps) => {
|
||||
<Alert.Root status="error" borderRadius="md">
|
||||
<Alert.Indicator height="75px" width="75px" />
|
||||
<Box flex="1">
|
||||
<AlertTitle fontWeight="bold" fontSize="35px">An unexpected error has occurred.</AlertTitle>
|
||||
<AlertDescription padding="5" marginTop="3" fontSize="20px" lineHeight="1.4">
|
||||
<AlertTitle fontWeight="bold" fontSize="35px">
|
||||
An unexpected error has occurred.
|
||||
</AlertTitle>
|
||||
<AlertDescription
|
||||
padding="5"
|
||||
marginTop="3"
|
||||
fontSize="20px"
|
||||
lineHeight="1.4"
|
||||
>
|
||||
<br />
|
||||
{error.message}
|
||||
</AlertDescription>
|
||||
@ -28,6 +30,10 @@ const ErrorFallback = ({ error }: FallbackProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorBoundary: FC<React.PropsWithChildren<unknown>> = (props) => {
|
||||
return <ReactErrorBoundary FallbackComponent={ErrorFallback} {...props} />;
|
||||
export const ErrorBoundary = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<ReactErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
{children}
|
||||
</ReactErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { StrictMode } from 'react';
|
||||
|
||||
import { ChakraProvider } from '@chakra-ui/react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import App from '@/App';
|
||||
|
||||
import { ColorModeProvider } from './components/ui/color-mode';
|
||||
import { Toaster } from './components/ui/toaster';
|
||||
import { systemTheme } from './theme/theme';
|
||||
|
||||
// Render the app
|
||||
const rootElement = document.getElementById('root');
|
||||
@ -12,7 +16,10 @@ if (rootElement && !rootElement.innerHTML) {
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ColorModeProvider>
|
||||
<App />
|
||||
<ChakraProvider value={systemTheme}>
|
||||
<App />
|
||||
<Toaster />
|
||||
</ChakraProvider>
|
||||
</ColorModeProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
@ -1,15 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { createBrowserHistory } from 'history';
|
||||
import {
|
||||
unstable_HistoryRouter as HistoryRouter,
|
||||
Navigate,
|
||||
Route,
|
||||
Routes,
|
||||
} from 'react-router-dom';
|
||||
@ -14,8 +15,9 @@ import { HomePage } from '@/scene/home/HomePage';
|
||||
import { SSORoutes } from '@/scene/sso/SSORoutes';
|
||||
import { TrackRoutes } from '@/scene/track/TrackRoutes';
|
||||
import { useHasRight } from '@/service/session';
|
||||
import { SettingsPage } from './home/SettingsPage';
|
||||
|
||||
import { AddPage } from './home/AddPage';
|
||||
import { SettingsPage } from './home/SettingsPage';
|
||||
import { OnAirPage } from './onAir/OnAirPage';
|
||||
|
||||
export const AppRoutes = () => {
|
||||
@ -31,10 +33,11 @@ export const AppRoutes = () => {
|
||||
<Route path="sso/*" element={<SSORoutes />} />
|
||||
{isReadable ? (
|
||||
<>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="help" element={<HelpPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="add" element={<AddPage />} />
|
||||
<Route path="/" element={<Navigate to="home" replace />} />
|
||||
<Route path="home/*" element={<HomePage />} />
|
||||
<Route path="help/*" element={<HelpPage />} />
|
||||
<Route path="settings/*" element={<SettingsPage />} />
|
||||
<Route path="add/*" element={<AddPage />} />
|
||||
<Route path="on-air/*" element={<OnAirPage />} />
|
||||
<Route path="artist/*" element={<ArtistRoutes />} />
|
||||
<Route path="album/*" element={<AlbumRoutes />} />
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Box, Button, Flex, Text } from '@chakra-ui/react';
|
||||
import { LuDisc3 } from 'react-icons/lu';
|
||||
import { Md1kPlus, MdAdd, MdEdit, MdPlusOne } from 'react-icons/md';
|
||||
import { MdAdd, MdEdit } from 'react-icons/md';
|
||||
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { Covers } from '@/components/Cover';
|
||||
@ -11,11 +11,11 @@ import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar';
|
||||
import { AlbumEditPopUp } from '@/components/popup/AlbumEditPopUp';
|
||||
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
||||
import { DisplayTrackFull } from '@/components/track/DisplayTrackFull';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { BASE_WRAP_SPACING } from '@/constants/genericSpacing';
|
||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||
import { useSpecificAlbum } from '@/service/Album';
|
||||
import { useTracksOfAnAlbum } from '@/service/Track';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { BASE_WRAP_SPACING } from '@/constants/genericSpacing';
|
||||
|
||||
export const AlbumDetailPage = () => {
|
||||
const { albumId } = useParams();
|
||||
@ -64,8 +64,7 @@ export const AlbumDetailPage = () => {
|
||||
<MdEdit />
|
||||
</Button>
|
||||
</TopBar>
|
||||
<PageLayout
|
||||
data-testid="Album-detail-page_layout">
|
||||
<PageLayout data-testid="Album-detail-page_layout">
|
||||
<Flex
|
||||
direction="row"
|
||||
width="80%"
|
||||
@ -73,11 +72,7 @@ export const AlbumDetailPage = () => {
|
||||
padding="10px"
|
||||
gap="10px"
|
||||
>
|
||||
<Covers
|
||||
data={dataAlbum?.covers}
|
||||
iconEmpty={<LuDisc3 />}
|
||||
slideshow
|
||||
/>
|
||||
<Covers data={dataAlbum?.covers} iconEmpty={<LuDisc3 />} slideshow />
|
||||
<Flex direction="column" width="80%" marginRight="auto">
|
||||
<Text fontSize="24px" fontWeight="bold">
|
||||
{dataAlbum?.name}
|
||||
@ -128,7 +123,7 @@ export const AlbumDetailPage = () => {
|
||||
{
|
||||
icon: <MdAdd />,
|
||||
name: 'Add Playlist',
|
||||
onClick: () => { }
|
||||
onClick: () => {},
|
||||
},
|
||||
]}
|
||||
data-testid="Album-detail-page_display-detail"
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Flex, HStack } from '@chakra-ui/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { EmptyEnd } from '@/components/EmptyEnd';
|
||||
@ -8,10 +9,13 @@ import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
import { SearchInput } from '@/components/SearchInput';
|
||||
import { TopBar } from '@/components/TopBar/TopBar';
|
||||
import { DisplayAlbum } from '@/components/album/DisplayAlbum';
|
||||
import { useOrderedAlbums } from '@/service/Album';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { BASE_WRAP_SPACING, BASE_WRAP_WIDTH, BASE_WRAP_HEIGHT } from '@/constants/genericSpacing';
|
||||
import { Flex, HStack } from '@chakra-ui/react';
|
||||
import {
|
||||
BASE_WRAP_HEIGHT,
|
||||
BASE_WRAP_SPACING,
|
||||
BASE_WRAP_WIDTH,
|
||||
} from '@/constants/genericSpacing';
|
||||
import { useOrderedAlbums } from '@/service/Album';
|
||||
|
||||
export const AlbumsPage = () => {
|
||||
const [filterTitle, setFilterTitle] = useState<string | undefined>(undefined);
|
||||
@ -36,9 +40,16 @@ export const AlbumsPage = () => {
|
||||
<SearchInput onChange={setFilterTitle} />
|
||||
</TopBar>
|
||||
<PageLayout>
|
||||
<HStack wrap="wrap" gap={BASE_WRAP_SPACING} marginX="auto" padding="20px" justify="center">
|
||||
<HStack
|
||||
wrap="wrap"
|
||||
gap={BASE_WRAP_SPACING}
|
||||
marginX="auto"
|
||||
padding="20px"
|
||||
justify="center"
|
||||
>
|
||||
{dataAlbums.map((data) => (
|
||||
<Flex align="flex-start"
|
||||
<Flex
|
||||
align="flex-start"
|
||||
width={BASE_WRAP_WIDTH}
|
||||
height={BASE_WRAP_HEIGHT}
|
||||
border="1px"
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Box, Button, Flex, Text } from '@chakra-ui/react';
|
||||
import { LuDisc3, LuUser } from 'react-icons/lu';
|
||||
import { LuDisc3 } from 'react-icons/lu';
|
||||
import { MdAdd, MdEdit, MdPerson } from 'react-icons/md';
|
||||
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
@ -11,11 +11,11 @@ import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar';
|
||||
import { AlbumEditPopUp } from '@/components/popup/AlbumEditPopUp';
|
||||
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
||||
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||
import { useSpecificAlbum } from '@/service/Album';
|
||||
import { useSpecificArtist } from '@/service/Artist';
|
||||
import { useTracksOfAnAlbum } from '@/service/Track';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
|
||||
export const ArtistAlbumDetailPage = () => {
|
||||
const { artistId, albumId } = useParams();
|
||||
@ -92,11 +92,7 @@ export const ArtistAlbumDetailPage = () => {
|
||||
padding="10px"
|
||||
gap="10px"
|
||||
>
|
||||
<Covers
|
||||
data={dataAlbum?.covers}
|
||||
iconEmpty={<LuDisc3 />}
|
||||
slideshow
|
||||
/>
|
||||
<Covers data={dataAlbum?.covers} iconEmpty={<LuDisc3 />} slideshow />
|
||||
<Flex direction="column" width="80%" marginRight="auto">
|
||||
<Text fontSize="24px" fontWeight="bold">
|
||||
{dataAlbum?.name}
|
||||
@ -147,7 +143,7 @@ export const ArtistAlbumDetailPage = () => {
|
||||
{
|
||||
icon: <MdAdd />,
|
||||
name: 'Add Playlist',
|
||||
onClick: () => { }
|
||||
onClick: () => {},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, Flex, Text, HStack } from '@chakra-ui/react';
|
||||
import { Button, Flex, HStack, Text } from '@chakra-ui/react';
|
||||
import { LuUser } from 'react-icons/lu';
|
||||
import { MdEdit, MdGroup } from 'react-icons/md';
|
||||
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||
@ -10,10 +10,14 @@ import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar';
|
||||
import { DisplayAlbumId } from '@/components/album/DisplayAlbumId';
|
||||
import { ArtistEditPopUp } from '@/components/popup/ArtistEditPopUp';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import {
|
||||
BASE_WRAP_HEIGHT,
|
||||
BASE_WRAP_SPACING,
|
||||
BASE_WRAP_WIDTH,
|
||||
} from '@/constants/genericSpacing';
|
||||
import { useSpecificArtist } from '@/service/Artist';
|
||||
import { useAlbumIdsOfAnArtist } from '@/service/Track';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { BASE_WRAP_HEIGHT, BASE_WRAP_SPACING, BASE_WRAP_WIDTH } from '@/constants/genericSpacing';
|
||||
|
||||
export const ArtistDetailPage = () => {
|
||||
const { artistId } = useParams();
|
||||
@ -68,11 +72,7 @@ export const ArtistDetailPage = () => {
|
||||
padding="10px"
|
||||
gap="10px"
|
||||
>
|
||||
<Covers
|
||||
data={dataArtist?.covers}
|
||||
iconEmpty={<LuUser />}
|
||||
slideshow
|
||||
/>
|
||||
<Covers data={dataArtist?.covers} iconEmpty={<LuUser />} slideshow />
|
||||
<Flex direction="column" width="80%" marginRight="auto">
|
||||
<Text fontSize="24px" fontWeight="bold">
|
||||
{dataArtist?.name}
|
||||
@ -88,9 +88,16 @@ export const ArtistDetailPage = () => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<HStack wrap="wrap" gap={BASE_WRAP_SPACING} marginX="auto" padding="20px" justify="center">
|
||||
<HStack
|
||||
wrap="wrap"
|
||||
gap={BASE_WRAP_SPACING}
|
||||
marginX="auto"
|
||||
padding="20px"
|
||||
justify="center"
|
||||
>
|
||||
{albumIdsOfAnArtist?.map((data) => (
|
||||
<Flex align="flex-start"
|
||||
<Flex
|
||||
align="flex-start"
|
||||
width={BASE_WRAP_WIDTH}
|
||||
height={BASE_WRAP_HEIGHT}
|
||||
border="1px"
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button, Flex, Text, Tooltip, HStack, Span } from '@chakra-ui/react';
|
||||
import { Button, Flex, HStack, Span, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { LuUser } from 'react-icons/lu';
|
||||
import { MdOutlineForkRight } from 'react-icons/md';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Artist, Track } from '@/back-api';
|
||||
@ -10,14 +11,19 @@ import { EmptyEnd } from '@/components/EmptyEnd';
|
||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||
import { SearchInput } from '@/components/SearchInput';
|
||||
import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar';
|
||||
import { useOrderedArtists } from '@/service/Artist';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { BASE_WRAP_HEIGHT, BASE_WRAP_ICON_SIZE, BASE_WRAP_SPACING, BASE_WRAP_WIDTH } from '@/constants/genericSpacing';
|
||||
import { MdOutlineForkRight } from 'react-icons/md';
|
||||
import {
|
||||
BASE_WRAP_HEIGHT,
|
||||
BASE_WRAP_ICON_SIZE,
|
||||
BASE_WRAP_SPACING,
|
||||
BASE_WRAP_WIDTH,
|
||||
} from '@/constants/genericSpacing';
|
||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||
import { shuffleArray } from '@/utils/arrayTools';
|
||||
import { useOrderedArtists } from '@/service/Artist';
|
||||
import { useTrackService } from '@/service/Track';
|
||||
import { shuffleArray } from '@/utils/arrayTools';
|
||||
import { DataTools, TypeCheck } from '@/utils/data-tools';
|
||||
|
||||
const LIMIT_RANDOM_VALUES = 25;
|
||||
|
||||
export const ArtistsPage = () => {
|
||||
@ -30,7 +36,7 @@ export const ArtistsPage = () => {
|
||||
const { dataArtist } = useOrderedArtists(filterName);
|
||||
const { store: trackStore } = useTrackService();
|
||||
const onRandomPlay = () => {
|
||||
const data = shuffleArray(dataArtist)
|
||||
const data = shuffleArray(dataArtist);
|
||||
const playingList: number[] = [];
|
||||
for (let i = 0; i < Math.min(data.length, LIMIT_RANDOM_VALUES); i++) {
|
||||
const selectedArtist: Artist = data[i];
|
||||
@ -48,10 +54,12 @@ export const ArtistsPage = () => {
|
||||
if (listArtistTracks.length === 0) {
|
||||
continue;
|
||||
}
|
||||
playingList.push(listArtistTracks[Math.floor(Math.random() * listArtistTracks.length)].id);
|
||||
playingList.push(
|
||||
listArtistTracks[Math.floor(Math.random() * listArtistTracks.length)].id
|
||||
);
|
||||
}
|
||||
playInList(0, playingList)
|
||||
}
|
||||
playInList(0, playingList);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -59,14 +67,21 @@ export const ArtistsPage = () => {
|
||||
<SearchInput onChange={setFilterName} />
|
||||
<Tooltip.Root aria-label="Random play">
|
||||
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onRandomPlay}>
|
||||
<MdOutlineForkRight style={{ width: "100%", height: "100%" }} />
|
||||
<MdOutlineForkRight style={{ width: '100%', height: '100%' }} />
|
||||
</Button>
|
||||
</Tooltip.Root>
|
||||
</TopBar>
|
||||
<PageLayout>
|
||||
<HStack wrap="wrap" gap={BASE_WRAP_SPACING} marginX="auto" padding="20px" justify="center">
|
||||
<HStack
|
||||
wrap="wrap"
|
||||
gap={BASE_WRAP_SPACING}
|
||||
marginX="auto"
|
||||
padding="20px"
|
||||
justify="center"
|
||||
>
|
||||
{dataArtist?.map((data) => (
|
||||
<Flex align="flex-start"
|
||||
<Flex
|
||||
align="flex-start"
|
||||
width={BASE_WRAP_WIDTH}
|
||||
height={BASE_WRAP_HEIGHT}
|
||||
border="1px"
|
||||
@ -98,7 +113,7 @@ export const ArtistsPage = () => {
|
||||
>
|
||||
<Text
|
||||
textAlign="left"
|
||||
/*noOfLines={[1, 2]}*/
|
||||
/*noOfLines={[1, 2]}*/
|
||||
>
|
||||
<Span
|
||||
fontSize="20px"
|
||||
@ -109,7 +124,6 @@ export const ArtistsPage = () => {
|
||||
>
|
||||
{data.name}
|
||||
</Span>
|
||||
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@ -117,7 +131,7 @@ export const ArtistsPage = () => {
|
||||
))}
|
||||
</HStack>
|
||||
<EmptyEnd />
|
||||
</PageLayout >
|
||||
</PageLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -10,13 +10,12 @@ import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar';
|
||||
import { GenderEditPopUp } from '@/components/popup/GenderEditPopUp';
|
||||
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
||||
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
||||
import { DisplayTrackFull } from '@/components/track/DisplayTrackFull';
|
||||
//import { useTracksOfAGender } from '@/service/Track';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||
import { useSpecificGender } from '@/service/Gender';
|
||||
import { useTracksOfAGender } from '@/service/Track';
|
||||
//import { useTracksOfAGender } from '@/service/Track';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
|
||||
export const GenderDetailPage = () => {
|
||||
const { genderId } = useParams();
|
||||
@ -73,11 +72,7 @@ export const GenderDetailPage = () => {
|
||||
padding="10px"
|
||||
gap="10px"
|
||||
>
|
||||
<Covers
|
||||
data={dataGender?.covers}
|
||||
iconEmpty={<LuDisc3 />}
|
||||
slideshow
|
||||
/>
|
||||
<Covers data={dataGender?.covers} iconEmpty={<LuDisc3 />} slideshow />
|
||||
<Flex direction="column" width="80%" marginRight="auto">
|
||||
<Text fontSize="24px" fontWeight="bold">
|
||||
{dataGender?.name}
|
||||
@ -120,7 +115,7 @@ export const GenderDetailPage = () => {
|
||||
navigate(`/gender/${genderId}/edit-track/${data.id}`);
|
||||
},
|
||||
},
|
||||
{ name: 'Add Playlist', onClick: () => { } },
|
||||
{ name: 'Add Playlist', onClick: () => {} },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
@ -131,7 +126,7 @@ export const GenderDetailPage = () => {
|
||||
<Route path="edit-track/:trackId" element={<TrackEditPopUp />} />
|
||||
<Route path="edit-gender/:genderId" element={<GenderEditPopUp />} />
|
||||
</Routes>
|
||||
</PageLayout >
|
||||
</PageLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Flex, HStack } from '@chakra-ui/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { EmptyEnd } from '@/components/EmptyEnd';
|
||||
@ -8,9 +9,8 @@ import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
import { SearchInput } from '@/components/SearchInput';
|
||||
import { TopBar } from '@/components/TopBar/TopBar';
|
||||
import { DisplayGender } from '@/components/gender/DisplayGender';
|
||||
import { useOrderedGenders } from '@/service/Gender';
|
||||
import { useColorModeValue } from '@/components/ui/color-mode';
|
||||
import { Flex, HStack } from '@chakra-ui/react';
|
||||
import { useOrderedGenders } from '@/service/Gender';
|
||||
|
||||
export const GendersPage = () => {
|
||||
const [filterTitle, setFilterTitle] = useState<string | undefined>(undefined);
|
||||
@ -35,9 +35,16 @@ export const GendersPage = () => {
|
||||
<SearchInput onChange={setFilterTitle} />
|
||||
</TopBar>
|
||||
<PageLayout>
|
||||
<HStack wrap="wrap" gap="20px" marginX="auto" padding="20px" justify="center">
|
||||
<HStack
|
||||
wrap="wrap"
|
||||
gap="20px"
|
||||
marginX="auto"
|
||||
padding="20px"
|
||||
justify="center"
|
||||
>
|
||||
{dataGenders.map((data) => (
|
||||
<Flex align="flex-start"
|
||||
<Flex
|
||||
align="flex-start"
|
||||
width="270px"
|
||||
height="120px"
|
||||
border="1px"
|
||||
|
@ -1,12 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import {
|
||||
Flex,
|
||||
Input,
|
||||
Table,
|
||||
Text,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { Button, Flex, Input, Table, Text } from '@chakra-ui/react';
|
||||
import { LuTrash } from 'react-icons/lu';
|
||||
import { MdCloudUpload } from 'react-icons/md';
|
||||
|
||||
@ -527,9 +521,7 @@ export const AddPage = () => {
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button
|
||||
onClick={(e) =>
|
||||
removeElementFromList(data, e.target)
|
||||
}
|
||||
onClick={(e) => removeElementFromList(data, e.target)}
|
||||
>
|
||||
<LuTrash /> Remove
|
||||
</Button>
|
||||
@ -579,9 +571,7 @@ export const AddPage = () => {
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Text
|
||||
color={
|
||||
data.nameDetected === true ? 'red' : undefined
|
||||
}
|
||||
color={data.nameDetected === true ? 'red' : undefined}
|
||||
>
|
||||
{data.title}
|
||||
</Text>
|
||||
|
@ -5,9 +5,7 @@ export const HelpPage = () => {
|
||||
return (
|
||||
<>
|
||||
<TopBar title="Help" />
|
||||
<PageLayout>
|
||||
No help available right now
|
||||
</PageLayout>
|
||||
<PageLayout>No help available right now</PageLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user