[DEV] initiate migration

This commit is contained in:
Edouard DUPIN 2025-02-18 22:00:21 +01:00
parent 6f16bffe31
commit 86fec254aa
194 changed files with 35278 additions and 13 deletions

View File

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

View File

@ -9,6 +9,7 @@ import org.kar.archidata.externalRestApi.TsGenerateApi;
import org.kar.archidata.migration.MigrationSqlStep;
import org.kar.archidata.model.Data;
import org.kar.archidata.model.User;
import org.kar.archidata.model.token.JwtToken;
import org.kar.karideo.api.Front;
import org.kar.karideo.api.HealthCheck;
import org.kar.karideo.api.MediaResource;
@ -27,32 +28,35 @@ import org.slf4j.LoggerFactory;
public class Initialization extends MigrationSqlStep {
private static final Logger LOGGER = LoggerFactory.getLogger(Initialization.class);
public static final int KARSO_INITIALISATION_ID = 1;
public static final List<Class<?>> CLASSES_BASE = List.of(Data.class, Media.class, Type.class, Series.class, Season.class, User.class, UserMediaAdvancement.class);
public static final List<Class<?>> CLASSES_BASE = List.of(Data.class, Media.class, Type.class, Series.class,
Season.class, User.class, UserMediaAdvancement.class);
@Override
public String getName() {
return "Initialization";
}
public static void generateObjects() throws Exception {
LOGGER.info("Generate APIs");
final List<Class<?>> listOfResources = List.of(Front.class, HealthCheck.class, SeasonResource.class, SeriesResource.class, TypeResource.class, UserMediaAdvancementResource.class,
UserResource.class, MediaResource.class, DataResource.class);
final List<Class<?>> listOfResources = List.of(Front.class, HealthCheck.class, SeasonResource.class,
SeriesResource.class, TypeResource.class, UserMediaAdvancementResource.class, UserResource.class,
MediaResource.class, DataResource.class);
final AnalyzeApi api = new AnalyzeApi();
api.addAllApi(listOfResources);
TsGenerateApi.generateApi(api, "../front/src/app/back-api/");
api.addModel(JwtToken.class);
TsGenerateApi.generateApi(api, "../front/src/back-api/");
LOGGER.info("Generate APIs (DONE)");
}
@Override
public void generateStep() throws Exception {
for (final Class<?> clazz : CLASSES_BASE) {
addClass(clazz);
}
addAction((final DBAccess da) -> {
final List<Type> data = List.of(//
new Type("Documentary", "Documentary (animals, space, earth...)"), //
@ -85,7 +89,7 @@ public class Initialization extends MigrationSqlStep {
ALTER TABLE `userMediaAdvancement` AUTO_INCREMENT = 1000;
""", "mysql");
}
public static void dropAll(final DBAccess da) {
for (final Class<?> element : CLASSES_BASE) {
try {
@ -96,7 +100,7 @@ public class Initialization extends MigrationSqlStep {
}
}
}
public static void cleanAll(final DBAccess da) {
for (final Class<?> element : CLASSES_BASE) {
try {

1
front/.env Normal file
View File

@ -0,0 +1 @@
NODE_ENV=development

2
front/.env.production Normal file
View File

@ -0,0 +1,2 @@
# URL for database connection
VITE_API_BASE_URL=karusic/api/

27
front/.storybook/main.ts Normal file
View File

@ -0,0 +1,27 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
framework: {
name: '@storybook/react-vite',
options: {},
},
core: {
disableTelemetry: true,
builder: '@storybook/builder-vite',
},
stories: ['../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
staticDirs: ['../public'],
typescript: {
reactDocgen: false,
},
docs: {},
};
export default config;

View File

@ -0,0 +1,16 @@
<style>
html {
background: transparent !important;
}
.docs-story > :first-child {
padding: 0;
}
.docs-story > * {
background: transparent !important;
}
#root #start-ui-storybook-wrapper {
min-height: 100vh;
}
</style>

View File

@ -0,0 +1,34 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { ChakraProvider } from '@chakra-ui/react';
import { MemoryRouter } from 'react-router-dom';
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">
{children}
</Box>
);
};
export const decorators = [
(Story, context) => (
<ColorModeProvider>
<ChakraProvider value={systemTheme}>
{/* Using MemoryRouter to avoid route clashing with Storybook */}
<MemoryRouter>
<DocumentationWrapper>
<Story {...context} />
</DocumentationWrapper>
</MemoryRouter>
<Toaster />
</ChakraProvider>
</ColorModeProvider>
),
];

2
front/LICENSE Normal file
View File

@ -0,0 +1,2 @@
Proprietary
@copyright Edouard Dupin 2024

10637
front/config sample.yaml Normal file

File diff suppressed because it is too large Load Diff

0
front/doc/.keep Normal file
View File

13
front/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Karideo</title>
<link rel="icon" href="/favicon.ico" />
</head>
<body style="width:100vw;height:100vh;min-width:100%;min-height:100%;">
<div id="root" style="width:100%;height:100%;min-width:100%;min-height:100%;"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

9
front/knip.ts Normal file
View File

@ -0,0 +1,9 @@
import type { KnipConfig } from 'knip';
const config: KnipConfig = {
// Ignoring mostly shell binaries
ignoreBinaries: ['export', 'sleep'],
ignore: [],
};
export default config;

91
front/package.json Normal file
View File

@ -0,0 +1,91 @@
{
"name": "karideo",
"private": true,
"version": "0.0.1",
"description": "KAR web music application",
"author": {
"name": "Edouard DUPIN",
"email": "yui.heero@gmail.farm"
},
"license": "PROPRIETARY",
"engines": {
"node": ">=20"
},
"scripts": {
"update_packages": "ncu --target minor",
"upgrade_packages": "ncu --upgrade ",
"install_dependency": "pnpm install",
"test": "vitest run",
"test:watch": "vitest watch",
"build": "tsc && vite build",
"static:build": "pnpm build",
"dev": "vite",
"pretty": "prettier -w .",
"lint": "pnpm tsc --noEmit",
"storybook": "storybook dev -p 3001",
"storybook:build": "storybook build && mv ./storybook-static ./public/storybook"
},
"lint-staged": {
"*.{ts,tsx,js,jsx,json}": "prettier --write"
},
"dependencies": {
"@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": "19.0.0-rc.1",
"react-dom": "19.0.0-rc.1",
"react-error-boundary": "5.0.0",
"react-icons": "5.4.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.50.1",
"@storybook/addon-actions": "8.5.4",
"@storybook/addon-essentials": "8.5.4",
"@storybook/addon-links": "8.5.4",
"@storybook/addon-mdx-gfm": "8.5.4",
"@storybook/react": "8.5.4",
"@storybook/react-vite": "8.5.4",
"@storybook/theming": "8.5.4",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "16.2.0",
"@testing-library/user-event": "14.6.1",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/jest": "29.5.14",
"@types/node": "22.13.1",
"@types/react": "19.0.8",
"@types/react-dom": "19.0.3",
"@typescript-eslint/eslint-plugin": "8.24.0",
"@typescript-eslint/parser": "8.24.0",
"@vitejs/plugin-react": "4.3.4",
"eslint": "9.20.1",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "5.1.0",
"eslint-plugin-storybook": "0.11.2",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"knip": "5.44.0",
"lint-staged": "15.4.3",
"npm-check-updates": "^17.1.14",
"prettier": "3.5.0",
"puppeteer": "24.2.0",
"react-is": "19.0.0",
"storybook": "8.5.4",
"ts-node": "10.9.2",
"typescript": "5.7.3",
"vite": "6.1.0",
"vitest": "3.0.5"
}
}

View File

10742
front/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

16
front/prettier.config.js Normal file
View File

@ -0,0 +1,16 @@
// Using a JS file, allowing us to add comments
module.exports = {
// This plugins line is mandatory for the plugin to work with pnpm.
// https://github.com/trivago/prettier-plugin-sort-imports/blob/61d069711008c530f5a41ca4e254781abc5de358/README.md?plain=1#L89-L96
plugins: ['@trivago/prettier-plugin-sort-imports'],
endOfLine: 'lf',
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
arrowParens: 'always',
importOrder: ['^react$', '^(?!^react$|^@/|^[./]).*', '^@/(.*)$', '^[./]'],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
importOrderParserPlugins: ['jsx', 'typescript'],
};

BIN
front/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

20
front/src/App.tsx Normal file
View File

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

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="499"
height="498"
viewBox="0 0 499 498"
version="1.1"
id="svg10"
sodipodi:docname="avatar_generic.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs14" />
<sodipodi:namedview
id="namedview12"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="3.5060241"
inkscape:cx="344.97766"
inkscape:cy="288.78866"
inkscape:window-width="3838"
inkscape:window-height="2118"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:current-layer="svg10" />
<rect
style="fill:#4c83e5;fill-opacity:1;stroke:none;stroke-width:8.82841587;stroke-opacity:1"
width="500"
height="500"
x="0"
y="-1.9999847"
id="rect2" />
<path
style="fill:#4e4e4d;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="m 198.78572,292.40268 -11.97951,28.10434 -7.03868,-39.62542 -32.77188,20.51375 14.30166,-59.1095 -47.70972,20.8692 30.95257,-58.44261 -40.08325,-11.9709 50.99682,-31.08004 -30.32488,-33.92052 41.38608,6.88643 c 0,0 -27.42157,-58.130582 -26.08007,-58.130582 1.34149,0 54.85161,37.962212 54.85161,37.962212 l 6.10019,-52.959427 22.55992,46.026947 33.20732,-51.718401 8.75817,54.113371 53.78031,-55.134502 -14.88381,76.635492 112.00146,-17.67965 -84.26404,54.10353 65.61018,10.26713 -53.91421,37.51917 40.05564,55.00796 -51.48529,-23.57551 7.49544,56.99322 -27.79947,-24.64556 -3.80452,36.62241 -23.37869,-22.14564 -5.89389,23.82189 -20.64297,-29.48769 -15.46209,46.92367 -7.2608,-54.46889 -21.32424,51.07849 z"
id="path3162"
sodipodi:nodetypes="cccccccccccsccccccccccccccccccccccc" />
<path
style="fill:#fbd0a6;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 126.90172,351.92657 55.90379,145.23242 168.77912,0.48055 8.10502,-147.83696 -56.19339,-10.73582 -9.91368,3.09728 -8.25753,-48.27446 -8.77147,-41.82541 -73.97306,-0.86753 -4.65072,84.85557 z"
id="path3467"
sodipodi:nodetypes="ccccccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 45.319305,524.24985 50.878125,-168.64351 91.02866,-21.85078 124.81002,189.43887 15.4976,-12.33208 -130.63444,-191.36234 -9.67318,14.25555 124.81002,189.43887"
id="path275"
sodipodi:nodetypes="cccccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 443.88287,524.02559 393.00474,355.38208 301.97608,333.5313 177.16606,522.97017 161.66846,510.63809 292.3029,319.27575 301.97608,333.5313 177.16606,522.97017"
id="path275-6"
sodipodi:nodetypes="cccccccc" />
<path
style="fill:#fbd0a6;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 187.52213,139.10279 14.34593,25.22547 9.57434,-32.06638 13.85516,33.79118 18.44245,-38.89028 4.92331,44.11511 20.28515,-38.50102 -2.21466,39.35905 31.27764,-28.90273 -5.47875,45.83312 23.27252,-10.25342 -8.67174,30.59353 c 24.86464,-33.77835 23.21015,12.27629 3.94365,35.3922 l -12.95127,26.98572 -12.80079,22.10524 -26.61623,18.28625 -53.44338,-20.79546 c -21.13665,-20.42844 -23.443,-25.48798 -31.95313,-51.61993 -19.36867,-32.18928 -17.20493,-59.66994 4.27858,-38.00437 l -2.30548,-27.96686 11.07502,10.22035 -14.60569,-33.15484 22.1057,18.70679 z"
id="path9531"
sodipodi:nodetypes="cccccccccccccccccccccccc" />
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="256"
height="256"
viewBox="0 0 67.733333 67.733333"
version="1.1"
id="svg8"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
sodipodi:docname="ikon_gray.svg"
inkscape:export-filename="/home/heero/dev/perso/appl_pro/NoKomment/plugin/chrome/ikon.png"
inkscape:export-xdpi="7.1250005"
inkscape:export-ydpi="7.1250005"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2">
<filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter5338"
x="-0.12319682"
y="-0.081815216"
width="1.2463936"
height="1.1636304">
<feFlood
flood-opacity="1"
flood-color="rgb(255,255,255)"
result="flood"
id="feFlood5328" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite5330" />
<feGaussianBlur
in="composite1"
stdDeviation="2.1"
result="blur"
id="feGaussianBlur5332" />
<feOffset
dx="0"
dy="0"
result="offset"
id="feOffset5334" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite5336" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter1159"
x="-0.11802406"
width="1.2360481"
y="-0.078379973"
height="1.1567599">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="2.0118255"
id="feGaussianBlur1161" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="7.9195959"
inkscape:cx="100.06824"
inkscape:cy="115.66247"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:snap-text-baseline="false"
inkscape:window-width="3838"
inkscape:window-height="2118"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid4504"
originx="0"
originy="0"
spacingy="1"
spacingx="1"
units="px"
visible="true" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-229.26668)"
style="display:inline">
<g
id="text821-7"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:84.55024719px;line-height:1.25;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;opacity:1;fill:#2b3137;fill-opacity:1;stroke:none;stroke-width:2.11376619;stroke-opacity:1"
transform="matrix(0.8407653,0,0,0.83753055,-37.28971,3.4402954)"
aria-label="K">
<path
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccssccccssscccccccccccccccsscccccsssccccccccccccccssscsscsss"
inkscape:connector-curvature="0"
id="path823-5"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:84.5502px;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;opacity:0.775;fill:#2b3137;fill-opacity:1;stroke-width:2.11377;filter:url(#filter5338)"
d="m 65.200546,279.9533 h 8.949095 v 27.37877 l 25.568842,-27.37877 6.392207,6.84469 -20.455071,21.90302 20.455071,27.37876 -6.392207,5.47576 -19.176632,-27.37877 -6.39221,6.84469 v 20.53408 h -8.949095 z m 3.913007,39.48974 c -0.26846,-0.43226 -0.592093,-0.92734 -0.887692,-1.37494 l 0.02075,-0.022 c 0.456433,0.27687 0.977308,0.56258 1.422211,0.80755 l 0.407045,0.22999 -0.710959,0.75468 z m 0.591316,3.01367 0.778423,-0.82629 -0.642969,-1.02783 1.022328,-1.08519 1.052938,0.59264 0.80956,-0.85934 -4.631256,-2.27837 -0.913349,0.96952 z m 6.54394,-6.94635 0.762854,-0.80977 -1.289542,-1.22424 0.399591,-0.42416 1.938227,0.53566 0.856265,-0.90892 -2.195068,-0.54992 c 0.187965,-0.54159 0.09714,-1.11827 -0.429654,-1.61839 -0.850547,-0.80747 -1.705543,-0.42955 -2.421693,0.33064 l -1.198771,1.27249 z m -1.168715,-2.64352 -1.004195,-0.95334 0.373643,-0.39662 c 0.40478,-0.42967 0.738271,-0.54092 1.089465,-0.20751 0.351195,0.33341 0.31951,0.73118 -0.08527,1.16085 z m 7.416862,-3.98885 2.345648,-2.48989 -0.68044,-0.64598 -0.788801,0.83731 -2.216914,-2.10465 0.788802,-0.83731 -0.680439,-0.64598 -2.345647,2.48989 0.680439,0.64598 0.788802,-0.83731 2.216913,2.10465 -0.788801,0.83731 z m 6.206624,-6.58829 0.980813,-1.04113 c 0.939292,-0.99705 1.006613,-2.22712 -0.222565,-3.39405 -1.229181,-1.16694 -2.41593,-0.99961 -3.401932,0.047 l -0.934107,0.99155 z m 0.115038,-1.43521 -2.271787,-2.15674 0.124547,-0.13221 c 0.52932,-0.56189 1.150798,-0.69191 2.006834,0.12078 0.856035,0.81268 0.794297,1.47411 0.264954,2.03597 z m 6.226516,-5.29631 2.293753,-2.4348 -0.68044,-0.64599 -1.525707,1.61953 -0.823112,-0.78143 1.250665,-1.32757 -0.674951,-0.64077 -1.250666,1.32757 -0.71885,-0.68245 1.473813,-1.56444 -0.680438,-0.64598 -2.241858,2.37972 z m 7.372646,-7.6936 c 0.80437,-0.85384 0.71213,-2.05798 -0.51156,-3.2197 -1.19626,-1.13568 -2.393561,-1.15578 -3.197932,-0.30194 -0.80437,0.85383 -0.717618,2.05277 0.478638,3.18844 1.223694,1.16173 2.426484,1.18703 3.230854,0.3332 z m -0.6969,-0.66161 c -0.35289,0.37458 -0.97662,0.23116 -1.750342,-0.50339 -0.7408,-0.70328 -0.918234,-1.32045 -0.565349,-1.69503 0.352885,-0.37459 0.976611,-0.23116 1.717411,0.47213 0.77373,0.73454 0.95116,1.3517 0.59828,1.72629 z" />
</g>
<g
id="text821"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:84.55024719px;line-height:1.25;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#2b3137;fill-opacity:1;stroke:none;stroke-width:2.11376619;stroke-opacity:1;filter:url(#filter1159)"
transform="matrix(1.0347881,0,0,0.96638144,-54.239583,-37.041665)"
aria-label="K" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -0,0 +1,128 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import {
ObjectId,
} from "../model";
export namespace DataResource {
/**
* Get back some data from the data environment (with a beautiful name (permit download with basic name)
*/
export function retrieveDataFull({
restConfig,
queries,
params,
data,
}: {
restConfig: RESTConfig,
queries: {
Authorization?: string,
},
params: {
name: string,
oid: ObjectId,
},
data: string,
}): Promise<object> {
return RESTRequestJson({
restModel: {
endPoint: "/data/{oid}/{name}",
requestType: HTTPRequestModel.GET,
},
restConfig,
params,
queries,
data,
});
};
/**
* Get back some data from the data environment
*/
export function retrieveDataId({
restConfig,
queries,
params,
data,
}: {
restConfig: RESTConfig,
queries: {
Authorization?: string,
},
params: {
oid: ObjectId,
},
data: string,
}): Promise<object> {
return RESTRequestJson({
restModel: {
endPoint: "/data/{oid}",
requestType: HTTPRequestModel.GET,
},
restConfig,
params,
queries,
data,
});
};
/**
* Get a thumbnail of from the data environment (if resize is possible)
*/
export function retrieveDataThumbnailId({
restConfig,
queries,
params,
data,
}: {
restConfig: RESTConfig,
queries: {
Authorization?: string,
},
params: {
oid: ObjectId,
},
data: string,
}): Promise<object> {
return RESTRequestJson({
restModel: {
endPoint: "/data/thumbnail/{oid}",
requestType: HTTPRequestModel.GET,
},
restConfig,
params,
queries,
data,
});
};
/**
* Insert a new data in the data environment
*/
export function uploadFile({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: {
file: File,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/data//upload/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
},
restConfig,
data,
});
};
}

View File

@ -0,0 +1,6 @@
/**
* Interface of the server (auto-generated code)
*/
export namespace Front {
}

View File

@ -0,0 +1,35 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTConfig,
RESTRequestJson,
} from "../rest-tools";
import {
HealthResult,
isHealthResult,
} from "../model";
export namespace HealthCheck {
/**
* Get the server state (health)
*/
export function getHealth({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<HealthResult> {
return RESTRequestJson({
restModel: {
endPoint: "/health_check/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isHealthResult);
};
}

View File

@ -0,0 +1,12 @@
/**
* Interface of the server (auto-generated code)
*/
export * from "./data-resource"
export * from "./front"
export * from "./health-check"
export * from "./media-resource"
export * from "./season-resource"
export * from "./series-resource"
export * from "./type-resource"
export * from "./user-media-advancement-resource"
export * from "./user-resource"

View File

@ -0,0 +1,214 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTCallbacks,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Long,
Media,
MediaWrite,
UUID,
ZodMedia,
isMedia,
} from "../model";
export namespace MediaResource {
/**
* Get a specific Media with his ID
*/
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<Media> {
return RESTRequestJson({
restModel: {
endPoint: "/media/{id}",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isMedia);
};
export const ZodGetsTypeReturn = zod.array(ZodMedia);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
/**
* Get all Media
*/
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/media/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
/**
* Modify a specific Media
*/
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: MediaWrite,
}): Promise<Media> {
return RESTRequestJson({
restModel: {
endPoint: "/media/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isMedia);
};
/**
* Remove a specific Media
*/
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/media/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
/**
* Remove a specific cover of a media
*/
export function removeCover({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
coverId: UUID,
id: Long,
},
}): Promise<Media> {
return RESTRequestJson({
restModel: {
endPoint: "/media/{id}/cover/{coverId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isMedia);
};
/**
* Upload a new season cover media
*/
export function uploadCover({
restConfig,
params,
data,
callbacks,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: {
file: File,
},
callbacks?: RESTCallbacks,
}): Promise<Media> {
return RESTRequestJson({
restModel: {
endPoint: "/media/{id}/cover",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
callbacks,
}, isMedia);
};
/**
* Create a new Media
*/
export function uploadFile({
restConfig,
data,
callbacks,
}: {
restConfig: RESTConfig,
data: {
fileName: string,
file: File,
series: string,
universe: string,
season: string,
episode: string,
typeId: string,
title: string,
},
callbacks?: RESTCallbacks,
}): Promise<Media> {
return RESTRequestJson({
restModel: {
endPoint: "/media/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
callbacks,
}, isMedia);
};
}

View File

@ -0,0 +1,203 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTCallbacks,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Long,
Season,
SeasonWrite,
UUID,
ZodSeason,
isSeason,
} from "../model";
export namespace SeasonResource {
/**
* Get all season
*/
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<Season> {
return RESTRequestJson({
restModel: {
endPoint: "/season/{id}",
requestType: HTTPRequestModel.GET,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isSeason);
};
export const ZodGetsTypeReturn = zod.array(ZodSeason);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
/**
* Get a specific Season with his ID
*/
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/season/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
/**
* Modify a specific season
*/
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: SeasonWrite,
}): Promise<Season> {
return RESTRequestJson({
restModel: {
endPoint: "/season/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isSeason);
};
/**
* Create a new season
*/
export function post({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: SeasonWrite,
}): Promise<Season> {
return RESTRequestJson({
restModel: {
endPoint: "/season/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
}, isSeason);
};
/**
* Remove a specific season
*/
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/season/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
/**
* Remove a specific cover of a season
*/
export function removeCover({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
coverId: UUID,
id: Long,
},
}): Promise<Season> {
return RESTRequestJson({
restModel: {
endPoint: "/season/{id}/cover/{coverId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isSeason);
};
/**
* Upload a new season cover season
*/
export function uploadCover({
restConfig,
params,
data,
callbacks,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: {
file: File,
},
callbacks?: RESTCallbacks,
}): Promise<Season> {
return RESTRequestJson({
restModel: {
endPoint: "/season/{id}/cover",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
callbacks,
}, isSeason);
};
}

View File

@ -0,0 +1,203 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTCallbacks,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Long,
Series,
SeriesWrite,
UUID,
ZodSeries,
isSeries,
} from "../model";
export namespace SeriesResource {
/**
* Get a specific Series with his ID
*/
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<Series> {
return RESTRequestJson({
restModel: {
endPoint: "/series/{id}",
requestType: HTTPRequestModel.GET,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isSeries);
};
export const ZodGetsTypeReturn = zod.array(ZodSeries);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
/**
* Get all Series
*/
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/series/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
/**
* Modify a specific Series
*/
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: SeriesWrite,
}): Promise<Series> {
return RESTRequestJson({
restModel: {
endPoint: "/series/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isSeries);
};
/**
* Create a new Series
*/
export function post({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: SeriesWrite,
}): Promise<Series> {
return RESTRequestJson({
restModel: {
endPoint: "/series/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
}, isSeries);
};
/**
* Remove a specific Series
*/
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/series/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
/**
* Remove a specific Series of a season
*/
export function removeCover({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
coverId: UUID,
id: Long,
},
}): Promise<Series> {
return RESTRequestJson({
restModel: {
endPoint: "/series/{id}/cover/{coverId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isSeries);
};
/**
* Upload a new season cover Series
*/
export function uploadCover({
restConfig,
params,
data,
callbacks,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: {
file: File,
},
callbacks?: RESTCallbacks,
}): Promise<Series> {
return RESTRequestJson({
restModel: {
endPoint: "/series/{id}/cover",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
callbacks,
}, isSeries);
};
}

View File

@ -0,0 +1,203 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTCallbacks,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Long,
Type,
TypeWrite,
UUID,
ZodType,
isType,
} from "../model";
export namespace TypeResource {
/**
* Get a specific Type with his ID
*/
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<Type> {
return RESTRequestJson({
restModel: {
endPoint: "/type/{id}",
requestType: HTTPRequestModel.GET,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isType);
};
export const ZodGetsTypeReturn = zod.array(ZodType);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
/**
* Get all Type
*/
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/type/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
/**
* Modify a specific Type
*/
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: TypeWrite,
}): Promise<Type> {
return RESTRequestJson({
restModel: {
endPoint: "/type/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isType);
};
/**
* Create a new Type
*/
export function post({
restConfig,
data,
}: {
restConfig: RESTConfig,
data: TypeWrite,
}): Promise<Type> {
return RESTRequestJson({
restModel: {
endPoint: "/type/",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
data,
}, isType);
};
/**
* Remove a specific Type
*/
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/type/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
/**
* Remove a specific cover of a type
*/
export function removeCover({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
coverId: UUID,
id: Long,
},
}): Promise<Type> {
return RESTRequestJson({
restModel: {
endPoint: "/type/{id}/cover/{coverId}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isType);
};
/**
* Upload a new season cover Type
*/
export function uploadCover({
restConfig,
params,
data,
callbacks,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: {
file: File,
},
callbacks?: RESTCallbacks,
}): Promise<Type> {
return RESTRequestJson({
restModel: {
endPoint: "/type/{id}/cover",
requestType: HTTPRequestModel.POST,
contentType: HTTPMimeType.MULTIPART,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
callbacks,
}, isType);
};
}

View File

@ -0,0 +1,124 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTConfig,
RESTRequestJson,
RESTRequestVoid,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Long,
MediaInformationsDeltaWrite,
UserMediaAdvancement,
ZodUserMediaAdvancement,
isUserMediaAdvancement,
} from "../model";
export namespace UserMediaAdvancementResource {
/**
* Get a specific user advancement with his ID
*/
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<UserMediaAdvancement> {
return RESTRequestJson({
restModel: {
endPoint: "/advancement/{id}",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isUserMediaAdvancement);
};
export const ZodGetsTypeReturn = zod.array(ZodUserMediaAdvancement);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
/**
* Get all user advancement
*/
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/advancement/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
/**
* Modify a user advancement
*/
export function patch({
restConfig,
params,
data,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
data: MediaInformationsDeltaWrite,
}): Promise<UserMediaAdvancement> {
return RESTRequestJson({
restModel: {
endPoint: "/advancement/{id}",
requestType: HTTPRequestModel.PATCH,
contentType: HTTPMimeType.JSON,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
data,
}, isUserMediaAdvancement);
};
/**
* Remove a specific user advancement
*/
export function remove({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<void> {
return RESTRequestVoid({
restModel: {
endPoint: "/advancement/{id}",
requestType: HTTPRequestModel.DELETE,
contentType: HTTPMimeType.TEXT_PLAIN,
},
restConfig,
params,
});
};
}

View File

@ -0,0 +1,93 @@
/**
* Interface of the server (auto-generated code)
*/
import {
HTTPMimeType,
HTTPRequestModel,
RESTConfig,
RESTRequestJson,
} from "../rest-tools";
import { z as zod } from "zod"
import {
Long,
UserKarideo,
UserOut,
ZodUserKarideo,
isUserKarideo,
isUserOut,
} from "../model";
export namespace UserResource {
/**
* Get a specific user data
*/
export function get({
restConfig,
params,
}: {
restConfig: RESTConfig,
params: {
id: Long,
},
}): Promise<UserKarideo> {
return RESTRequestJson({
restModel: {
endPoint: "/users/{id}",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
params,
}, isUserKarideo);
};
/**
* Get the user personal data
*/
export function getMe({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<UserOut> {
return RESTRequestJson({
restModel: {
endPoint: "/users/me",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isUserOut);
};
export const ZodGetsTypeReturn = zod.array(ZodUserKarideo);
export type GetsTypeReturn = zod.infer<typeof ZodGetsTypeReturn>;
export function isGetsTypeReturn(data: any): data is GetsTypeReturn {
try {
ZodGetsTypeReturn.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGetsTypeReturn' error=${e}`);
return false;
}
}
/**
* Get all the users
*/
export function gets({
restConfig,
}: {
restConfig: RESTConfig,
}): Promise<GetsTypeReturn> {
return RESTRequestJson({
restModel: {
endPoint: "/users/",
requestType: HTTPRequestModel.GET,
accept: HTTPMimeType.JSON,
},
restConfig,
}, isGetsTypeReturn);
};
}

View File

@ -0,0 +1,7 @@
/**
* Interface of the server (auto-generated code)
*/
export * from "./model";
export * from "./api";
export * from "./rest-tools";

View File

@ -0,0 +1,67 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
import {ZodObjectId} from "./object-id";
import {ZodOIDGenericDataSoftDelete, ZodOIDGenericDataSoftDeleteWrite } from "./oid-generic-data-soft-delete";
export const ZodData = ZodOIDGenericDataSoftDelete.extend({
/**
* Sha512 of the data
*/
sha512: zod.string().max(512),
/**
* Mime -type of the media
*/
mimeType: zod.string().max(512),
/**
* Size in Byte of the data
*/
size: ZodLong,
/**
* Unique ObjectID of the object
*/
oid: ZodObjectId.readonly(),
});
export type Data = zod.infer<typeof ZodData>;
export function isData(data: any): data is Data {
try {
ZodData.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodData' error=${e}`);
return false;
}
}
export const ZodDataWrite = ZodOIDGenericDataSoftDeleteWrite.extend({
/**
* Sha512 of the data
*/
sha512: zod.string().max(512).optional(),
/**
* Mime -type of the media
*/
mimeType: zod.string().max(512).optional(),
/**
* Size in Byte of the data
*/
size: ZodLong.optional(),
});
export type DataWrite = zod.infer<typeof ZodDataWrite>;
export function isDataWrite(data: any): data is DataWrite {
try {
ZodDataWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodDataWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodFloat = zod.number();
export type Float = zod.infer<typeof ZodFloat>;

View File

@ -0,0 +1,39 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodGenericData, ZodGenericDataWrite } from "./generic-data";
export const ZodGenericDataSoftDelete = ZodGenericData.extend({
/**
* Deleted state
*/
deleted: zod.boolean().readonly().optional(),
});
export type GenericDataSoftDelete = zod.infer<typeof ZodGenericDataSoftDelete>;
export function isGenericDataSoftDelete(data: any): data is GenericDataSoftDelete {
try {
ZodGenericDataSoftDelete.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataSoftDelete' error=${e}`);
return false;
}
}
export const ZodGenericDataSoftDeleteWrite = ZodGenericDataWrite;
export type GenericDataSoftDeleteWrite = zod.infer<typeof ZodGenericDataSoftDeleteWrite>;
export function isGenericDataSoftDeleteWrite(data: any): data is GenericDataSoftDeleteWrite {
try {
ZodGenericDataSoftDeleteWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataSoftDeleteWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,40 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
import {ZodGenericTiming, ZodGenericTimingWrite } from "./generic-timing";
export const ZodGenericData = ZodGenericTiming.extend({
/**
* Unique Id of the object
*/
id: ZodLong.readonly(),
});
export type GenericData = zod.infer<typeof ZodGenericData>;
export function isGenericData(data: any): data is GenericData {
try {
ZodGenericData.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericData' error=${e}`);
return false;
}
}
export const ZodGenericDataWrite = ZodGenericTimingWrite;
export type GenericDataWrite = zod.infer<typeof ZodGenericDataWrite>;
export function isGenericDataWrite(data: any): data is GenericDataWrite {
try {
ZodGenericDataWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericDataWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,45 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodIsoDate} from "./iso-date";
export const ZodGenericTiming = zod.object({
/**
* Create time of the object
*/
createdAt: ZodIsoDate.readonly().optional(),
/**
* When update the object
*/
updatedAt: ZodIsoDate.readonly().optional(),
});
export type GenericTiming = zod.infer<typeof ZodGenericTiming>;
export function isGenericTiming(data: any): data is GenericTiming {
try {
ZodGenericTiming.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericTiming' error=${e}`);
return false;
}
}
export const ZodGenericTimingWrite = zod.object({
});
export type GenericTimingWrite = zod.infer<typeof ZodGenericTimingWrite>;
export function isGenericTimingWrite(data: any): data is GenericTimingWrite {
try {
ZodGenericTimingWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodGenericTimingWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,36 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodHealthResult = zod.object({
});
export type HealthResult = zod.infer<typeof ZodHealthResult>;
export function isHealthResult(data: any): data is HealthResult {
try {
ZodHealthResult.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodHealthResult' error=${e}`);
return false;
}
}
export const ZodHealthResultWrite = zod.object({
});
export type HealthResultWrite = zod.infer<typeof ZodHealthResultWrite>;
export function isHealthResultWrite(data: any): data is HealthResultWrite {
try {
ZodHealthResultWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodHealthResultWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,30 @@
/**
* Interface of the server (auto-generated code)
*/
export * from "./data"
export * from "./float"
export * from "./generic-data"
export * from "./generic-data-soft-delete"
export * from "./generic-timing"
export * from "./health-result"
export * from "./integer"
export * from "./iso-date"
export * from "./jwt-header"
export * from "./jwt-payload"
export * from "./jwt-token"
export * from "./long"
export * from "./media"
export * from "./media-informations-delta"
export * from "./object-id"
export * from "./oid-generic-data"
export * from "./oid-generic-data-soft-delete"
export * from "./rest-error-response"
export * from "./season"
export * from "./series"
export * from "./timestamp"
export * from "./type"
export * from "./user"
export * from "./user-karideo"
export * from "./user-media-advancement"
export * from "./user-out"
export * from "./uuid"

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodInteger = zod.number().safe();
export type Integer = zod.infer<typeof ZodInteger>;

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodIsoDate = zod.string().datetime({ precision: 3 });
export type IsoDate = zod.infer<typeof ZodIsoDate>;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodLong = zod.number();
export type Long = zod.infer<typeof ZodLong>;

View File

@ -0,0 +1,36 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodMediaInformationsDelta = zod.object({
});
export type MediaInformationsDelta = zod.infer<typeof ZodMediaInformationsDelta>;
export function isMediaInformationsDelta(data: any): data is MediaInformationsDelta {
try {
ZodMediaInformationsDelta.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodMediaInformationsDelta' error=${e}`);
return false;
}
}
export const ZodMediaInformationsDeltaWrite = zod.object({
});
export type MediaInformationsDeltaWrite = zod.infer<typeof ZodMediaInformationsDeltaWrite>;
export function isMediaInformationsDeltaWrite(data: any): data is MediaInformationsDeltaWrite {
try {
ZodMediaInformationsDeltaWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodMediaInformationsDeltaWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,122 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodLong} from "./long";
import {ZodInteger} from "./integer";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodMedia = ZodGenericDataSoftDelete.extend({
/**
* Name of the media (this represent the title)
*/
name: zod.string(),
/**
* Description of the media
*/
description: zod.string().optional(),
/**
* Foreign Key Id of the data
*/
dataId: ZodObjectId,
/**
* Type of the media
*/
typeId: ZodLong.optional(),
/**
* Series reference of the media
*/
seriesId: ZodLong.optional(),
/**
* Season reference of the media
*/
seasonId: ZodLong.optional(),
/**
* Episode Id
*/
episode: ZodInteger.optional(),
date: ZodInteger.optional(),
/**
* Creation years of the media
*/
time: ZodInteger.optional(),
/**
* Limitation Age of the media
*/
ageLimit: ZodInteger.optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).optional(),
});
export type Media = zod.infer<typeof ZodMedia>;
export function isMedia(data: any): data is Media {
try {
ZodMedia.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodMedia' error=${e}`);
return false;
}
}
export const ZodMediaWrite = ZodGenericDataSoftDeleteWrite.extend({
/**
* Name of the media (this represent the title)
*/
name: zod.string().optional(),
/**
* Description of the media
*/
description: zod.string().nullable().optional(),
/**
* Foreign Key Id of the data
*/
dataId: ZodObjectId.optional(),
/**
* Type of the media
*/
typeId: ZodLong.nullable().optional(),
/**
* Series reference of the media
*/
seriesId: ZodLong.nullable().optional(),
/**
* Season reference of the media
*/
seasonId: ZodLong.nullable().optional(),
/**
* Episode Id
*/
episode: ZodInteger.nullable().optional(),
date: ZodInteger.nullable().optional(),
/**
* Creation years of the media
*/
time: ZodInteger.nullable().optional(),
/**
* Limitation Age of the media
*/
ageLimit: ZodInteger.nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
});
export type MediaWrite = zod.infer<typeof ZodMediaWrite>;
export function isMediaWrite(data: any): data is MediaWrite {
try {
ZodMediaWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodMediaWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodObjectId = zod.string().length(24, "Invalid ObjectId length").regex(/^[a-fA-F0-9]{24}$/, "Invalid ObjectId format");
export type ObjectId = zod.infer<typeof ZodObjectId>;

View File

@ -0,0 +1,44 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodOIDGenericData, ZodOIDGenericDataWrite } from "./oid-generic-data";
export const ZodOIDGenericDataSoftDelete = ZodOIDGenericData.extend({
/**
* Deleted state
*/
deleted: zod.boolean().readonly().optional(),
/**
* Unique ObjectID of the object
*/
oid: ZodObjectId.readonly(),
});
export type OIDGenericDataSoftDelete = zod.infer<typeof ZodOIDGenericDataSoftDelete>;
export function isOIDGenericDataSoftDelete(data: any): data is OIDGenericDataSoftDelete {
try {
ZodOIDGenericDataSoftDelete.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodOIDGenericDataSoftDelete' error=${e}`);
return false;
}
}
export const ZodOIDGenericDataSoftDeleteWrite = ZodOIDGenericDataWrite;
export type OIDGenericDataSoftDeleteWrite = zod.infer<typeof ZodOIDGenericDataSoftDeleteWrite>;
export function isOIDGenericDataSoftDeleteWrite(data: any): data is OIDGenericDataSoftDeleteWrite {
try {
ZodOIDGenericDataSoftDeleteWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodOIDGenericDataSoftDeleteWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,40 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodGenericTiming, ZodGenericTimingWrite } from "./generic-timing";
export const ZodOIDGenericData = ZodGenericTiming.extend({
/**
* Unique ObjectID of the object
*/
oid: ZodObjectId.readonly(),
});
export type OIDGenericData = zod.infer<typeof ZodOIDGenericData>;
export function isOIDGenericData(data: any): data is OIDGenericData {
try {
ZodOIDGenericData.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodOIDGenericData' error=${e}`);
return false;
}
}
export const ZodOIDGenericDataWrite = ZodGenericTimingWrite;
export type OIDGenericDataWrite = zod.infer<typeof ZodOIDGenericDataWrite>;
export function isOIDGenericDataWrite(data: any): data is OIDGenericDataWrite {
try {
ZodOIDGenericDataWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodOIDGenericDataWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,24 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export enum PartRight {
READ = 1,
NONE = 0,
WRITE = 2,
READ_WRITE = 3,
};
export const ZodPartRight = zod.nativeEnum(PartRight);
export function isPartRight(data: any): data is PartRight {
try {
ZodPartRight.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodPartRight' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,29 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodInteger} from "./integer";
export const ZodRestErrorResponse = zod.object({
oid: ZodObjectId.optional(),
name: zod.string(),
message: zod.string(),
time: zod.string(),
status: ZodInteger,
statusMessage: zod.string(),
});
export type RestErrorResponse = zod.infer<typeof ZodRestErrorResponse>;
export function isRestErrorResponse(data: any): data is RestErrorResponse {
try {
ZodRestErrorResponse.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodRestErrorResponse' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,71 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
import {ZodObjectId} from "./object-id";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodSeason = ZodGenericDataSoftDelete.extend({
/**
* Name of the media (this represent the title)
*/
name: zod.string(),
/**
* Description of the media
*/
description: zod.string().optional(),
/**
* series parent ID
*/
parentId: ZodLong,
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).optional(),
});
export type Season = zod.infer<typeof ZodSeason>;
export function isSeason(data: any): data is Season {
try {
ZodSeason.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodSeason' error=${e}`);
return false;
}
}
export const ZodSeasonWrite = ZodGenericDataSoftDeleteWrite.extend({
/**
* Name of the media (this represent the title)
*/
name: zod.string().optional(),
/**
* Description of the media
*/
description: zod.string().nullable().optional(),
/**
* series parent ID
*/
parentId: ZodLong.optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
});
export type SeasonWrite = zod.infer<typeof ZodSeasonWrite>;
export function isSeasonWrite(data: any): data is SeasonWrite {
try {
ZodSeasonWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodSeasonWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,71 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
import {ZodObjectId} from "./object-id";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodSeries = ZodGenericDataSoftDelete.extend({
/**
* Name of the media (this represent the title)
*/
name: zod.string(),
/**
* Description of the media
*/
description: zod.string().optional(),
/**
* series parent ID
*/
parentId: ZodLong,
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).optional(),
});
export type Series = zod.infer<typeof ZodSeries>;
export function isSeries(data: any): data is Series {
try {
ZodSeries.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodSeries' error=${e}`);
return false;
}
}
export const ZodSeriesWrite = ZodGenericDataSoftDeleteWrite.extend({
/**
* Name of the media (this represent the title)
*/
name: zod.string().optional(),
/**
* Description of the media
*/
description: zod.string().nullable().optional(),
/**
* series parent ID
*/
parentId: ZodLong.optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
});
export type SeriesWrite = zod.infer<typeof ZodSeriesWrite>;
export function isSeriesWrite(data: any): data is SeriesWrite {
try {
ZodSeriesWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodSeriesWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodTimestamp = zod.string().datetime({ precision: 3 });
export type Timestamp = zod.infer<typeof ZodTimestamp>;

View File

@ -0,0 +1,62 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodObjectId} from "./object-id";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodType = ZodGenericDataSoftDelete.extend({
/**
* Name of the media (this represent the title)
*/
name: zod.string(),
/**
* Description of the media
*/
description: zod.string().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).optional(),
});
export type Type = zod.infer<typeof ZodType>;
export function isType(data: any): data is Type {
try {
ZodType.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodType' error=${e}`);
return false;
}
}
export const ZodTypeWrite = ZodGenericDataSoftDeleteWrite.extend({
/**
* Name of the media (this represent the title)
*/
name: zod.string().optional(),
/**
* Description of the media
*/
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodObjectId).nullable().optional(),
});
export type TypeWrite = zod.infer<typeof ZodTypeWrite>;
export function isTypeWrite(data: any): data is TypeWrite {
try {
ZodTypeWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodTypeWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,33 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodUser, ZodUserWrite } from "./user";
export const ZodUserKarideo = ZodUser;
export type UserKarideo = zod.infer<typeof ZodUserKarideo>;
export function isUserKarideo(data: any): data is UserKarideo {
try {
ZodUserKarideo.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserKarideo' error=${e}`);
return false;
}
}
export const ZodUserKarideoWrite = ZodUserWrite;
export type UserKarideoWrite = zod.infer<typeof ZodUserKarideoWrite>;
export function isUserKarideoWrite(data: any): data is UserKarideoWrite {
try {
ZodUserKarideoWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserKarideoWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,80 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
import {ZodFloat} from "./float";
import {ZodInteger} from "./integer";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodUserMediaAdvancement = ZodGenericDataSoftDelete.extend({
/**
* Foreign Key Id of the user
*/
userId: ZodLong.optional(),
/**
* Id of the media
*/
mediaId: ZodLong,
/**
* Percent of advancement in the media
*/
percent: ZodFloat,
/**
* Number of second of advancement in the media
*/
time: ZodInteger,
/**
* Number of time this media has been read
*/
count: ZodInteger,
});
export type UserMediaAdvancement = zod.infer<typeof ZodUserMediaAdvancement>;
export function isUserMediaAdvancement(data: any): data is UserMediaAdvancement {
try {
ZodUserMediaAdvancement.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserMediaAdvancement' error=${e}`);
return false;
}
}
export const ZodUserMediaAdvancementWrite = ZodGenericDataSoftDeleteWrite.extend({
/**
* Foreign Key Id of the user
*/
userId: ZodLong.nullable().optional(),
/**
* Id of the media
*/
mediaId: ZodLong.optional(),
/**
* Percent of advancement in the media
*/
percent: ZodFloat.optional(),
/**
* Number of second of advancement in the media
*/
time: ZodInteger.optional(),
/**
* Number of time this media has been read
*/
count: ZodInteger.optional(),
});
export type UserMediaAdvancementWrite = zod.infer<typeof ZodUserMediaAdvancementWrite>;
export function isUserMediaAdvancementWrite(data: any): data is UserMediaAdvancementWrite {
try {
ZodUserMediaAdvancementWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserMediaAdvancementWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,41 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodLong} from "./long";
export const ZodUserOut = zod.object({
id: ZodLong,
login: zod.string().optional(),
});
export type UserOut = zod.infer<typeof ZodUserOut>;
export function isUserOut(data: any): data is UserOut {
try {
ZodUserOut.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserOut' error=${e}`);
return false;
}
}
export const ZodUserOutWrite = zod.object({
id: ZodLong,
login: zod.string().nullable().optional(),
});
export type UserOutWrite = zod.infer<typeof ZodUserOutWrite>;
export function isUserOutWrite(data: any): data is UserOutWrite {
try {
ZodUserOutWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserOutWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,55 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import {ZodTimestamp} from "./timestamp";
import {ZodUUID} from "./uuid";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
export const ZodUser = ZodGenericDataSoftDelete.extend({
login: zod.string().min(3).max(128),
lastConnection: ZodTimestamp.optional(),
blocked: zod.boolean().optional(),
blockedReason: zod.string().max(512).optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).optional(),
});
export type User = zod.infer<typeof ZodUser>;
export function isUser(data: any): data is User {
try {
ZodUser.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUser' error=${e}`);
return false;
}
}
export const ZodUserWrite = ZodGenericDataSoftDeleteWrite.extend({
login: zod.string().min(3).max(128).optional(),
lastConnection: ZodTimestamp.nullable().optional(),
blocked: zod.boolean().nullable().optional(),
blockedReason: zod.string().max(512).nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
});
export type UserWrite = zod.infer<typeof ZodUserWrite>;
export function isUserWrite(data: any): data is UserWrite {
try {
ZodUserWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodUserWrite' error=${e}`);
return false;
}
}

View File

@ -0,0 +1,8 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
export const ZodUUID = zod.string().uuid();
export type UUID = zod.infer<typeof ZodUUID>;

View File

@ -0,0 +1,454 @@
/** @file
* @author Edouard DUPIN
* @copyright 2024, Edouard DUPIN, all right reserved
* @license MPL-2
*/
import { RestErrorResponse, isRestErrorResponse } from "./model";
export enum HTTPRequestModel {
ARCHIVE = "ARCHIVE",
DELETE = "DELETE",
HEAD = "HEAD",
GET = "GET",
OPTION = "OPTION",
PATCH = "PATCH",
POST = "POST",
PUT = "PUT",
RESTORE = "RESTORE",
}
export enum HTTPMimeType {
ALL = "*/*",
CSV = "text/csv",
IMAGE = "image/*",
IMAGE_JPEG = "image/jpeg",
IMAGE_PNG = "image/png",
JSON = "application/json",
MULTIPART = "multipart/form-data",
OCTET_STREAM = "application/octet-stream",
TEXT_PLAIN = "text/plain",
}
export interface RESTConfig {
// base of the server: http(s)://my.server.org/plop/api/
server: string;
// Token to access of the data.
token?: string;
}
export interface RESTModel {
// base of the local API request: "sheep/{id}".
endPoint: string;
// Type of the request.
requestType?: HTTPRequestModel;
// Input type requested.
accept?: HTTPMimeType;
// Content of the local data.
contentType?: HTTPMimeType;
// Mode of the TOKEN in URL or Header (?token:${tokenInUrl})
tokenInUrl?: boolean;
}
export interface ModelResponseHttp {
status: number;
data: any;
}
function isNullOrUndefined(data: any): data is undefined | null {
return data === undefined || data === null;
}
// generic progression callback
export type ProgressCallback = (count: number, total: number) => void;
export interface RESTAbort {
abort?: () => boolean;
}
// Rest generic callback have a basic model to upload and download advancement.
export interface RESTCallbacks {
progressUpload?: ProgressCallback;
progressDownload?: ProgressCallback;
abortHandle?: RESTAbort;
}
export interface RESTRequestType {
restModel: RESTModel;
restConfig: RESTConfig;
data?: any;
params?: object;
queries?: object;
callbacks?: RESTCallbacks;
}
function replaceAll(input, searchValue, replaceValue) {
return input.split(searchValue).join(replaceValue);
}
function removeTrailingSlashes(input: string): string {
if (isNullOrUndefined(input)) {
return "undefined";
}
return input.replace(/\/+$/, "");
}
function removeLeadingSlashes(input: string): string {
if (isNullOrUndefined(input)) {
return "";
}
return input.replace(/^\/+/, "");
}
export function RESTUrl({
restModel,
restConfig,
params,
queries,
}: RESTRequestType): string {
// Create the URL PATH:
let generateUrl = `${removeTrailingSlashes(
restConfig.server
)}/${removeLeadingSlashes(restModel.endPoint)}`;
if (params !== undefined) {
for (let key of Object.keys(params)) {
generateUrl = replaceAll(generateUrl, `{${key}}`, `${params[key]}`);
}
}
if (
queries === undefined &&
(restConfig.token === undefined || restModel.tokenInUrl !== true)
) {
return generateUrl;
}
const searchParams = new URLSearchParams();
if (queries !== undefined) {
for (let key of Object.keys(queries)) {
const value = queries[key];
if (Array.isArray(value)) {
for (const element of value) {
searchParams.append(`${key}`, `${element}`);
}
} else {
searchParams.append(`${key}`, `${value}`);
}
}
}
if (restConfig.token !== undefined && restModel.tokenInUrl === true) {
searchParams.append("Authorization", `Bearer ${restConfig.token}`);
}
return generateUrl + "?" + searchParams.toString();
}
export function fetchProgress(
generateUrl: string,
{
method,
headers,
body,
}: {
method: HTTPRequestModel;
headers: any;
body: any;
},
{ progressUpload, progressDownload, abortHandle }: RESTCallbacks
): Promise<Response> {
const xhr: {
io?: XMLHttpRequest;
} = {
io: new XMLHttpRequest(),
};
return new Promise((resolve, reject) => {
// Stream the upload progress
if (progressUpload) {
xhr.io?.upload.addEventListener("progress", (dataEvent) => {
if (dataEvent.lengthComputable) {
progressUpload(dataEvent.loaded, dataEvent.total);
}
});
}
// Stream the download progress
if (progressDownload) {
xhr.io?.addEventListener("progress", (dataEvent) => {
if (dataEvent.lengthComputable) {
progressDownload(dataEvent.loaded, dataEvent.total);
}
});
}
if (abortHandle) {
abortHandle.abort = () => {
if (xhr.io) {
console.log(`Request abort on the XMLHttpRequest: ${generateUrl}`);
xhr.io.abort();
return true;
}
console.log(
`Request abort (FAIL) on the XMLHttpRequest: ${generateUrl}`
);
return false;
};
}
// Check if we have an internal Fail:
xhr.io?.addEventListener("error", () => {
xhr.io = undefined;
reject(new TypeError("Failed to fetch"));
});
// Capture the end of the stream
xhr.io?.addEventListener("loadend", () => {
if (xhr.io?.readyState !== XMLHttpRequest.DONE) {
return;
}
if (xhr.io?.status === 0) {
//the stream has been aborted
reject(new TypeError("Fetch has been aborted"));
return;
}
// Stream is ended, transform in a generic response:
const response = new Response(xhr.io.response, {
status: xhr.io.status,
statusText: xhr.io.statusText,
});
const headersArray = replaceAll(
xhr.io.getAllResponseHeaders().trim(),
"\r\n",
"\n"
).split("\n");
headersArray.forEach(function (header) {
const firstColonIndex = header.indexOf(":");
if (firstColonIndex !== -1) {
const key = header.substring(0, firstColonIndex).trim();
const value = header.substring(firstColonIndex + 1).trim();
response.headers.set(key, value);
} else {
response.headers.set(header, "");
}
});
xhr.io = undefined;
resolve(response);
});
xhr.io?.open(method, generateUrl, true);
if (!isNullOrUndefined(headers)) {
for (const [key, value] of Object.entries(headers)) {
xhr.io?.setRequestHeader(key, value as string);
}
}
xhr.io?.send(body);
});
}
export function RESTRequest({
restModel,
restConfig,
data,
params,
queries,
callbacks,
}: RESTRequestType): Promise<ModelResponseHttp> {
// Create the URL PATH:
let generateUrl = RESTUrl({ restModel, restConfig, data, params, queries });
let headers: any = {};
if (restConfig.token !== undefined && restModel.tokenInUrl !== true) {
headers["Authorization"] = `Bearer ${restConfig.token}`;
}
if (restModel.accept !== undefined) {
headers["Accept"] = restModel.accept;
}
if (restModel.requestType !== HTTPRequestModel.GET &&
restModel.requestType !== HTTPRequestModel.ARCHIVE &&
restModel.requestType !== HTTPRequestModel.RESTORE
) {
// if Get we have not a content type, the body is empty
if (restModel.contentType !== HTTPMimeType.MULTIPART &&
restModel.contentType !== undefined
) {
// special case of multi-part ==> no content type otherwise the browser does not set the ";bundary=--****"
headers["Content-Type"] = restModel.contentType;
}
}
let body = data;
if (restModel.contentType === HTTPMimeType.JSON) {
body = JSON.stringify(data);
} else if (restModel.contentType === HTTPMimeType.MULTIPART) {
const formData = new FormData();
for (const name in data) {
formData.append(name, data[name]);
}
body = formData;
}
return new Promise((resolve, reject) => {
let action: undefined | Promise<Response> = undefined;
if (
isNullOrUndefined(callbacks) ||
(isNullOrUndefined(callbacks.progressDownload) &&
isNullOrUndefined(callbacks.progressUpload) &&
isNullOrUndefined(callbacks.abortHandle))
) {
// No information needed: call the generic fetch interface
action = fetch(generateUrl, {
method: restModel.requestType,
headers,
body,
});
} else {
// need progression information: call old fetch model (XMLHttpRequest) that permit to keep % upload and % download for HTTP1.x
action = fetchProgress(
generateUrl,
{
method: restModel.requestType ?? HTTPRequestModel.GET,
headers,
body,
},
callbacks
);
}
action
.then((response: Response) => {
if (response.status >= 200 && response.status <= 299) {
const contentType = response.headers.get("Content-Type");
if (
!isNullOrUndefined(restModel.accept) &&
restModel.accept !== contentType
) {
reject({
name: "Model accept type incompatible",
time: Date().toString(),
status: 901,
message: `REST Content type are not compatible: ${restModel.accept} != ${contentType}`,
statusMessage: "Fetch error",
error: "rest-tools.ts Wrong type in the message return type",
} as RestErrorResponse);
} else if (contentType === HTTPMimeType.JSON) {
response
.json()
.then((value: any) => {
resolve({ status: response.status, data: value });
})
.catch((reason: Error) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: 902,
message: `REST parse json fail: ${reason}`,
statusMessage: "Fetch parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
});
} else {
resolve({ status: response.status, data: response.body });
}
} else {
// the answer is not correct not a 2XX
// clone the response to keep the raw data if case of error:
response
.clone()
.json()
.then((value: any) => {
if (isRestErrorResponse(value)) {
reject(value);
} else {
response
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: 903,
message: `REST parse error json with wrong type fail. ${dataError}`,
statusMessage: "Fetch parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
});
}
})
.catch((reason: Error) => {
response
.text()
.then((dataError: string) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ${dataError} with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT parse error",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
})
.catch((reason: any) => {
reject({
name: "API serialization error",
time: Date().toString(),
status: response.status,
message: `unmanaged error model: ??? with error: ${reason}`,
statusMessage: "Fetch ERROR TEXT FAIL",
error: "rest-tools.ts Wrong message model to parse",
} as RestErrorResponse);
});
});
}
})
.catch((error: Error) => {
if (isRestErrorResponse(error)) {
reject(error);
} else {
reject({
name: "Request fail",
time: Date(),
status: 999,
message: error,
statusMessage: "Fetch catch error",
error: "rest-tools.ts detect an error in the fetch request",
});
}
});
});
}
export function RESTRequestJson<TYPE>(
request: RESTRequestType,
checker?: (data: any) => data is TYPE
): Promise<TYPE> {
return new Promise((resolve, reject) => {
RESTRequest(request)
.then((value: ModelResponseHttp) => {
if (isNullOrUndefined(checker)) {
console.log(`Have no check of MODEL in API: ${RESTUrl(request)}`);
resolve(value.data);
} else if (checker === undefined || checker(value.data)) {
resolve(value.data);
} else {
reject({
name: "Model check fail",
time: Date().toString(),
status: 950,
error: "REST Fail to verify the data",
statusMessage: "API cast ERROR",
message: "api.ts Check type as fail",
} as RestErrorResponse);
}
})
.catch((reason: RestErrorResponse) => {
reject(reason);
});
});
}
export function RESTRequestVoid(request: RESTRequestType): Promise<void> {
return new Promise((resolve, reject) => {
RESTRequest(request)
.then((value: ModelResponseHttp) => {
resolve();
})
.catch((reason: RestErrorResponse) => {
reject(reason);
});
});
}

View File

@ -0,0 +1,374 @@
import { useEffect, useRef, useState } from 'react';
import { Box, Flex, IconButton, SliderTrack, Text } from '@chakra-ui/react';
import {
MdFastForward,
MdFastRewind,
MdLooksOne,
MdNavigateBefore,
MdNavigateNext,
MdPause,
MdPlayArrow,
MdRepeat,
MdRepeatOne,
MdStop,
MdTrendingFlat,
} from 'react-icons/md';
import { useColorModeValue } from '@/components/ui/color-mode';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificMedia } from '@/service/Media';
import { useSpecificSeason } from '@/service/Season';
import { useSpecificSeries } from '@/service/Series';
import { useSpecificType } from '@/service/Type';
import { DataUrlAccess } from '@/utils/data-url-access';
import { isNullOrUndefined } from '@/utils/validator';
import { Slider } from './ui/slider';
export enum PlayMode {
PLAY_ONE,
PLAY_ALL,
PLAY_ONE_LOOP,
PLAY_ALL_LOOP,
}
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%' }} />
),
};
export type AudioPlayerProps = {};
const formatTime = (time) => {
if (time && !isNaN(time)) {
const minutes = Math.floor(time / 60);
const formatMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
const seconds = Math.floor(time % 60);
const formatSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${formatMinutes}:${formatSeconds}`;
}
return '00:00';
};
export const AudioPlayer = ({}: AudioPlayerProps) => {
const { playMediaList, MediaOffset, previous, next, first } =
useActivePlaylistService();
const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [timeProgress, setTimeProgress] = useState<number>(0);
const [playingMode, setPlayingMode] = useState<PlayMode>(PlayMode.PLAY_ALL);
const [duration, setDuration] = useState<number>(0);
const { dataMedia } = useSpecificMedia(
MediaOffset !== undefined ? playMediaList[MediaOffset] : undefined
);
const { dataSeason } = useSpecificSeason(dataMedia?.seasonId);
const { dataType } = useSpecificType(dataMedia?.typeId);
const { dataSeries } = useSpecificSeries(dataMedia?.seriesId);
const [mediaSource, setMediaSource] = useState<string>('');
useEffect(() => {
setMediaSource(
dataMedia && dataMedia?.dataId
? DataUrlAccess.getUrl(dataMedia?.dataId)
: ''
);
}, [dataMedia, setMediaSource]);
const backColor = useColorModeValue('back.100', 'back.800');
const configButton = {
borderRadius: 'full',
backgroundColor: 'transparent',
_hover: {
bgColor: 'brand.500',
},
width: '50px',
height: '50px',
padding: '5px',
};
useEffect(() => {
if (!audioRef || !audioRef.current) {
return;
}
if (isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
}
}, [isPlaying, audioRef]);
const onAudioEnded = () => {
if (playMediaList.length === 0 || isNullOrUndefined(MediaOffset)) {
return;
}
if (playingMode === PlayMode.PLAY_ALL_LOOP) {
if (playMediaList.length == MediaOffset + 1) {
first();
} else {
next();
}
} else if (playingMode === PlayMode.PLAY_ALL) {
next();
} else if (playingMode === PlayMode.PLAY_ONE_LOOP) {
onSeek(0);
onPlay();
}
};
const onSeek = (newValue) => {
console.log(`onSeek: ${newValue}`);
if (!audioRef || !audioRef.current) {
return;
}
audioRef.current.currentTime = newValue;
};
const onPlay = () => {
if (!audioRef || !audioRef.current) {
return;
}
if (isPlaying) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
};
const onStop = () => {
if (!audioRef || !audioRef.current) {
return;
}
if (audioRef.current.currentTime == 0 && audioRef.current.paused) {
// TODO remove current playing value
} else {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
};
const onNavigatePrevious = () => {
previous();
};
const onFastRewind = () => {
if (!audioRef || !audioRef.current) {
return;
}
audioRef.current.currentTime -= 10;
};
const onFastForward = () => {
if (!audioRef || !audioRef.current) {
return;
}
audioRef.current.currentTime += 10;
};
const onNavigateNext = () => {
next();
};
const onTypePlay = () => {
setPlayingMode((value: PlayMode) => {
if (value === PlayMode.PLAY_ONE) {
return PlayMode.PLAY_ALL;
} else if (value === PlayMode.PLAY_ALL) {
return PlayMode.PLAY_ONE_LOOP;
} else if (value === PlayMode.PLAY_ONE_LOOP) {
return PlayMode.PLAY_ALL_LOOP;
} else {
return PlayMode.PLAY_ONE;
}
});
};
/**
* Call when meta-data is updated
*/
function onChangeMetadata(): void {
const seconds = audioRef.current?.duration;
if (seconds !== undefined) {
setDuration(seconds);
}
}
const onTimeUpdate = () => {
if (!audioRef || !audioRef.current) {
return;
}
console.log(`onTimeUpdate ${audioRef.current.currentTime}`);
setTimeProgress(audioRef.current.currentTime);
};
const onDurationChange = (event) => {};
const onChangeStateToPlay = () => {
setIsPlaying(true);
};
const onChangeStateToPause = () => {
setIsPlaying(false);
};
const marks = () => {
const minutes = Math.floor(duration / 60);
const result: number[] = [];
for (let i = 1; i <= minutes; i++) {
result.push(60 * i);
}
return result;
};
return (
<>
{!isNullOrUndefined(MediaOffset) && (
<Flex
position="absolute"
height="150px"
minHeight="150px"
paddingY="5px"
paddingX="10px"
marginX="15px"
bottom={0}
left={0}
right={0}
zIndex={1000}
borderWidth="1px"
borderColor="brand.900"
bgColor={backColor}
borderTopRadius="10px"
direction="column"
>
<Text
alignContent="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
// noOfLines={1}
>
{dataMedia?.name ?? '???'}
</Text>
<Text
alignContent="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
overflow="hidden"
// noOfLines={1}
>
{dataSeries && dataSeries.name}
{dataSeason && dataSeason?.name}
{dataType && ` / ${dataType.name}`}
</Text>
<Box width="full" paddingX="15px">
<Slider
defaultValue={[0]}
value={[timeProgress]}
min={0}
max={duration}
step={0.1}
onValueChange={(e) => onSeek(e.value)}
variant="outline"
colorPalette="brand"
marks={marks()}
//focusCapture={false}
>
<SliderTrack
bg="brand.200"
height="10px"
borderRadius="full"
></SliderTrack>
</Slider>
</Box>
<Flex>
<Text
alignContent="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
overflow="hidden"
// noOfLines={1}
>
{formatTime(timeProgress)}
</Text>
<Text alignContent="left" fontSize="16px" userSelect="none">
{formatTime(duration)}
</Text>
</Flex>
<Flex gap="5px">
<IconButton
{...configButton}
aria-label={'Play'}
onClick={onPlay}
variant="ghost"
>
{isPlaying ? (
<MdPause style={{ width: '100%', height: '100%' }} />
) : (
<MdPlayArrow style={{ width: '100%', height: '100%' }} />
)}
</IconButton>
<IconButton
{...configButton}
aria-label={'Stop'}
onClick={onStop}
variant="ghost"
>
<MdStop style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton
{...configButton}
aria-label={'Previous Media'}
onClick={onNavigatePrevious}
marginLeft="auto"
variant="ghost"
>
<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>
<IconButton
{...configButton}
aria-label={'jump 15sec in future'}
onClick={onFastForward}
variant="ghost"
>
<MdFastForward style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton
{...configButton}
aria-label={'Next Media'}
marginRight="auto"
onClick={onNavigateNext}
variant="ghost"
>
<MdNavigateNext style={{ width: '100%', height: '100%' }} />
</IconButton>
<IconButton
{...configButton}
aria-label={'continue to the end'}
onClick={onTypePlay}
variant="ghost"
>
{playModeIcon[playingMode]}
</IconButton>
</Flex>
</Flex>
)}
<audio
src={mediaSource}
ref={audioRef}
//preload={true}
onPlay={onChangeStateToPlay}
onPause={onChangeStateToPause}
onTimeUpdate={onTimeUpdate}
onDurationChange={onDurationChange}
onLoadedMetadata={onChangeMetadata}
autoPlay={true}
onEnded={onAudioEnded}
/>
</>
);
};

View File

@ -0,0 +1,112 @@
import { ReactElement, useEffect, useState } from 'react';
import { Box, BoxProps, Flex, FlexProps } from '@chakra-ui/react';
import { Image } from '@chakra-ui/react';
import { ObjectId } from '@/back-api';
import { DataUrlAccess } from '@/utils/data-url-access';
import { Icon } from './Icon';
export type CoversProps = Omit<BoxProps, 'iconEmpty'> & {
data?: ObjectId[];
size?: BoxProps['width'];
iconEmpty?: ReactElement;
slideshow?: boolean;
};
export const Covers = ({
data,
iconEmpty,
size = '100px',
slideshow = false,
...rest
}: CoversProps) => {
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [previousImageIndex, setPreviousImageIndex] = useState(0);
const [topOpacity, setTopOpacity] = useState(0.0);
useEffect(() => {
if (!slideshow) {
return;
}
const interval = setInterval(() => {
setPreviousImageIndex(currentImageIndex);
setTopOpacity(0.0);
setTimeout(() => {
setCurrentImageIndex(
(prevIndex) => (prevIndex + 1) % (data?.length ?? 1)
);
setTopOpacity(1.0);
}, 1500);
}, 3000);
return () => clearInterval(interval);
}, [slideshow, data]);
if (!data || data.length < 1) {
if (iconEmpty) {
return <Icon children={iconEmpty} sizeIcon={size} />;
} else {
return (
<Box
width={size}
height={size}
minHeight={size}
minWidth={size}
borderColor="blue"
borderWidth="1px"
margin="auto"
{...rest}
></Box>
);
}
}
if (slideshow === false || data.length === 1) {
const url = DataUrlAccess.getThumbnailUrl(data[0]);
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>
);
};

View File

@ -0,0 +1,13 @@
import { Box } from '@chakra-ui/react';
export const EmptyEnd = () => {
return (
<Box
width="full"
height="25%"
minHeight="250px"
// borderWidth="1px"
// borderColor="red"
></Box>
);
};

View File

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

View File

@ -0,0 +1,40 @@
import { ReactNode, forwardRef } from 'react';
import { Box, Flex, FlexProps } from '@chakra-ui/react';
export type IconProps = FlexProps & {
children: ReactNode;
color?: string;
sizeIcon?: FlexProps['width'];
};
export const Icon = forwardRef<HTMLDivElement, IconProps>(
({ 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';

View File

@ -0,0 +1,52 @@
import React, { ReactNode, useEffect } from 'react';
import { Flex, Image } from '@chakra-ui/react';
import { useLocation } from 'react-router-dom';
import ikon from '@/assets/images/ikon.svg';
import { TOP_BAR_HEIGHT } from '@/components/TopBar/TopBar';
export type LayoutProps = React.PropsWithChildren<unknown> & {
topBar?: ReactNode;
};
export const PageLayout = ({ children }: LayoutProps) => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return (
<>
<Flex
minH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
maxH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
position="absolute"
top={TOP_BAR_HEIGHT}
bottom={0}
left={0}
right={0}
minWidth="300px"
zIndex={0}
>
<Image src={ikon} boxSize="90vh" margin="auto" />
</Flex>
<Flex
direction="column"
overflowX="auto"
overflowY="auto"
minH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
maxH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
position="absolute"
top={TOP_BAR_HEIGHT}
bottom={0}
left={0}
right={0}
minWidth="300px"
>
{children}
</Flex>
</>
);
};

View File

@ -0,0 +1,43 @@
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 { useColorModeValue } from '@/components/ui/color-mode';
import { colors } from '@/theme/colors';
export type LayoutProps = FlexProps & {
children: ReactNode;
};
export const PageLayoutInfoCenter = ({
children,
width = '25%',
...rest
}: LayoutProps) => {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return (
<PageLayout>
<Flex
direction="column"
margin="auto"
minWidth={width}
border="back.900"
borderWidth="1px"
borderRadius="8px"
padding="10px"
boxShadow={'0px 0px 16px ' + colors.back[900]}
backgroundColor={useColorModeValue('#FFFFFF', '#000000')}
{...rest}
>
{children}
</Flex>
</PageLayout>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,53 @@
import { useState } from 'react';
import { Group, Input } from '@chakra-ui/react';
import { MdSearch } from 'react-icons/md';
export type SearchInputProps = {
onChange?: (data?: string) => void;
onSubmit?: (data?: string) => void;
};
export const SearchInput = ({
onChange: onChangeValue,
onSubmit: onSubmitValue,
}: SearchInputProps) => {
const [inputData, setInputData] = useState<string | undefined>(undefined);
const [searchInputProperty, setSearchInputProperty] =
useState<any>(undefined);
function onFocusKeep(): void {
setSearchInputProperty({
width: '70%',
maxWidth: '70%',
});
}
function onFocusLost(): void {
setSearchInputProperty({
width: '250px',
});
}
function onChange(event): void {
const data =
event.target.value.length === 0 ? undefined : event.target.value;
setInputData(data);
if (onChangeValue) {
onChangeValue(data);
}
}
function onSubmit(): void {
if (onSubmitValue) {
onSubmitValue(inputData);
}
}
return (
<Group maxWidth="200px" marginLeft="auto" {...searchInputProperty}>
<MdSearch color="gray.300" />
<Input
onFocus={onFocusKeep}
onBlur={() => setTimeout(() => onFocusLost(), 200)}
onChange={onChange}
onSubmit={onSubmit}
/>
</Group>
);
};

View File

@ -0,0 +1,277 @@
import { ReactNode } from 'react';
import { Box, Button, ConditionalValue, Flex, HStack, IconButton, Span, Text, useDisclosure } from '@chakra-ui/react';
import { LuAlignJustify, LuArrowBigLeft, LuCircleUserRound, LuKeySquare, LuLogIn, LuLogOut, LuMoon, LuSettings, LuSun } from 'react-icons/lu';
import { MdHelp, MdHome, MdMore, MdOutlinePlaylistPlay, MdOutlineUploadFile, MdSupervisedUserCircle } from 'react-icons/md';
import { useNavigate } from 'react-router-dom';
import { useColorMode, useColorModeValue } from '@/components/ui/color-mode';
import { DrawerBody, DrawerContent, DrawerHeader, DrawerRoot } from '@/components/ui/drawer';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '@/components/ui/menu';
import { environment } from '@/environment';
import { useServiceContext } from '@/service/ServiceContext';
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
>,
//colorPalette: "brand",
fontSize: '20px',
textTransform: 'uppercase',
height: TOP_BAR_HEIGHT,
};
export type TopBarProps = {
children?: ReactNode;
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 navigate = useNavigate();
const { colorMode, toggleColorMode } = useColorMode();
const { session } = useServiceContext();
const { clearToken } = useSessionService();
const backColor = useColorModeValue('back.100', 'back.800');
const drawerDisclose = useDisclosure();
const onChangeTheme = () => {
drawerDisclose.onOpen();
};
const onSignIn = (): void => {
clearToken();
requestSignIn();
};
const onSignUp = (): void => {
clearToken();
requestSignUp();
};
const onSignOut = (): void => {
clearToken();
requestSignOut();
};
const onKarso = (): void => {
requestOpenSite();
};
return (
<Flex
position="absolute"
top={0}
left={0}
right={0}
height={TOP_BAR_HEIGHT}
alignItems="center"
justifyContent="space-between"
backgroundColor={backColor}
gap="2"
px="2"
boxShadow={'0px 2px 4px ' + colors.back[900]}
zIndex={200}
>
{session?.isConnected ?
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onChangeTheme}>
<HStack>
<LuAlignJustify />
<Text paddingLeft="3px" fontWeight="bold">
{environment.applName}
</Text>
</HStack>
</Button>
:(
<HStack {...BUTTON_TOP_BAR_PROPERTY} >
<LuAlignJustify />
<Span paddingLeft="3px" fontWeight="bold">{environment.applName}</Span>
</HStack>)
}
{title && (
<Text
fontSize="20px"
fontWeight="bold"
textTransform="uppercase"
marginRight="auto"
userSelect="none"
color="brand.500"
>
{title}
</Text>
)}
{children}
<Flex right="0">
{!session?.isConnected && (
<>
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onSignIn}>
<LuLogIn />
<Text paddingLeft="3px" fontWeight="bold">
Sign-in
</Text>
</Button>
<Button
{...BUTTON_TOP_BAR_PROPERTY}
onClick={onSignUp}
disabled={true}
>
<MdMore />
<Text paddingLeft="3px" fontWeight="bold">
Sign-up
</Text>
</Button>
</>
)}
{session?.isConnected && (
<MenuRoot>
<MenuTrigger asChild>
<IconButton {...BUTTON_TOP_BAR_PROPERTY} width={TOP_BAR_HEIGHT}>
<LuCircleUserRound />
</IconButton>
</MenuTrigger>
<MenuContent>
<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={() => 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}
>
<LuMoon /> Set dark mode
</MenuItem>
) : (
<MenuItem
value="set-light"
valueText="set-light"
onClick={toggleColorMode}
>
<LuSun /> Set light mode
</MenuItem>
)}
</MenuContent>
</MenuRoot>
)}
</Flex>
{session?.isConnected &&
<DrawerRoot
placement="start"
onOpenChange={drawerDisclose.onClose}
open={drawerDisclose.open}
data-testid="top-bar_drawer-root"
>
<DrawerContent data-testid="top-bar_drawer-content">
<DrawerHeader
paddingY="auto"
as="button"
onClick={drawerDisclose.onClose}
boxShadow={'0px 2px 4px ' + colors.back[900]}
backgroundColor={backColor}
color={useColorModeValue('brand.900', 'brand.50')}
textTransform="uppercase"
>
<HStack {...BUTTON_TOP_BAR_PROPERTY} cursor="pointer">
<LuArrowBigLeft />
<Span paddingLeft="3px">{environment.applName}</Span>
</HStack>
</DrawerHeader>
<DrawerBody paddingX="0px">
<Box marginY="3" />
<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>
}
</Flex>
);
};

View File

@ -0,0 +1,51 @@
import { ReactNode } from 'react';
import { Box } from '@chakra-ui/react';
import { LuMenu } from 'react-icons/lu';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '../ui/menu';
export type MenuElement = {
icon?: ReactNode;
name: string;
onClick: () => void;
};
export type ContextMenuProps = {
elements?: MenuElement[];
};
export const ContextMenu = ({ elements }: ContextMenuProps) => {
if (!elements) {
return <></>;
}
return (
<MenuRoot data-testid="context-menu">
<MenuTrigger
asChild
marginY="auto"
marginRight="4px"
data-testid="context-menu_trigger"
>
<Box asChild color="brand.500" cursor="pointer">
<LuMenu />
</Box>
</MenuTrigger>
<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"
>
{data.icon}
{data.name}
</MenuItem>
))}
</MenuContent>
</MenuRoot>
);
};

View File

@ -0,0 +1,162 @@
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 { FormGroup } from '@/components/form/FormGroup';
import { DataUrlAccess } from '@/utils/data-url-access';
import { useFormidableContextElement } from '../formidable';
export type DragNdropProps = {
onFilesSelected?: (file: File[]) => void;
onUriSelected?: (uri: string) => void;
width?: string;
height?: string;
};
export const DragNdrop = ({
onFilesSelected = () => {},
onUriSelected = () => {},
width = '100px',
height = '100px',
}: DragNdropProps) => {
const handleFileChange = (event) => {
const selectedFiles = event.target.files;
if (selectedFiles && selectedFiles.length > 0) {
const newFiles: File[] = Array.from(selectedFiles);
onFilesSelected(newFiles);
}
};
const handleDrop = (eventInput: any) => {
const event = eventInput as DragEvent;
event.preventDefault();
const droppedFiles = event.dataTransfer?.files;
console.log('drop ...' + droppedFiles?.length);
if (droppedFiles && droppedFiles?.length > 0) {
const newFiles: File[] = Array.from(droppedFiles);
onFilesSelected(newFiles);
} else {
console.log(`drop types: ${event.dataTransfer?.types}`);
const listUri = event.dataTransfer?.getData('text/uri-list');
console.log(`listUri: ${listUri}`);
if (!listUri) {
return;
}
onUriSelected(listUri);
}
};
return (
<Box
width={width}
height={height}
border="2px"
borderRadius="5px"
borderStyle="dashed"
onDrop={handleDrop}
onDragOver={(event) => event.preventDefault()}
>
<label htmlFor="browse">
<Box paddingY="15%" height="100%" cursor="pointer">
<Center>
<MdUploadFile size="50%" />
</Center>
<Center>
<input
type="file"
hidden
id="browse"
onChange={handleFileChange}
//accept=".pdf,.docx,.pptx,.txt,.xlsx"
multiple
/>
Browse files
</Center>
</Box>
</label>
</Box>
);
};
export type CenterIconProps = BoxProps & {
children: ReactNode;
sizeIcon?: string;
};
export const CenterIcon = ({
children,
sizeIcon = '15px',
...rest
}: CenterIconProps) => {
return (
<Box position="relative" w={sizeIcon} h={sizeIcon} flex="none" {...rest}>
<Box
w={sizeIcon}
h={sizeIcon}
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
>
{children}
</Box>
</Box>
);
};
export type FormCoversProps = {
name: string;
ref?: RefObject<any>;
label?: string;
isRequired?: boolean;
onFilesSelected?: (files: File[]) => void;
onUriSelected?: (uri: string) => void;
onRemove?: (index: number) => void;
};
/** This field component is a direct insertion component ==> not manage with formidable */
export const FormCovers = ({
name,
ref,
onFilesSelected = () => {},
onUriSelected = () => {},
onRemove = () => {},
...rest
}: FormCoversProps) => {
const { value } = useFormidableContextElement(name);
const urls = DataUrlAccess.getListThumbnailUrl(value) ?? [];
return (
<FormGroup name={name} {...rest}>
<HStack wrap="wrap" width="full">
{urls.map((data, index) => (
<Flex align="flex-start" key={data}>
<Box width="125px" height="125px" position="relative">
<Box width="125px" height="125px" position="absolute">
<CenterIcon
width="125px"
sizeIcon="100%"
zIndex="+1"
color="#00000020"
_hover={{ color: 'red' }}
onClick={() => onRemove && onRemove(index)}
>
<MdHighlightOff />
</CenterIcon>
</Box>
<Image loading="lazy" src={data} boxSize="full" />
</Box>
</Flex>
))}
<Flex align="flex-start" key="data">
<DragNdrop
height="125px"
width="125px"
onFilesSelected={onFilesSelected}
onUriSelected={onUriSelected}
/>
</Flex>
</HStack>
</FormGroup>
);
};

View File

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

View File

@ -0,0 +1,36 @@
import { RefObject } from 'react';
import { Input } from '@chakra-ui/react';
import { FormGroup, FormGroupProps } from '@/components/form/FormGroup';
import { useFormidableContextElement } from '../formidable';
export type FormInputProps = {
name: string;
ref?: RefObject<any>;
label?: string;
placeholder?: string;
isRequired?: boolean;
} & Omit<FormGroupProps, 'children'>;
export const FormInput = ({
name,
ref,
placeholder,
...rest
}: FormInputProps) => {
const { value, onChange } = useFormidableContextElement(name);
return (
<FormGroup name={name} {...rest}>
<Input
ref={ref}
type="text"
name={name}
autoComplete={name}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormGroup>
);
};

View File

@ -0,0 +1,50 @@
import { RefObject } from 'react';
import { FormGroup } from '@/components/form/FormGroup';
import { useFormidableContextElement } from '../formidable';
import {
NumberInputField,
NumberInputProps,
NumberInputRoot,
} from '../ui/number-input';
export type FormNumberProps = Pick<
NumberInputProps,
'step' | 'defaultValue' | 'min' | 'max'
> & {
name: string;
ref?: RefObject<any>;
label?: string;
placeholder?: string;
isRequired?: boolean;
};
export const FormNumber = ({
name,
ref,
placeholder,
step,
min,
max,
defaultValue,
...rest
}: FormNumberProps) => {
const { form, value, isModify, onChange, onRestore } =
useFormidableContextElement(name);
return (
<FormGroup name={name} {...rest}>
<NumberInputRoot
ref={ref}
value={value}
onValueChange={(e) => onChange(e.value)}
step={step}
defaultValue={defaultValue}
min={min}
max={max}
>
<NumberInputField />
</NumberInputRoot>
</FormGroup>
);
};

View File

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

View File

@ -0,0 +1,126 @@
import { useState } from 'react';
import { Box } from '@chakra-ui/react';
import { FormSelect } from '@/components/form/FormSelect';
import { useFormidable } from '@/components/formidable/FormidableConfig';
import { Formidable } from '../formidable';
export default {
title: 'Components/FormSelect',
};
type BasicFormData = {
data?: number;
};
export const Default = () => {
const form = useFormidable<BasicFormData>({});
return (
<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 (
<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 (
<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 = () => {
const form = useFormidable<BasicFormData>({});
const [data, setData] = useState([
{ id: 111, name: 'first Item' },
{ id: 222, name: 'Second Item' },
{ id: 333, name: 'third item' },
]);
return (
<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 }];
});
resolve({ id: upperId, name: data });
});
}}
options={data}
/>
</Formidable.From>
);
};
export const DarkBackground = {
render: () => {
const form = useFormidable<BasicFormData>({});
return (
<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>
);
},
parameters: {
docs: {
description: {
story: 'some story **markdown**',
},
},
},
};

View File

@ -0,0 +1,66 @@
import { RefObject } from 'react';
import { FormGroup } from '@/components/form/FormGroup';
import { SelectSingle } from '@/components/select/SelectSingle';
import { useFormidableContextElement } from '../formidable';
export type FormSelectProps = {
// Form: Name of the variable
name: string;
// Forward object reference
ref?: RefObject<any>;
// Form: Label of the input
label?: string;
// Form: Placeholder if nothing is selected
placeholder?: string;
// Form: Specify if the element is required or not
isRequired?: boolean;
// List of object options
options: object[];
// in the option specify the value Key
keyInputKey?: string;
// in the option specify the value field
keyInputValue?: string;
// Add capability to add an item (no key but only value)
addNewItem?: (data: string) => Promise<any>;
// if a suggestion exist at the auto compleat
suggestion?: string;
};
export const FormSelect = ({
name,
ref,
placeholder,
options,
keyInputKey = 'id',
keyInputValue = 'name',
suggestion,
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({ [name]: data[keyInputKey] })
);
};
return (
<FormGroup name={name} {...rest}>
<SelectSingle
ref={ref}
value={value}
options={options}
onChange={(value) => onChange(value)}
keyKey={keyInputKey}
keyValue={keyInputValue}
onCreate={onCreate}
suggestion={suggestion}
/>
</FormGroup>
);
};

View File

@ -0,0 +1,126 @@
import { useState } from 'react';
import { Box } from '@chakra-ui/react';
import { FormSelectMultiple } from '@/components/form/FormSelectMultiple';
import { useFormidable } from '@/components/formidable/FormidableConfig';
import { Formidable } from '../formidable';
export default {
title: 'Components/FormSelectMultipleMultiple',
};
type BasicFormData = {
data?: number[];
};
export const Default = () => {
const form = useFormidable<BasicFormData>({});
return (
<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 (
<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 (
<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 = () => {
const form = useFormidable<BasicFormData>({});
const [data, setData] = useState([
{ id: 111, name: 'first Item' },
{ id: 222, name: 'Second Item' },
{ id: 333, name: 'third item' },
]);
return (
<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 }];
});
resolve({ id: upperId, name: data });
});
}}
options={data}
/>
</Formidable.From>
);
};
export const DarkBackground = {
render: () => {
const form = useFormidable<BasicFormData>({});
return (
<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>
);
},
parameters: {
docs: {
description: {
story: 'some story **markdown**',
},
},
},
};

View File

@ -0,0 +1,67 @@
import { RefObject } from 'react';
import { FormGroup } from '@/components/form/FormGroup';
import { SelectMultiple } from '@/components/select/SelectMultiple';
import {
useFormidableContext,
useFormidableContextElement,
} from '../formidable';
export type FormSelectMultipleProps = {
// Form: Name of the variable
name: string;
// Forward object reference
ref?: RefObject<any>;
// Form: Label of the input
label?: string;
// Form: Placeholder if nothing is selected
placeholder?: string;
// Form: Specify if the element is required or not
isRequired?: boolean;
// List of object options
options: object[];
// in the option specify the value Key
keyInputKey?: string;
// in the option specify the value field
keyInputValue?: string;
// Add capability to add an item (no key but only value)
addNewItem?: (data: string) => Promise<any>;
};
export const FormSelectMultiple = ({
name,
ref,
placeholder,
options,
keyInputKey = 'id',
keyInputValue = 'name',
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({
[name]: [...(form.values[name] ?? []), data[keyInputKey]],
})
);
};
return (
<FormGroup name={name} {...rest}>
<SelectMultiple
//ref={ref}
values={value}
options={options}
onChange={(value) => onChange(value)}
keyKey={keyInputKey}
keyValue={keyInputValue}
onCreate={onCreate}
/>
</FormGroup>
);
};

View File

@ -0,0 +1,35 @@
import { RefObject } from 'react';
import { Textarea } from '@chakra-ui/react';
import { FormGroup } from '@/components/form/FormGroup';
import { useFormidableContextElement } from '../formidable';
export type FormTextareaProps = {
name: string;
ref?: RefObject<any>;
label?: string;
placeholder?: string;
isRequired?: boolean;
};
export const FormTextarea = ({
name,
ref,
placeholder,
...rest
}: FormTextareaProps) => {
const { value, onChange } = useFormidableContextElement(name);
return (
<FormGroup name={name} {...rest}>
<Textarea
name={name}
ref={ref}
autoComplete={name}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
</FormGroup>
);
};

View File

@ -0,0 +1,149 @@
import { useCallback, useEffect, useState } from 'react';
import { isNullOrUndefined } from '@/utils/validator';
import { getDifferences, hasAnyTrue } from './utils';
export type FormidableDeltaConfig<TYPE> = {
omit?: string[]; // (keyof TYPE)[];
only?: string[]; // (keyof TYPE)[];
};
export type FormidableConfig = {
enableReset?: boolean;
enableModifyNotification?: boolean;
singleLineForm?: boolean;
};
const initialFormConfig: Required<FormidableConfig> = {
enableReset: true,
enableModifyNotification: true,
singleLineForm: false,
};
export const useFormidable = <TYPE extends object = object>({
initialValues = {} as TYPE,
configuration: inputConfiguration = initialFormConfig,
deltaConfig,
resolver = (_data: TYPE) => {
return {};
},
}: {
initialValues?: TYPE;
configuration?: FormidableConfig;
deltaConfig?: FormidableDeltaConfig<TYPE>;
resolver?: (data: any) => Record<string, string>;
}) => {
const configuration: Required<FormidableConfig> = {
...initialFormConfig,
...inputConfiguration,
};
const [values, setValues] = useState<TYPE>({ ...initialValues } as TYPE);
const [errors, setErrors] = useState<object>({});
const [initialData, setInitialData] = useState<TYPE>(initialValues);
const [isModify, setIsModify] = useState<{ [key: string]: boolean }>({});
const [isFormModified, setIsFormModified] = useState<boolean>(false);
useEffect(() => {
setInitialData((previous) => {
//console.log(`FORMIDABLE: useMemo initial Values(${JSON.stringify(initialValues)})`);
const previousJson = JSON.stringify(previous);
const newJson = JSON.stringify(initialValues);
if (previousJson === newJson) {
return previous;
}
//console.log(`FORMIDABLE: ==> update new values`);
setValues({ ...initialValues });
const ret = getDifferences(initialValues, initialValues);
setIsModify(ret);
setIsFormModified(hasAnyTrue(ret));
return initialValues;
});
}, [
initialValues,
setInitialData,
setValues,
setIsModify,
setIsFormModified,
]);
const restoreValues = useCallback(() => {
setValues({ ...initialData });
}, [setValues, initialData]);
const setValuesExternal = useCallback(
(data: object) => {
//console.log(`FORMIDABLE: setValuesExternal(${JSON.stringify(data)}) ==> keys=${Object.keys(data)}`);
setValues((previous) => {
const newValues = { ...previous, ...data };
const ret = getDifferences(initialData, newValues);
setIsModify(ret);
setIsFormModified(hasAnyTrue(ret));
setErrors(resolver(newValues));
return newValues;
});
},
[setValues, initialData, setErrors, setIsFormModified, setIsModify]
);
const restoreValue = useCallback(
(data: object) => {
setValues((previous) => {
const keysInPrevious = Object.keys(previous);
const newValue = { ...previous };
let countModify = 0;
//console.log(`restore value ${JSON.stringify(data, null, 2)}`);
for (const key of Object.keys(data)) {
if (!keysInPrevious.includes(key)) {
continue;
}
if (data[key] === false) {
continue;
}
newValue[key] = initialValues[key];
countModify++;
}
if (countModify === 0) {
return previous;
}
//console.log(`initialData data ${JSON.stringify(initialData, null, 2)}`);
//console.log(`New data ${JSON.stringify(newValue, null, 2)}`);
const ret = getDifferences(initialData, newValue);
setIsModify(ret);
setIsFormModified(hasAnyTrue(ret));
return newValue;
});
},
[setValues, initialData, setIsFormModified, setIsModify]
);
const getDeltaData = useCallback(
({ omit = [], only }: FormidableDeltaConfig<TYPE> = {}) => {
const out = {};
Object.keys(isModify).forEach((key) => {
if (omit.includes(key) || (only && !only.includes(key))) {
return;
}
if (!isModify[key]) {
return;
}
const tmpValue = values[key];
if (isNullOrUndefined(tmpValue)) {
out[key] = null;
} else {
out[key] = tmpValue;
}
});
return out;
},
[isModify, values]
);
return {
getDeltaData,
isFormModified,
isModify,
restoreValues,
restoreValue,
setValues: setValuesExternal,
values,
errors,
configuration,
deltaConfig,
};
};
export type UseFormidableReturn = ReturnType<typeof useFormidable>;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
export * from './AudioPlayer';
export * from './Cover';
export * from './EmptyEnd';
export * from './Icon';
export * from './SearchInput';

View File

@ -0,0 +1,55 @@
import { Flex, Text } from '@chakra-ui/react';
import { LuMusic2, LuPlay } from 'react-icons/lu';
import { Media } from '@/back-api';
import { Covers } from '@/components/Cover';
import { ContextMenu, MenuElement } from '@/components/contextMenu/ContextMenu';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
export type DisplayMediaProps = {
media: Media;
onClick?: () => void;
contextMenu?: MenuElement[];
};
export const DisplayMedia = ({
media,
onClick,
contextMenu,
}: DisplayMediaProps) => {
const { MediaActive } = useActivePlaylistService();
return (
<Flex direction="row" width="full" height="full">
<Covers
data={media?.covers}
size="50"
height="full"
iconEmpty={MediaActive?.id === media.id ? <LuPlay /> : <LuMusic2 />}
onClick={onClick}
/>
<Flex
direction="column"
width="full"
height="full"
paddingLeft="5px"
overflowX="hidden"
onClick={onClick}
>
<Text
as="span"
alignContent="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
// TODO: noOfLines={[1, 2]}
marginY="auto"
color={MediaActive?.id === media.id ? 'green.700' : undefined}
>
[{media.episode}] {media.name}
</Text>
</Flex>
<ContextMenu elements={contextMenu} />
</Flex>
);
};

View File

@ -0,0 +1,125 @@
import { Flex, Text } from '@chakra-ui/react';
import { LuMusic2, LuPlay } from 'react-icons/lu';
import { Media } from '@/back-api';
import { Covers } from '@/components/Cover';
import { ContextMenu, MenuElement } from '@/components/contextMenu/ContextMenu';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificSeason } from '@/service/Season';
import { useSpecificSeries } from '@/service/Series';
import { useSpecificType } from '@/service/Type';
export type DisplayMediaProps = {
media: Media;
onClick?: () => void;
contextMenu?: MenuElement[];
};
export const DisplayMediaFull = ({
media,
onClick,
contextMenu,
}: DisplayMediaProps) => {
const { MediaActive } = useActivePlaylistService();
const { dataSeason } = useSpecificSeason(media?.seasonId);
const { dataType } = useSpecificType(media?.typeId);
const { dataSeries } = useSpecificSeries(media?.seriesId);
return (
<Flex
direction="row"
width="full"
height="full"
data-testid="display-Media-full"
>
<Covers
data={media?.covers}
size="60px"
marginY="auto"
iconEmpty={MediaActive?.id === media.id ? <LuPlay /> : <LuMusic2 />}
onClick={onClick}
/>
<Flex
direction="column"
width="full"
height="full"
paddingLeft="5px"
overflowX="hidden"
onClick={onClick}
>
<Text
as="span"
alignContent="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
// TODO: noOfLines={1}
color={MediaActive?.id === media.id ? 'green.700' : undefined}
>
{media.name} {media.episode && ` [${media.episode}]`}
</Text>
{dataSeason && (
<Text
as="span"
alignContent="left"
fontSize="15px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
//noOfLines={1}
marginY="auto"
color={MediaActive?.id === media.id ? 'green.700' : undefined}
>
<Text as="span" fontWeight="normal">
Season:
</Text>{' '}
{dataSeason.name}
</Text>
)}
{dataSeries && (
<Text
as="span"
alignContent="left"
fontSize="15px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
//noOfLines={1}
marginY="auto"
color={MediaActive?.id === media.id ? 'green.700' : undefined}
>
<Text as="span" fontWeight="normal">
Series(s):
</Text>{' '}
{dataSeries && dataSeries.name}
</Text>
)}
{dataType && (
<Text
as="span"
alignContent="left"
fontSize="15px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
//noOfLines={1}
marginY="auto"
color={MediaActive?.id === media.id ? 'green.700' : undefined}
>
<Text as="span" fontWeight="normal">
Type:
</Text>{' '}
{dataType.name}
</Text>
)}
</Flex>
<ContextMenu
elements={contextMenu}
data-testid="display-Media-full_context-menu"
/>
</Flex>
);
};

View File

@ -0,0 +1,30 @@
import { Media } from '@/back-api';
import { MenuElement } from '@/components/contextMenu/ContextMenu';
import { useSpecificMedia } from '@/service/Media';
import { DisplayMediaFull } from './DisplayMediaFull';
import { DisplayMediaSkeleton } from './DisplayMediaSkeleton';
export type DisplayMediaProps = {
MediaId: Media['id'];
onClick?: () => void;
contextMenu?: MenuElement[];
};
export const DisplayMediaFullId = ({
MediaId,
onClick,
contextMenu,
}: DisplayMediaProps) => {
const { dataMedia } = useSpecificMedia(MediaId);
if (dataMedia) {
return (
<DisplayMediaFull
media={dataMedia}
onClick={onClick}
contextMenu={contextMenu}
/>
);
} else {
return <DisplayMediaSkeleton />;
}
};

View File

@ -0,0 +1,30 @@
import { Flex, Skeleton } from '@chakra-ui/react';
export const DisplayMediaSkeleton = () => {
return (
<Flex direction="row" width="full" height="full">
<Skeleton
borderRadius="0px"
height="50"
width="50"
minWidth="50"
minHeight="50"
/>
<Flex
direction="column"
width="full"
height="full"
paddingLeft="5px"
overflowX="hidden"
>
{/* <SkeletonText
skeletonHeight="20px"
noOfLines={1}
gap={0}
width="50%"
marginY="auto"
/> */}
</Flex>
</Flex>
);
};

View File

@ -0,0 +1,235 @@
import { useRef, useState } from 'react';
import { Flex, Text, useDisclosure } from '@chakra-ui/react';
import {
MdAdminPanelSettings,
MdDeleteForever,
MdEdit,
MdWarning,
} from 'react-icons/md';
import { useNavigate, useParams } from 'react-router-dom';
import { SeasonResource, SeasonWrite } from '@/back-api';
import { FormCovers } from '@/components/form/FormCovers';
import { FormGroupShow } from '@/components/form/FormGroup';
import { FormInput } from '@/components/form/FormInput';
import { FormTextarea } from '@/components/form/FormTextarea';
import { ConfirmPopUp } from '@/components/popup/ConfirmPopUp';
import {
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
} from '@/components/ui/dialog';
import { useSeasonCountVideo, useSeasonService, useSpecificSeason } from '@/service/Season';
import { useServiceContext } from '@/service/ServiceContext';
import { isNullOrUndefined } from '@/utils/validator';
import { Formidable, useFormidable } from '../formidable';
import { Button } from '../ui/button';
export type SeasonEditPopUpProps = {};
export const SeasonEditPopUp = ({}: SeasonEditPopUpProps) => {
const { SeasonId } = useParams();
const SeasonIdInt = isNullOrUndefined(SeasonId)
? undefined
: parseInt(SeasonId, 10);
const { session } = useServiceContext();
const { seasonCountVideo } = useSeasonCountVideo(SeasonIdInt);
const { store } = useSeasonService();
const { dataSeason } = useSpecificSeason(SeasonIdInt);
const [admin, setAdmin] = useState(false);
const navigate = useNavigate();
const disclosure = useDisclosure();
const onClose = () => {
navigate('../../', { relative: 'path' });
};
const onRemove = () => {
if (isNullOrUndefined(SeasonIdInt)) {
return;
}
store.remove(
SeasonIdInt,
SeasonResource.remove({
restConfig: session.getRestConfig(),
params: {
id: SeasonIdInt,
},
})
);
onClose();
};
const initialRef = useRef<HTMLButtonElement>(null);
const finalRef = useRef<HTMLButtonElement>(null);
const form = useFormidable<SeasonWrite>({
initialValues: dataSeason,
deltaConfig: { omit: ['covers'] },
});
const onSave = async (deltaData: SeasonWrite) => {
if (isNullOrUndefined(SeasonIdInt)) {
return;
}
console.log(`onSave = ${JSON.stringify(deltaData, null, 2)}`);
store.update(
SeasonResource.patch({
restConfig: session.getRestConfig(),
data: deltaData,
params: {
id: SeasonIdInt,
},
})
);
};
const onUriSelected = (uri: string) => {
if (isNullOrUndefined(SeasonIdInt)) {
return;
}
console.error("not implemented");
// store.update(
// SeasonResource.uploadCover({
// restConfig: session.getRestConfig(),
// data: {
// uri,
// },
// params: {
// id: SeasonIdInt,
// },
// })
// );
};
const onFilesSelected = (files: File[]) => {
files.forEach((element) => {
console.log(`Select file: '${element.name}'`);
});
if (isNullOrUndefined(SeasonIdInt)) {
return;
}
store.update(
SeasonResource.uploadCover({
restConfig: session.getRestConfig(),
data: {
file: files[0],
},
params: {
id: SeasonIdInt,
},
})
);
};
const onRemoveCover = (index: number) => {
if (isNullOrUndefined(dataSeason?.covers)) {
return;
}
if (isNullOrUndefined(SeasonIdInt)) {
return;
}
store.update(
SeasonResource.removeCover({
restConfig: session.getRestConfig(),
params: {
id: SeasonIdInt,
coverId: dataSeason.covers[index],
},
})
);
};
return (
<DialogRoot
//initialFocusRef={initialRef}
//finalFocusRef={finalRef}
//closeOnOverlayClick={false}
//onOpenChange={onClose}
open={true}
data-testid="Season-edit-pop-up"
>
<DialogContent>
<Formidable.From form={form} onSubmitDelta={onSave}>
<DialogHeader>Edit Season</DialogHeader>
{/* <DialogCloseButton ref={finalRef} /> */}
<DialogBody pb={6} gap="0px" paddingLeft="18px">
{admin && (
<>
<FormGroupShow isRequired label="Id">
<Text>{dataSeason?.id}</Text>
</FormGroupShow>
{seasonCountVideo !== 0 && (
<Flex paddingLeft="14px">
<MdWarning color="red.600" />
<Text paddingLeft="6px" color="red.600" fontWeight="bold">
Can not remove Season {seasonCountVideo} Media(s)
depend on it.
</Text>
</Flex>
)}
<FormGroupShow label="Action(s):">
<Button
onClick={disclosure.onOpen}
marginRight="auto"
colorPalette="@danger"
disabled={seasonCountVideo !== 0}
>
<MdDeleteForever /> Remove Media
</Button>
</FormGroupShow>
<ConfirmPopUp
disclosure={disclosure}
title="Remove Season"
body={`Remove Season [${dataSeason?.id}] ${dataSeason?.name}`}
confirmTitle="Remove"
onConfirm={onRemove}
/>
</>
)}
{!admin && (
<>
<FormInput
name="name"
isRequired
label="Title"
ref={initialRef}
/>
<FormTextarea name="description" label="Description" />
<FormInput name="publication" label="Publication" />
<FormCovers
name="covers"
onFilesSelected={onFilesSelected}
onUriSelected={onUriSelected}
onRemove={onRemoveCover}
/>
</>
)}
</DialogBody>
<DialogFooter>
<Button
onClick={() => setAdmin((value) => !value)}
marginRight="auto"
>
{admin ? (
<>
<MdEdit />
Edit
</>
) : (
<>
<MdAdminPanelSettings />
Admin
</>
)}
</Button>
{!admin && form.isFormModified && (
<Button colorScheme="blue" mr={3} type="submit">
Save
</Button>
)}
<Button onClick={onClose}>Cancel</Button>
</DialogFooter>
</Formidable.From>
</DialogContent>
</DialogRoot>
);
};

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