[FEAT] basic working karusic

This commit is contained in:
Edouard DUPIN 2024-08-25 12:51:14 +02:00
parent 1d4c547d89
commit 30bc82edb3
33 changed files with 1705 additions and 607 deletions

View File

@ -36,4 +36,4 @@
"defer" "defer"
] ]
} }
} }

View File

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

View File

@ -1,103 +0,0 @@
###############################################################
## Install dependency:
###############################################################
FROM node:latest AS dependency
# For pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
# copy the credential
COPY npmrc /root/.npmrc
COPY package.json pnpm-lock.yaml ./
COPY src/theme ./src/theme
# TODO: install only the production environment:
RUN pnpm install --prod=false
###############################################################
## Install sources
###############################################################
FROM dependency AS load_sources
# JUST to get the vertion of the application and his sha...
COPY build.js \
version.txt \
tsconfig.json \
tsconfig.node.json \
vite.config.mts \
.env.validator.js \
index.html \
./
COPY public public
COPY src src
#We are not in prod mode ==> we need to overwrite the production env.
ARG env=docker/.env.production
COPY ${env} .env
###############################################################
## Run the linter
###############################################################
FROM load_sources AS check
COPY .eslintrc.json app-build.json ./
# Run linter
RUN pnpm lint .
RUN pnpm tsc --noEmit
###############################################################
## Run the Unit test
###############################################################
FROM load_sources AS unittest
COPY vitest.config.mts app-build.json ./
# Run unit test
RUN pnpm test
###############################################################
## Build the story-book
###############################################################
FROM load_sources AS builder_storybook
COPY app-build.json ./app-build.json
COPY .storybook ./.storybook/
# build the storybook in static
RUN SKIP_ENV_VALIDATIONS=1 pnpm storybook:build
###############################################################
## Build the sources
###############################################################
FROM load_sources AS builder
# build in bundle mode all the application
RUN pnpm static:build
###############################################################
## Runner environment:
###############################################################
FROM httpd:latest AS runner
WORKDIR /app
# configure HTTP server (add a redirection on the index.html to manage new app model to re-find the generic page):
RUN sed -e '/DocumentRoot/,/Directory>/d' -i /usr/local/apache2/conf/httpd.conf
RUN sed -r 's|#LoadModule rewrite_module|LoadModule rewrite_module|' -i /usr/local/apache2/conf/httpd.conf
RUN echo '<VirtualHost *:80> \n\
ServerName my-app \n\
DocumentRoot "/usr/local/apache2/htdocs" \n\
<Directory "/usr/local/apache2/htdocs"> \n\
Options Indexes FollowSymLinks \n\
AllowOverride None \n\
Require all granted \n\
RewriteEngine on \n\
# Do not rewrite files or directories \n\
RewriteCond %{REQUEST_FILENAME} -f [OR] \n\
RewriteCond %{REQUEST_FILENAME} -d \n\
RewriteRule ^ - [L] \n\
# Rewrite everything else to index.html to allow HTML5 state links \n\
RewriteRule ^ app/index.html [L] \n\
</Directory> \n\
</VirtualHost> \n\
' >> /usr/local/apache2/conf/httpd.conf
# copy artifact build from the 'build environment'
COPY --from=builder /app/dist /usr/local/apache2/htdocs/app

View File

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="256"
height="256"
viewBox="0 0 67.733333 67.733333"
version="1.1"
id="svg8"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="ikon.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">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="52.480467"
inkscape:cy="138.73493"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:snap-text-baseline="false"
inkscape:window-width="1918"
inkscape:window-height="1038"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="1">
<inkscape:grid
type="xygrid"
id="grid4504" />
</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)">
<g
aria-label="K"
transform="scale(1.0347881,0.96638145)"
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:#000000;fill-opacity:1;stroke:none;stroke-width:2.11376619"
id="text821">
<path
d="m 12.784421,241.62303 h 8.949095 v 27.37877 l 25.568842,-27.37877 6.39221,6.84469 -20.455074,21.90302 20.455074,27.37876 -6.39221,5.47576 -19.176632,-27.37877 -6.39221,6.84469 0,20.53408 h -8.949095 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:84.55024719px;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;stroke-width:2.11376619;fill:#ff0000;fill-opacity:1"
id="path823"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccc" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -6,8 +6,8 @@
<title>Karusic</title> <title>Karusic</title>
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</head> </head>
<body className="flex flex-col"> <body style="width:100vw;height:100vh;min-width:100%;min-height:100%;">
<div id="root"></div> <div id="root" style="width:100%;height:100%;min-width:100%;min-height:100%;"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

180
front2/pnpm-lock.yaml generated
View File

@ -38,9 +38,6 @@ importers:
'@emotion/styled': '@emotion/styled':
specifier: 11.13.0 specifier: 11.13.0
version: 11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1) version: 11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
'@mui/icons-material':
specifier: 5.16.7
version: 5.16.7(@mui/material@5.16.7(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
allotment: allotment:
specifier: 1.20.2 specifier: 1.20.2
version: 1.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -1940,94 +1937,6 @@ packages:
'@types/react': '>=16' '@types/react': '>=16'
react: '>=16' react: '>=16'
'@mui/core-downloads-tracker@5.16.7':
resolution: {integrity: sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==}
'@mui/icons-material@5.16.7':
resolution: {integrity: sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@mui/material': ^5.0.0
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/material@5.16.7':
resolution: {integrity: sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
'@mui/private-theming@5.16.6':
resolution: {integrity: sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/styled-engine@5.16.6':
resolution: {integrity: sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/styled': ^11.3.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@mui/system@5.16.7':
resolution: {integrity: sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
'@mui/types@7.2.15':
resolution: {integrity: sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@mui/utils@5.16.6':
resolution: {integrity: sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -3113,10 +3022,6 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
co@4.6.0: co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@ -8677,89 +8582,6 @@ snapshots:
'@types/react': 18.3.3 '@types/react': 18.3.3
react: 18.3.1 react: 18.3.1
'@mui/core-downloads-tracker@5.16.7': {}
'@mui/icons-material@5.16.7(@mui/material@5.16.7(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.24.7
'@mui/material': 5.16.7(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.3
'@mui/material@5.16.7(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.24.7
'@mui/core-downloads-tracker': 5.16.7
'@mui/system': 5.16.7(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
'@mui/types': 7.2.15(@types/react@18.3.3)
'@mui/utils': 5.16.6(@types/react@18.3.3)(react@18.3.1)
'@popperjs/core': 2.11.8
'@types/react-transition-group': 4.4.10
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-is: 18.3.1
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
optionalDependencies:
'@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@mui/private-theming@5.16.6(@types/react@18.3.3)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.24.7
'@mui/utils': 5.16.6(@types/react@18.3.3)(react@18.3.1)
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.3
'@mui/styled-engine@5.16.6(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1)':
dependencies:
'@babel/runtime': 7.24.7
'@emotion/cache': 11.13.1
csstype: 3.1.3
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
'@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
'@mui/system@5.16.7(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.24.7
'@mui/private-theming': 5.16.6(@types/react@18.3.3)(react@18.3.1)
'@mui/styled-engine': 5.16.6(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1)
'@mui/types': 7.2.15(@types/react@18.3.3)
'@mui/utils': 5.16.6(@types/react@18.3.3)(react@18.3.1)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
'@emotion/react': 11.13.0(@types/react@18.3.3)(react@18.3.1)
'@emotion/styled': 11.13.0(@emotion/react@11.13.0(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@mui/types@7.2.15(@types/react@18.3.3)':
optionalDependencies:
'@types/react': 18.3.3
'@mui/utils@5.16.6(@types/react@18.3.3)(react@18.3.1)':
dependencies:
'@babel/runtime': 7.24.7
'@mui/types': 7.2.15(@types/react@18.3.3)
'@types/prop-types': 15.7.12
clsx: 2.1.1
prop-types: 15.8.1
react: 18.3.1
react-is: 18.3.1
optionalDependencies:
'@types/react': 18.3.3
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@ -10092,8 +9914,6 @@ snapshots:
clone@1.0.4: {} clone@1.0.4: {}
clsx@2.1.1: {}
co@4.6.0: {} co@4.6.0: {}
collect-v8-coverage@1.0.2: {} collect-v8-coverage@1.0.2: {}

View File

@ -0,0 +1,65 @@
<?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(0,255,0)" 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>
<filter style="color-interpolation-filters:sRGB" inkscape:label="Drop Shadow" id="filter5338-3" x="-0.12319682"
y="-0.081815216" width="1.2463936" height="1.1636304">
<feFlood flood-opacity="1" flood-color="rgb(0,255,0)" result="flood" id="feFlood5328-6" />
<feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1" id="feComposite5330-7" />
<feGaussianBlur in="composite1" stdDeviation="2.1" result="blur" id="feGaussianBlur5332-5" />
<feOffset dx="0" dy="0" result="offset" id="feOffset5334-3" />
<feComposite in="SourceGraphic" in2="offset" operator="over" result="composite2" id="feComposite5336-5" />
</filter>
</defs>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="4" inkscape:cx="27.125" inkscape:cy="217.5"
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" />
</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 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.200545,279.95309 v 61.60223 h 8.949095 v -20.53407 l 6.392211,-6.84449 19.176632,27.37856 6.392207,-5.47534 -20.455071,-27.37918 20.455071,-21.9026 -6.392207,-6.84511 -25.568843,27.37918 v -27.37918 z m 34.623041,3.89642 0.08113,0.91996 c -0.319958,0.0205 -0.600028,0.12056 -0.843281,0.38008 -0.481327,0.51353 -0.393613,1.29347 0.321455,1.96887 0.73708,0.69622 1.51285,0.73176 2.02523,0.18511 0.24321,-0.25953 0.31335,-0.58883 0.29195,-0.94218 l 0.90904,0.0154 c 0.0722,0.61998 -0.12994,1.18923 -0.58021,1.66963 -0.83844,0.89456 -2.190053,1.07514 -3.400168,-0.0679 -1.188114,-1.12225 -1.171861,-2.52288 -0.266137,-3.48919 0.424395,-0.45279 0.991241,-0.62554 1.460989,-0.63984 z m -6.777588,6.44528 0.676714,0.6386 -0.786733,0.83975 2.228054,2.10401 0.786119,-0.83914 0.676714,0.63861 -2.333772,2.49087 -0.676714,-0.63923 0.786734,-0.83913 -2.228054,-2.10462 -0.786734,0.83913 -0.676099,-0.6386 z m -5.778189,6.97221 0.129073,0.89158 c -0.419593,0.0825 -0.731113,0.21546 -1.057173,0.56333 -0.253606,0.27057 -0.314123,0.55655 -0.105103,0.75398 0.220021,0.20783 0.524373,0.0375 0.977271,-0.18017 l 0.595582,-0.27025 c 0.615941,-0.3031 1.187271,-0.32558 1.693321,0.15241 0.599556,0.56632 0.616629,1.36433 -0.19361,2.44089 -0.677315,0.60577 -1.102122,0.82218 -1.800268,0.88108 l -0.121083,-0.98844 c 0.484299,-0.0631 0.943518,-0.25438 1.274754,-0.60776 0.320886,-0.34235 0.344427,-0.63278 0.16841,-0.79903 -0.258525,-0.24419 -0.521361,-0.0857 -0.985261,0.12155 l -0.637377,0.28198 c -0.526655,0.25209 -1.170772,0.33129 -1.693321,-0.16228 -0.594058,-0.56111 -0.565292,-1.54388 0.169639,-2.32797 0.403694,-0.4307 0.971757,-0.716 1.585146,-0.7509 z m -6.585821,6.21884 2.205312,2.08364 c 0.929589,0.87805 1.047872,1.78072 0.224957,2.65869 -0.81774,0.8725 -1.743461,0.83116 -2.67305,-0.0469 l -2.205313,-2.08364 0.765836,-0.81692 2.288288,2.16138 c 0.429042,0.40526 0.810303,0.46332 1.126013,0.12649 0.320885,-0.34235 0.244649,-0.72634 -0.184391,-1.1316 l -2.288288,-2.16138 z m -4.57965,9.20516 2.197937,0.53865 -0.853729,0.91071 -1.930571,-0.5294 -0.407503,0.43499 1.287047,1.21551 -0.760919,0.81199 -3.580867,-3.38245 1.200998,-1.28091 c 0.446394,-0.47625 0.945677,-0.80165 1.465291,-0.78175 0.311768,0.0119 0.630508,0.14843 0.950227,0.45042 0.523732,0.4947 0.617235,1.06543 0.432089,1.61224 m 4e-6,5e-5 v 0 m -1.574086,-1.01133 c -0.219842,-0.009 -0.443011,0.13842 -0.69208,0.40414 l -0.378001,0.40291 1.006773,0.95081 0.378001,-0.4029 c 0.39852,-0.42517 0.434395,-0.8287 0.08236,-1.16122 -0.134075,-0.12664 -0.265149,-0.18828 -0.397054,-0.19374 z m -7.033891,5.87578 4.63128,2.26134 -0.807017,0.86135 -1.059017,-0.58493 -1.015378,1.08286 0.645982,1.02608 -0.781816,0.8342 -2.529841,-4.50355 z m 1.278214,2.86141 0.707674,-0.7537 -1.841448,-1.03411 -0.02028,0.0222 z m 0.707674,-0.7537 0.779358,0.43005 z"
sodipodi:nodetypes="cccccccccccccccsccccscsccccccccccccccccssccssccssccssccscccccscccsccccccccscccccccccssccccccccccccccccccc" />
</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: 8.2 KiB

View File

@ -0,0 +1,300 @@
import { SyntheticEvent, useEffect, useRef, useState } from 'react';
import {
Box,
Button,
Flex,
IconButton,
Slider,
SliderFilledTrack,
SliderThumb,
SliderTrack,
Text,
position,
} from '@chakra-ui/react';
import {
MdFastForward,
MdFastRewind,
MdGraphicEq,
MdNavigateBefore,
MdNavigateNext,
MdOutlinePlayArrow,
MdPause,
MdPlayArrow,
MdStop,
MdTrendingFlat,
} from 'react-icons/md';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificTrack } from '@/service/Track';
import { DataUrlAccess } from '@/utils/data-url-access';
import { useThemeMode } from '@/utils/theme-tools';
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 { mode } = useThemeMode();
const { playTrackList, trackOffset, previous, next } =
useActivePlaylistService();
const audioRef = useRef<HTMLAudioElement>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [timeProgress, setTimeProgress] = useState<number>(0);
const [duration, setDuration] = useState<number>(0);
const { dataTrack, updateTrackId } = useSpecificTrack(
trackOffset !== undefined ? playTrackList[trackOffset] : undefined
);
useEffect(() => {
console.log(`detect change of playlist ...`);
updateTrackId(
trackOffset !== undefined ? playTrackList[trackOffset] : undefined
);
}, [playTrackList, trackOffset, updateTrackId]);
const [mediaSource, setMediaSource] = useState<string>('');
useEffect(() => {
setMediaSource(
dataTrack && dataTrack?.dataId
? DataUrlAccess.getUrl(dataTrack?.dataId)
: ''
);
}, [dataTrack, setMediaSource]);
const backColor = mode('back.100', 'back.800');
const configButton = {
borderRadius: 'full',
backgroundColor: '#00000000',
_hover: {
boxShadow: 'outline-over',
bgColor: 'brand.500',
},
};
useEffect(() => {
if (!audioRef || !audioRef.current) {
return;
}
if (isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
}
}, [isPlaying, audioRef]);
const onAudioEnded = () => {
// TODO...
};
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 curent 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 = () => {};
/**
* 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);
};
return (
<>
<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
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
{dataTrack?.name ?? '???'}
</Text>
<Text
align="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
artist / title album
</Text>
<Box width="full" paddingX="15px">
<Slider
aria-label="slider-ex-4"
defaultValue={0}
value={timeProgress}
min={0}
max={duration}
step={0.1}
onChange={onSeek}
>
<SliderTrack bg="gray.200" height="10px" borderRadius="full">
<SliderFilledTrack bg="brand.600" />
</SliderTrack>
<SliderThumb boxSize={6}>
<Box color="brand.600" as={MdGraphicEq} />
</SliderThumb>
</Slider>
</Box>
<Flex>
<Text
align="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
>
{formatTime(timeProgress)}
</Text>
<Text align="left" fontSize="16px" userSelect="none">
{formatTime(duration)}
</Text>
</Flex>
<Flex gap="5px">
<IconButton
{...configButton}
aria-label={'Play'}
icon={
isPlaying ? <MdPause size="30px" /> : <MdPlayArrow size="30px" />
}
onClick={onPlay}
/>
<IconButton
{...configButton}
aria-label={'Stop'}
icon={<MdStop size="30px" />}
onClick={onStop}
/>
<IconButton
{...configButton}
aria-label={'Previous track'}
icon={<MdNavigateBefore size="30px" />}
onClick={onNavigatePrevious}
marginLeft="auto"
/>
<IconButton
{...configButton}
aria-label={'jump 15sec in past'}
icon={<MdFastRewind size="30px" />}
onClick={onFastRewind}
/>
<IconButton
{...configButton}
aria-label={'jump 15sec in future'}
icon={<MdFastForward size="30px" />}
onClick={onFastForward}
/>
<IconButton
{...configButton}
aria-label={'Next track'}
icon={<MdNavigateNext size="30px" />}
marginRight="auto"
onClick={onNavigateNext}
/>
<IconButton
{...configButton}
aria-label={'continue to the end'}
icon={<MdTrendingFlat size="30px" />}
onClick={onTypePlay}
/>
</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,40 @@
import { ReactElement } from 'react';
import { Box, BoxProps } from '@chakra-ui/react';
import { Image } from '@chakra-ui/react';
import { DataUrlAccess } from '@/utils/data-url-access';
export type CoversProps = BoxProps & {
data?: string[];
size?: string;
iconEmpty?: ReactElement;
};
export const Covers = ({
data,
iconEmpty,
size = '100px',
...rest
}: CoversProps) => {
if (!data || data.length < 1) {
if (iconEmpty) {
return iconEmpty;
} else {
return (
<Box
width={size}
height={size}
minHeight={size}
minWidth={size}
borderColor="blue"
borderWidth="1px"
margin="auto"
{...rest}
></Box>
);
}
}
const url = DataUrlAccess.getThumbnailUrl(data[0]);
return <Image src={url} boxSize={size} {...rest} />;
};

View File

@ -1,8 +1,9 @@
import React, { ReactNode, useEffect } from 'react'; import React, { ReactNode, useEffect } from 'react';
import { Flex } from '@chakra-ui/react'; import { Flex, Image } from '@chakra-ui/react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import background from '@/assets/images/ikon.svg';
import { TOP_BAR_HEIGHT } from '@/components/TopBar/TopBar'; import { TOP_BAR_HEIGHT } from '@/components/TopBar/TopBar';
export type LayoutProps = React.PropsWithChildren<unknown> & { export type LayoutProps = React.PropsWithChildren<unknown> & {
@ -17,19 +18,33 @@ export const PageLayout = ({ children }: LayoutProps) => {
}, [pathname]); }, [pathname]);
return ( return (
<Flex <>
direction="column" <Flex
overflowX="auto" minH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
overflowY="auto" maxH={`calc(100vh - ${TOP_BAR_HEIGHT})`}
minH={`calc(100vh - ${TOP_BAR_HEIGHT})`} position="absolute"
maxH={`calc(100vh - ${TOP_BAR_HEIGHT})`} top={TOP_BAR_HEIGHT}
position="absolute" bottom={0}
top={TOP_BAR_HEIGHT} left={0}
bottom={0} right={0}
left={0} zIndex={-1}
right={0} >
> <Image src={background} boxSize="90%" margin="auto" opacity="30%" />
{children} </Flex>
</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}
>
{children}
</Flex>
</>
); );
}; };

View File

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

View File

@ -93,6 +93,15 @@ export const TopBar = ({ children }: TopBarProps) => {
</Text> </Text>
</Button> </Button>
{children} {children}
<Text
fontSize="25px"
fontWeight="bold"
textTransform="uppercase"
marginRight="auto"
userSelect="none"
>
Karusic
</Text>
{session?.state !== SessionState.CONNECTED && ( {session?.state !== SessionState.CONNECTED && (
<> <>
<Button {...buttonProperty} onClick={onSignIn}> <Button {...buttonProperty} onClick={onSignIn}>
@ -149,6 +158,7 @@ export const TopBar = ({ children }: TopBarProps) => {
<DrawerContent> <DrawerContent>
<DrawerHeader <DrawerHeader
paddingY="auto" paddingY="auto"
as="button"
onClick={drawerDisclose.onClose} onClick={drawerDisclose.onClose}
boxShadow={'0px 2px 4px ' + colors.back[900]} boxShadow={'0px 2px 4px ' + colors.back[900]}
backgroundColor={backColor} backgroundColor={backColor}

View File

@ -8,6 +8,8 @@ import {
useTheme, useTheme,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { environment } from '@/environment'; import { environment } from '@/environment';
const Illustration = ({ colorScheme = 'gray', ...rest }) => { const Illustration = ({ colorScheme = 'gray', ...rest }) => {
@ -111,12 +113,9 @@ const Illustration = ({ colorScheme = 'gray', ...rest }) => {
export const Error404 = () => { export const Error404 = () => {
return ( return (
<Center flex="1" p="8"> <>
<Stack <TopBar />
direction={{ base: 'column', md: 'row' }} <PageLayoutInfoCenter>
align="center"
spacing="0"
>
<Illustration /> <Illustration />
<Box textAlign={{ base: 'center', md: 'left' }}> <Box textAlign={{ base: 'center', md: 'left' }}>
<Heading>Erreur 404</Heading> <Heading>Erreur 404</Heading>
@ -127,7 +126,7 @@ export const Error404 = () => {
Retour à l'accueil Retour à l'accueil
</Button> </Button>
</Box> </Box>
</Stack> </PageLayoutInfoCenter>
</Center> </>
); );
}; };

View File

@ -5,12 +5,13 @@ import {
Routes, Routes,
} from 'react-router-dom'; } from 'react-router-dom';
import { AudioPlayer } from '@/components/AudioPlayer';
import { Error404 } from '@/errors'; import { Error404 } from '@/errors';
import { ErrorBoundary } from '@/errors/ErrorBoundary'; import { ErrorBoundary } from '@/errors/ErrorBoundary';
import { ServiceContextProvider } from '@/service/ServiceContext';
import { ArtistRoutes } from '@/scene/artist/ArtistRoutes'; import { ArtistRoutes } from '@/scene/artist/ArtistRoutes';
import { HomePage } from '@/scene/home/HomePage'; import { HomePage } from '@/scene/home/HomePage';
import { SSORoutes } from '@/scene/sso/SSORoutes'; import { SSORoutes } from '@/scene/sso/SSORoutes';
import { ServiceContextProvider } from '@/service/ServiceContext';
export const App = () => { export const App = () => {
return ( return (
@ -28,6 +29,7 @@ export const App = () => {
</Routes> </Routes>
</HistoryRouter> </HistoryRouter>
</ErrorBoundary> </ErrorBoundary>
<AudioPlayer />
</ServiceContextProvider> </ServiceContextProvider>
); );
}; };

View File

@ -0,0 +1,180 @@
import { Box, Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { LuDisc3, LuFileAudio, LuMusic2, LuUser } from 'react-icons/lu';
import { useNavigate, useParams } from 'react-router-dom';
import { Album, Artist, Track } from '@/back-api';
import { Covers } from '@/components/Cover';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificAlbum } from '@/service/Album';
import { useArtistService, useSpecificArtist } from '@/service/Artist';
import { useAlbumIdsOfAnArtist, useTracksOfAnAlbum } from '@/service/Track';
import { useThemeMode } from '@/utils/theme-tools';
export type DisplayTrackProps = {
track: Track;
};
export const DisplayTrack = ({ track }: DisplayTrackProps) => {
return (
<Flex direction="row" width="full" height="full">
<Covers
data={track?.covers}
size="50"
height="full"
iconEmpty={<LuMusic2 size="50" height="full" />}
/>
<Flex
direction="column"
width="full"
height="full"
paddingLeft="5px"
overflowX="hidden"
>
<Text
as="span"
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={[1, 2]}
marginY="auto"
>
[{track.track}] {track.name}
</Text>
</Flex>
</Flex>
);
};
export const EmptyEnd = () => {
return (
<Box
width="full"
height="25%"
minHeight="25%"
borderWidth="1px"
borderColor="red"
></Box>
);
};
export const ArtistAlbumDetailPage = () => {
const { artistId, albumId } = useParams();
const artistIdInt = artistId ? parseInt(artistId, 10) : undefined;
const albumIdInt = albumId ? parseInt(albumId, 10) : undefined;
const { mode } = useThemeMode();
const { playInList } = useActivePlaylistService();
const { dataArtist } = useSpecificArtist(artistIdInt);
const { dataAlbum } = useSpecificAlbum(albumIdInt);
const { tracksOnAnAlbum } = useTracksOfAnAlbum(albumIdInt);
const onSelectItem = (trackId: number) => {
//navigate(`/artist/${artistIdInt}/album/${albumId}`);
let currentPlay = 0;
const listTrackId: number[] = [];
for (let iii = 0; iii < tracksOnAnAlbum.length; iii++) {
listTrackId.push(tracksOnAnAlbum[iii].id);
if (tracksOnAnAlbum[iii].id === trackId) {
currentPlay = iii;
}
}
playInList(currentPlay, listTrackId);
};
console.log(`dataAlbum = ${JSON.stringify(dataAlbum, null, 2)}`);
if (!dataAlbum) {
<>
<TopBar />
<PageLayoutInfoCenter>
Fail to load artist id: {artistId}
</PageLayoutInfoCenter>
</>;
}
return (
<>
<TopBar />
<PageLayout>
<Flex
direction="row"
width="80%"
marginX="auto"
padding="10px"
gap="10px"
>
<Covers
data={dataArtist?.covers}
iconEmpty={<LuUser size="100" height="full" />}
/>
<Flex direction="column" width="80%" marginRight="auto">
<Text fontSize="24px" fontWeight="bold">
{dataArtist?.name}
</Text>
{dataArtist?.description && (
<Text>Description: {dataArtist?.description}</Text>
)}
{dataArtist?.firstName && (
<Text>first name: {dataArtist?.firstName}</Text>
)}
{dataArtist?.surname && <Text>surname: {dataArtist?.surname}</Text>}
{dataArtist?.birth && <Text>birth: {dataArtist?.birth}</Text>}
</Flex>
</Flex>
<Flex
direction="row"
width="80%"
marginX="auto"
padding="10px"
gap="10px"
>
<Covers
data={dataAlbum?.covers}
iconEmpty={<LuDisc3 size="100" height="full" />}
/>
<Flex direction="column" width="80%" marginRight="auto">
<Text fontSize="24px" fontWeight="bold">
{dataAlbum?.name}
</Text>
{dataAlbum?.description && (
<Text>Description: {dataAlbum?.description}</Text>
)}
{dataAlbum?.publication && (
<Text>first name: {dataAlbum?.publication}</Text>
)}
</Flex>
</Flex>
<Flex
direction="column"
gap="20px"
marginX="auto"
padding="20px"
width="80%"
>
{tracksOnAnAlbum?.map((data) => (
<Box
minWidth="100%"
height="60px"
border="1px"
borderColor="brand.900"
backgroundColor={mode('#FFFFFF88', '#00000088')}
key={data.id}
padding="5px"
as="button"
_hover={{
boxShadow: 'outline-over',
bgColor: mode('#FFFFFFF7', '#000000F7'),
}}
onClick={() => onSelectItem(data.id)}
>
<DisplayTrack track={data} />
</Box>
))}
<EmptyEnd />
</Flex>
</PageLayout>
</>
);
};

View File

@ -0,0 +1,126 @@
import { Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { LuDisc3, LuUser } from 'react-icons/lu';
import { useNavigate, useParams } from 'react-router-dom';
import { Album, Artist } from '@/back-api';
import { Covers } from '@/components/Cover';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { useSpecificAlbum } from '@/service/Album';
import { useArtistService, useSpecificArtist } from '@/service/Artist';
import { useAlbumIdsOfAnArtist } from '@/service/Track';
import { useThemeMode } from '@/utils/theme-tools';
export type DisplayAlbumProps = {
id: number;
};
export const DisplayAlbum = ({ id }: DisplayAlbumProps) => {
const { dataAlbum } = useSpecificAlbum(id);
return (
<Flex direction="row" width="full" height="full">
<Covers
data={dataAlbum?.covers}
size="100"
height="full"
iconEmpty={<LuDisc3 size="100" height="full" />}
/>
<Flex
direction="column"
width="150px"
maxWidth="150px"
height="full"
paddingLeft="5px"
overflowX="hidden"
>
<Text
as="span"
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={[1, 2]}
>
{dataAlbum?.name}
</Text>
</Flex>
</Flex>
);
};
export const ArtistDetailPage = () => {
const { artistId } = useParams();
const artistIdInt = artistId ? parseInt(artistId, 10) : undefined;
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (albumId: number) => {
navigate(`/artist/${artistIdInt}/album/${albumId}`);
};
const { dataArtist } = useSpecificArtist(artistIdInt);
const { albumIdsOfAnArtist } = useAlbumIdsOfAnArtist(artistIdInt);
if (!dataArtist) {
<>
<TopBar />
<PageLayoutInfoCenter>
Fail to load artist id: {artistId}
</PageLayoutInfoCenter>
</>;
}
return (
<>
<TopBar />
<PageLayout>
<Flex
direction="row"
width="80%"
marginX="auto"
padding="10px"
gap="10px"
>
<Covers
data={dataArtist?.covers}
iconEmpty={<LuUser size="100" height="full" />}
/>
<Flex direction="column" width="80%" marginRight="auto">
<Text fontSize="24px" fontWeight="bold">
{dataArtist?.name}
</Text>
{dataArtist?.description && (
<Text>Description: {dataArtist?.description}</Text>
)}
{dataArtist?.firstName && (
<Text>first name: {dataArtist?.firstName}</Text>
)}
{dataArtist?.surname && <Text>surname: {dataArtist?.surname}</Text>}
{dataArtist?.birth && <Text>birth: {dataArtist?.birth}</Text>}
</Flex>
</Flex>
<Wrap spacing="20px" marginX="auto" padding="20px" justify="center">
{albumIdsOfAnArtist?.map((data) => (
<WrapItem
width="270px"
height="120px"
border="1px"
borderColor="brand.900"
backgroundColor={mode('#FFFFFF88', '#00000088')}
key={data}
padding="5px"
as="button"
_hover={{
boxShadow: 'outline-over',
bgColor: mode('#FFFFFFF7', '#000000F7'),
}}
onClick={() => onSelectItem(data)}
>
<DisplayAlbum id={data} key={data} />
</WrapItem>
))}
</Wrap>
</PageLayout>
</>
);
};

View File

@ -1,15 +1,21 @@
import { Text } from '@chakra-ui/react'; import { Navigate, Route, Routes } from 'react-router-dom';
import { PageLayout } from '@/components/Layout/PageLayout'; import { Error404 } from '@/errors';
import { TopBar } from '@/components/TopBar/TopBar'; import { ArtistAlbumDetailPage } from '@/scene/artist/ArtistAlbumDetailPage';
import { ArtistDetailPage } from '@/scene/artist/ArtistDetailPage';
import { ArtistsPage } from '@/scene/artist/ArtistsPage';
export const ArtistRoutes = () => { export const ArtistRoutes = () => {
return ( return (
<> <Routes>
<TopBar /> <Route path="/" element={<Navigate to="all" replace />} />
<PageLayout> <Route path="all" element={<ArtistsPage />} />
<Text>artist Page</Text>; <Route path=":artistId" element={<ArtistDetailPage />} />
</PageLayout> <Route
</> path=":artistId/album/:albumId"
element={<ArtistAlbumDetailPage />}
/>
<Route path="*" element={<Error404 />} />
</Routes>
); );
}; };

View File

@ -0,0 +1,76 @@
import { Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { LuUser } from 'react-icons/lu';
import { useNavigate } from 'react-router-dom';
import { Artist } from '@/back-api';
import { Covers } from '@/components/Cover';
import { PageLayout } from '@/components/Layout/PageLayout';
import { TopBar } from '@/components/TopBar/TopBar';
import { useArtistService } from '@/service/Artist';
import { useThemeMode } from '@/utils/theme-tools';
export const ArtistsPage = () => {
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (data: Artist) => {
navigate(`/artist/${data.id}/`);
};
const { store } = useArtistService();
return (
<>
<TopBar />
<PageLayout>
<Text>All Artists:</Text>
<Wrap spacing="20px" marginX="auto" padding="20px">
{store.data?.map((data) => (
<WrapItem
width="270px"
height="120px"
border="1px"
borderColor="brand.900"
backgroundColor={mode('#FFFFFF88', '#00000088')}
key={data.id}
padding="5px"
as="button"
_hover={{
boxShadow: 'outline-over',
bgColor: mode('#FFFFFFF7', '#000000F7'),
}}
onClick={() => onSelectItem(data)}
>
<Flex direction="row" width="full" height="full">
<Covers
data={data.covers}
size="100"
height="full"
iconEmpty={<LuUser size="100" height="full" />}
/>
<Flex
direction="column"
width="150px"
maxWidth="150px"
height="full"
paddingLeft="5px"
overflowX="hidden"
>
<Text
as="span"
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={[1, 2]}
>
{data.name}
</Text>
</Flex>
</Flex>
</WrapItem>
))}
</Wrap>
</PageLayout>
</>
);
};

View File

@ -1,104 +1,95 @@
import { Text } from '@chakra-ui/react'; import { ReactElement } from 'react';
import { Center, Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { LuCrown, LuDisc3, LuEar, LuFileAudio, LuUser } from 'react-icons/lu';
import { useNavigate } from 'react-router-dom';
import { PageLayout } from '@/components/Layout/PageLayout'; import { PageLayout } from '@/components/Layout/PageLayout';
import { TopBar } from '@/components/TopBar/TopBar'; import { TopBar } from '@/components/TopBar/TopBar';
import { useThemeMode } from '@/utils/theme-tools';
type HomeListType = {
id: number;
name: string;
icon: ReactElement;
to: string;
};
const homeList: HomeListType[] = [
{
id: 1,
name: 'Genders',
icon: <LuCrown size="60%" height="full" />,
to: 'gender',
},
{
id: 2,
name: 'Artists',
icon: <LuUser size="60%" height="full" />,
to: 'artist',
},
{
id: 3,
name: 'Albums',
icon: <LuDisc3 size="60%" height="full" />,
to: 'album',
},
{
id: 4,
name: 'Tracks',
icon: <LuFileAudio size="60%" height="full" />,
to: 'track',
},
{
id: 5,
name: 'Playlists',
icon: <LuEar size="60%" height="full" />,
to: 'playlists',
},
];
export const HomePage = () => { export const HomePage = () => {
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (data: HomeListType) => {
navigate(data.to);
};
return ( return (
<> <>
<TopBar /> <TopBar />
<PageLayout> <PageLayout>
<Text>Home Page 1</Text> <Wrap spacing="20px" marginX="auto" padding="20px">
<br /> {homeList.map((data) => (
<Text>Home Page 2</Text> <WrapItem
<br /> width="200px"
<Text>Home Page 3</Text> height="190px"
<br /> border="1px"
<Text>Home Page 4</Text> borderColor="brand.900"
<br /> backgroundColor={mode('#FFFFFF88', '#00000088')}
<Text>Home Page 5</Text> key={data.id}
<br /> padding="5px"
<Text>Home Page 6</Text> as="button"
<br />;<Text>Home Page 2</Text> _hover={{
<br /> boxShadow: 'outline-over',
<Text> bgColor: mode('#FFFFFFF7', '#000000F7'),
Home Page 3Home Page 3Home Page 3Home Page 3Home Page 3Home Page 3Home }}
Page 3Home Page 3Home Page 3Home Page 3Home Page 3Home Page 3Home Page onClick={() => onSelectItem(data)}
3Home Page 3Home Page 3Home Page 3Home Page 3Home Page 3Home Page >
3Home Page 3Home Page 3Home Page 3Home Page 3Home Page 3Home Page <Flex direction="column" width="full" height="full">
3Home Page 3Home Page 3Home Page 3Home Page 3Home Page 3Home Page <Center height="full">{data.icon}</Center>
3Home Page 3Home Page 3 <Center>
</Text> <Text
<br /> fontSize="25px"
<Text>Home Page 4</Text> fontWeight="bold"
<br /> textTransform="uppercase"
<Text>Home Page 5</Text> userSelect="none"
<br /> >
<Text>Home Page 6</Text> {data.name}
<br />;<Text>Home Page 2</Text> </Text>
<br /> </Center>
<Text>Home Page 3</Text> </Flex>
<br /> </WrapItem>
<Text>Home Page 4</Text> ))}
<br /> </Wrap>
<Text>Home Page 5</Text>
<br />
<Text>Home Page 6</Text>
<br />;<Text>Home Page 2</Text>
<br />
<Text>Home Page 3</Text>
<br />
<Text>Home Page 4</Text>
<br />
<Text>Home Page 5</Text>
<br />
<Text>Home Page 6</Text>
<br />;<Text>Home Page 2</Text>
<br />
<Text>Home Page 3</Text>
<br />
<Text>Home Page 4</Text>
<br />
<Text>Home Page 5</Text>
<br />
<Text>Home Page 6</Text>
<br />;<Text>Home Page 2</Text>
<br />
<Text>Home Page 3</Text>
<br />
<Text>Home Page 4</Text>
<br />
<Text>Home Page 5</Text>
<br />
<Text>Home Page 6</Text>
<br />;<Text>Home Page 2</Text>
<br />
<Text>Home Page 3</Text>
<br />
<Text>Home Page 4</Text>
<br />
<Text>Home Page 5</Text>
<br />
<Text>Home Page 6</Text>
<br />;<Text>Home Page 2</Text>
<br />
<Text>Home Page 3</Text>
<br />
<Text>Home Page 4</Text>
<br />
<Text>Home Page 5</Text>
<br />
<Text>Home Page 6</Text>
<br />;<Text>Home Page 2</Text>
<br />
<Text>Home Page 3</Text>
<br />
<Text>Home Page 4</Text>
<br />
<Text>Home Page 5</Text>
<br />
<Text>Home Page 6</Text>
<br />;
</PageLayout> </PageLayout>
</> </>
); );

View File

@ -1,15 +0,0 @@
import { Text } from '@chakra-ui/react';
import { PageLayout } from '@/components/Layout/PageLayout';
import { TopBar } from '@/components/TopBar/TopBar';
export const SSOEmpty = () => {
return (
<>
<TopBar />
<PageLayout>
<Text>SSO empty page</Text>;
</PageLayout>
</>
);
};

View File

@ -1,14 +1,13 @@
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import { ArtistRoutes } from '@/scene/artist/ArtistRoutes'; import { Error404 } from '@/errors';
import { SSOEmpty } from '@/scene/sso/SSOEmpty';
import { SSOPage } from '@/scene/sso/SSOPage'; import { SSOPage } from '@/scene/sso/SSOPage';
export const SSORoutes = () => { export const SSORoutes = () => {
return ( return (
<Routes> <Routes>
<Route path=":data/:keepConnected/:token" element={<SSOPage />} /> <Route path=":data/:keepConnected/:token" element={<SSOPage />} />
<Route path="*" element={<SSOEmpty />} /> <Route path="*" element={<Error404 />} />
</Routes> </Routes>
); );
}; };

View File

@ -0,0 +1,125 @@
import { useCallback, useState } from 'react';
import { useServiceContext } from '@/service/ServiceContext';
export type PlaylistElement = {
trackId: number;
};
export type ActivePlaylistServiceProps = {
playTrackList: number[];
trackOffset?: number;
setNewPlaylist: (listIds: number[]) => void;
setNewPlaylistShuffle: (listIds: number[]) => void;
playInList: (id: number, listIds: number[]) => void;
play: (id: number) => void;
previous: () => void;
next: () => void;
};
export const useActivePlaylistService = (): ActivePlaylistServiceProps => {
const { activePlaylist } = useServiceContext();
return activePlaylist;
};
export function localShuffle<T>(array: T[]): T[] {
let currentIndex = array.length,
randomIndex;
// While there remain elements to shuffle.
while (currentIndex != 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex],
array[currentIndex],
];
}
return array;
}
export const useActivePlaylistServiceWrapped =
(): ActivePlaylistServiceProps => {
const [playTrackList, setPlayTrackList] = useState<number[]>([]);
const [trackOffset, setTrackOffset] = useState<number | undefined>();
const clear = useCallback(() => {
setPlayTrackList([]);
setTrackOffset(undefined);
}, [setPlayTrackList, setTrackOffset]);
const play = useCallback(
(id: number) => {
setPlayTrackList([id]);
setTrackOffset(0);
},
[setPlayTrackList, setTrackOffset]
);
const playInList = useCallback(
(id: number, listIds: number[]) => {
console.log(`Request paly in list: ${id} in ${listIds}`);
setPlayTrackList(listIds);
setTrackOffset(id);
},
[setPlayTrackList, setTrackOffset]
);
const setNewPlaylist = useCallback(
(listIds: number[]) => {
if (listIds.length == 0) {
clear();
return;
}
setPlayTrackList(listIds);
setTrackOffset(0);
},
[setPlayTrackList, setTrackOffset, clear]
);
const setNewPlaylistShuffle = useCallback(
(listIds: number[]) => {
if (listIds.length == 0) {
clear();
return;
}
const newList = localShuffle(listIds);
setPlayTrackList(newList);
setTrackOffset(0);
},
[setPlayTrackList, setTrackOffset, clear]
);
const previous = useCallback(() => {
setTrackOffset((previous) => {
if (previous === undefined) {
return previous;
}
if (previous === 0) {
return previous;
}
return previous - 1;
});
}, [setTrackOffset]);
const next = useCallback(() => {
setTrackOffset((previous) => {
if (previous === undefined || playTrackList.length === 0) {
return previous;
}
if (previous >= playTrackList.length - 1) {
return previous;
}
return previous + 1;
});
}, [playTrackList, setTrackOffset]);
return {
playTrackList,
trackOffset,
setNewPlaylist,
setNewPlaylistShuffle,
playInList,
play,
previous,
next,
};
};

View File

@ -0,0 +1,49 @@
import { useEffect, useState } from 'react';
import { Album, AlbumResource } from '@/back-api';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionServiceProps } from '@/service/session';
import { DataStoreType, useDataStore } from '@/utils/data-store';
export type AlbumServiceProps = {
store: DataStoreType<Album>;
};
export const useAlbumService = (): AlbumServiceProps => {
const { album } = useServiceContext();
return album;
};
export const useAlbumServiceWrapped = (
session: SessionServiceProps
): AlbumServiceProps => {
const store = useDataStore<Album>({
restApiName: 'ALBUM',
primaryKey: 'id',
getsCall: () => {
return AlbumResource.gets({
restConfig: session.getRestConfig(),
});
},
});
return { store };
};
export const useSpecificAlbum = (id: number | undefined) => {
const [dataAlbum, setDataAlbum] = useState<Album | undefined>(undefined);
const { store } = useAlbumService();
useEffect(() => {
setDataAlbum(store.get(id));
}, [store.data]);
return { dataAlbum };
};
export const useAlbumOfAnArtist = (idArtist: number | undefined) => {
const [dataAlbum, setDataAlbum] = useState<Album | undefined>(undefined);
const { store } = useAlbumService();
useEffect(() => {
setDataAlbum(store.get(idArtist));
}, [store.data]);
return { dataAlbum };
};

View File

@ -0,0 +1,40 @@
import { useEffect, useState } from 'react';
import { Artist, ArtistResource } from '@/back-api';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionServiceProps } from '@/service/session';
import { DataStoreType, useDataStore } from '@/utils/data-store';
export type ArtistServiceProps = {
store: DataStoreType<Artist>;
};
export const useArtistService = (): ArtistServiceProps => {
const { artist } = useServiceContext();
return artist;
};
export const useArtistServiceWrapped = (
session: SessionServiceProps
): ArtistServiceProps => {
const store = useDataStore<Artist>({
restApiName: 'ARTIST',
primaryKey: 'id',
getsCall: () => {
return ArtistResource.gets({
restConfig: session.getRestConfig(),
});
},
});
return { store };
};
export const useSpecificArtist = (id: number | undefined) => {
const [dataArtist, setDataArtist] = useState<Artist | undefined>(undefined);
const { store } = useArtistService();
useEffect(() => {
setDataArtist(store.get(id));
}, [store.data]);
return { dataArtist };
};

View File

@ -0,0 +1,101 @@
import { useDataStore } from '@/utils/data-store';
import { DataTools, TypeCheck } from '@/utils/data-tools';
import { isNullOrUndefined } from '@/utils/validator';
export class GenericDataService<TYPE> {
constructor(protected dataStore: DataStore<TYPE>) {}
gets(): Promise<TYPE[]> {
return this.dataStore.getData();
}
get(id: number): Promise<TYPE> {
let self = this;
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.get(response, id);
if (isNullOrUndefined(data)) {
reject('Data does not exist in the local BDD');
return;
}
resolve(data);
return;
})
.catch((response) => {
reject(response);
});
});
}
getAll(ids: number[]): Promise<TYPE[]> {
let self = this;
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.getsWhere(response, [
{
check: TypeCheck.EQUAL,
key: 'id',
value: ids,
},
]);
resolve(data);
return;
})
.catch((response) => {
reject(response);
});
});
}
getLike(value: string): Promise<TYPE[]> {
let self = this;
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.getNameLike(response, value);
if (isNullOrUndefined(data) || data.length === 0) {
reject('Data does not exist in the local BDD');
return;
}
resolve(data);
return;
})
.catch((response) => {
reject(response);
});
});
}
getOrder(): Promise<TYPE[]> {
let self = this;
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.getsWhere(
response,
[
{
check: TypeCheck.NOT_EQUAL,
key: 'id',
value: [undefined, null],
},
],
['name', 'id']
);
resolve(data);
})
.catch((response) => {
console.log(
`[E] ${self.constructor.name}: can not retrieve BDD values`
);
reject(response);
});
});
}
}

View File

@ -1,15 +1,26 @@
import { ReactNode, createContext, useContext, useMemo } from 'react'; import { ReactNode, createContext, useContext, useMemo } from 'react';
import {
ActivePlaylistServiceProps,
useActivePlaylistServiceWrapped,
} from '@/service/ActivePlaylist';
import { AlbumServiceProps, useAlbumServiceWrapped } from '@/service/Album';
import { ArtistServiceProps, useArtistServiceWrapped } from '@/service/Artist';
import { SessionState } from '@/service/SessionState'; import { SessionState } from '@/service/SessionState';
import { TrackServiceProps, useTrackServiceWrapped } from '@/service/Track';
import { import {
RightPart, RightPart,
SessionServiceProps, SessionServiceProps,
useSessionService, getRestConfig,
useSessionServiceWrapped, useSessionServiceWrapped,
} from '@/service/session'; } from '@/service/session';
export type ServiceContextType = { export type ServiceContextType = {
session: SessionServiceProps; session: SessionServiceProps;
track: TrackServiceProps;
artist: ArtistServiceProps;
album: AlbumServiceProps;
activePlaylist: ActivePlaylistServiceProps;
}; };
export const ServiceContext = createContext<ServiceContextType>({ export const ServiceContext = createContext<ServiceContextType>({
@ -19,6 +30,39 @@ export const ServiceContext = createContext<ServiceContextType>({
hasReadRight: (part: RightPart) => false, hasReadRight: (part: RightPart) => false,
hasWriteRight: (part: RightPart) => false, hasWriteRight: (part: RightPart) => false,
state: SessionState.NO_USER, state: SessionState.NO_USER,
getRestConfig: getRestConfig,
},
track: {
store: {
data: [],
isLoading: true,
get: (value, key) => undefined,
error: undefined,
},
},
artist: {
store: {
data: [],
isLoading: true,
get: (value, key) => undefined,
error: undefined,
},
},
album: {
store: {
data: [],
isLoading: true,
get: (value, key) => undefined,
error: undefined,
},
},
activePlaylist: {
playTrackList: [],
trackOffset: undefined,
setNewPlaylist: (listIds: number[]) => {},
setNewPlaylistShuffle: (listIds: number[]) => {},
playInList: (id: number, listIds: number[]) => {},
play: (id: number) => {},
}, },
}); });
@ -30,11 +74,19 @@ export const ServiceContextProvider = ({
children: ReactNode; children: ReactNode;
}) => { }) => {
const session = useSessionServiceWrapped(); const session = useSessionServiceWrapped();
const track = useTrackServiceWrapped(session);
const artist = useArtistServiceWrapped(session);
const album = useAlbumServiceWrapped(session);
const activePlaylist = useActivePlaylistServiceWrapped();
const contextObjectData = useMemo( const contextObjectData = useMemo(
() => ({ () => ({
session, session,
track,
artist,
album,
activePlaylist,
}), }),
[session] [session, track, artist, album]
); );
return ( return (
<ServiceContext.Provider value={contextObjectData}> <ServiceContext.Provider value={contextObjectData}>

180
front2/src/service/Track.ts Normal file
View File

@ -0,0 +1,180 @@
import { useCallback, useEffect, useState } from 'react';
import { Track, TrackResource } from '@/back-api';
import { useServiceContext } from '@/service/ServiceContext';
import { SessionServiceProps } from '@/service/session';
import { DataStoreType, useDataStore } from '@/utils/data-store';
import { DataTools, TypeCheck } from '@/utils/data-tools';
import { isArrayOf, isNumber } from '@/utils/validator';
export type TrackServiceProps = {
store: DataStoreType<Track>;
};
export const useTrackService = (): TrackServiceProps => {
const { track } = useServiceContext();
return track;
};
export const useTrackServiceWrapped = (
session: SessionServiceProps
): TrackServiceProps => {
const store = useDataStore<Track>({
restApiName: 'TRACK',
primaryKey: 'id',
getsCall: () => {
return TrackResource.gets({
restConfig: session.getRestConfig(),
});
},
});
return { store };
};
export const useSpecificTrack = (id: number | undefined) => {
const [dataTrack, setDataTrack] = useState<Track | undefined>(undefined);
const { store } = useTrackService();
useEffect(() => {
console.log(`retrieve specific track: ${id}`);
setDataTrack(store.get(id));
}, [store.data]);
const updateTrackId = useCallback(
(id: number | undefined) => {
console.log(`retrieve specific track (update): ${id}`);
setDataTrack(store.get(id));
},
[setDataTrack, store]
);
return { dataTrack, updateTrackId };
};
/**
* Get all the track for a specific artist
* @param idArtist - Id of the artist.
* @returns a promise on the list of track elements
*/
export const useTracksOfAnArtist = (idArtist?: number) => {
const [tracksOnAnArtist, setTracksOnAnArtist] = useState<Track[]>([]);
const { store } = useTrackService();
useEffect(() => {
if (idArtist) {
setTracksOnAnArtist(
DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.CONTAINS,
key: 'artists',
value: idArtist,
},
],
['track', 'name']
)
);
}
}, [store.data]);
return { tracksOnAnArtist };
};
/**
* Get the number of track in this artist ID
* @param id - Id of the artist.
* @returns The number of track present in this artist
*/
export const useCountTracksOfAnArtist = (idArtist: number) => {
const { tracksOnAnArtist } = useTracksOfAnArtist(idArtist);
const countTracksOnAnArtist = tracksOnAnArtist.length;
return { countTracksOnAnArtist };
};
/**
* Get all the album of a specific artist
* @param artistId - ID of the artist
* @returns the required List.
*/
export const useAlbumIdsOfAnArtist = (idArtist?: number) => {
const [albumIdsOfAnArtist, setAlbumIdsOfAnArtist] = useState<number[]>([]);
const { tracksOnAnArtist } = useTracksOfAnArtist(idArtist);
useEffect(() => {
// extract a single time all value "id" in an array
const listAlbumId = DataTools.extractLimitOne(tracksOnAnArtist, 'albumId');
if (isArrayOf(listAlbumId, isNumber)) {
setAlbumIdsOfAnArtist(listAlbumId);
} else {
console.log(
'Fail to parse the result of the list value (impossible case)'
);
setAlbumIdsOfAnArtist([]);
}
}, [tracksOnAnArtist]);
return { albumIdsOfAnArtist };
};
export const useTracksOfAnAlbum = (idAlbum?: number) => {
const [tracksOnAnAlbum, setTracksOnAnAlbum] = useState<Track[]>([]);
const { store } = useTrackService();
useEffect(() => {
if (idAlbum) {
setTracksOnAnAlbum(
DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.EQUAL,
key: 'albumId',
value: idAlbum,
},
],
['track', 'name', 'id']
)
);
}
}, [store.data]);
return { tracksOnAnAlbum };
};
/**
* Get the number of track in this season ID
* @param AlbumId - Id of the album.
* @returns The number of element present in this season
*/
export const useCountTracksWithAlbumId = (albumId: number) => {
const { tracksOnAnAlbum } = useTracksOfAnAlbum(albumId);
const countTracksOnAnArtist = tracksOnAnAlbum.length;
return { countTracksOnAnArtist };
};
/**
* Get all the track for a specific artist
* @param idArtist - Id of the artist.
* @returns a promise on the list of track elements
*/
export const useTracksOfArtistWithNoAlbum = (idArtist: number) => {
const [tracksOnAnArtistWithNoAlbum, setTracksOnAnArtistWithNoAlbum] =
useState<Track[]>([]);
const { store } = useTrackService();
useEffect(() => {
if (idArtist) {
setTracksOnAnArtistWithNoAlbum(
DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.CONTAINS,
key: 'artists',
value: idArtist,
},
{
check: TypeCheck.EQUAL,
key: 'albumId',
value: undefined,
},
],
['track', 'name']
)
);
}
}, [store.data]);
return { tracksOnAnArtistWithNoAlbum };
};

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { PartRight, RESTConfig, UserMe, UserResource } from '@/back-api'; import { PartRight, RESTConfig, UserMe, UserResource } from '@/back-api';
import { environment, getApiUrl } from '@/environment'; import { environment, getApiUrl } from '@/environment';
@ -35,12 +35,14 @@ export type SessionServiceProps = {
hasReadRight: (part: RightPart) => boolean; hasReadRight: (part: RightPart) => boolean;
hasWriteRight: (part: RightPart) => boolean; hasWriteRight: (part: RightPart) => boolean;
state: SessionState; state: SessionState;
getRestConfig: () => RESTConfig;
}; };
export const useSessionService = (): SessionServiceProps => { export const useSessionService = (): SessionServiceProps => {
const { session } = useServiceContext(); const { session } = useServiceContext();
return session; return session;
}; };
export const useSessionServiceWrapped = (): SessionServiceProps => { export const useSessionServiceWrapped = (): SessionServiceProps => {
const [token, setToken] = useState<string | undefined>( const [token, setToken] = useState<string | undefined>(
isBrowser ? (localStorage.getItem(TOKEN_KEY) ?? undefined) : undefined isBrowser ? (localStorage.getItem(TOKEN_KEY) ?? undefined) : undefined
@ -113,6 +115,17 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
[config] [config]
); );
const getRestConfig = useCallback((): RESTConfig => {
return {
server: getApiUrl(),
token: token ?? '',
};
}, [token]);
useEffect(() => {
updateRight();
}, [updateRight]);
return { return {
token, token,
setToken: setTokenLocal, setToken: setTokenLocal,
@ -121,5 +134,6 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
hasReadRight, hasReadRight,
hasWriteRight, hasWriteRight,
state, state,
getRestConfig,
}; };
}; };

View File

@ -7,6 +7,7 @@ export const styles: Styles = {
overflowY: 'none', overflowY: 'none',
bg: mode('back.50', 'back.700')(props), bg: mode('back.50', 'back.700')(props),
color: mode('text.900', 'text.50')(props), color: mode('text.900', 'text.50')(props),
fontFamily: 'Roboto, Helvetica, Arial, "sans-serif"',
}, },
}), }),
}; };

View File

@ -3,102 +3,78 @@
* @copyright 2024, Edouard DUPIN, all right reserved * @copyright 2024, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file) * @license PROPRIETARY (see license file)
*/ */
import { useCallback, useEffect, useState } from 'react';
import { RestErrorResponse } from '@/back-api';
import { isNullOrUndefined } from '@/utils/validator'; import { isNullOrUndefined } from '@/utils/validator';
export class DataStore<TYPE> { export type DataStoreType<TYPE> = {
private data?: TYPE[]; isLoading: boolean;
private dataPromise?: { error: RestErrorResponse | undefined;
resolve: (response: TYPE[]) => void; data: TYPE[];
reject: (error: Error) => void; get: <MODEL>(value: MODEL, key?: string) => TYPE | undefined;
}[]; };
constructor( export const useDataStore = <TYPE>({
private _gets: () => Promise<TYPE[]>, primaryKey = 'id',
private primaryKey: string = 'id' restApiName,
) {} getsCall,
}: {
restApiName?: string;
primaryKey?: string;
getsCall: () => Promise<TYPE[]>;
}): DataStoreType<TYPE> => {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<RestErrorResponse | undefined>(undefined);
const [data, setData] = useState<TYPE[]>([]);
public getData(): Promise<TYPE[]> { // on instantiation ==> call the request of the data...
let self = this; useEffect(() => {
if (!isNullOrUndefined(this.data)) { console.log(`[${restApiName}] call data ...`);
return new Promise((resolve, reject) => { setError(undefined);
resolve(self.data ?? []); setIsLoading(true);
getsCall()
.then((response: TYPE[]) => {
console.log(
`[${restApiName}] getData Response: ${JSON.stringify(response, null, 2)}`
);
setData(response);
setError(undefined);
setIsLoading(false);
})
.catch((error: RestErrorResponse) => {
console.log(
`[${restApiName}] catch error: ${JSON.stringify(error, null, 2)}`
);
setError(error);
setIsLoading(false);
}); });
} }, [setIsLoading, setData]);
if (isNullOrUndefined(this.dataPromise)) {
this.dataPromise = [];
return new Promise((resolve, reject) => {
self
._gets()
.then((response: TYPE[]) => {
self.data = response;
if (self.dataPromise) {
for (let iii = 0; iii < self.dataPromise.length; iii++) {
self.dataPromise[iii].resolve(self.data);
}
}
resolve(self.data);
})
.catch((error) => {
console.log(
`[E] ${self.constructor.name}: can not get data from remote server: ${JSON.stringify(error, null, 2)}`
);
if (self.dataPromise) {
for (let iii = 0; iii < self.dataPromise.length; iii++) {
self.dataPromise[iii].reject(error);
}
}
reject(error);
});
});
}
return new Promise((resolve, reject) => {
if (!isNullOrUndefined(self.data)) {
resolve(self.data);
return;
}
if (self.dataPromise) {
self.dataPromise.push({ resolve: resolve, reject: reject });
}
});
}
public get(id: any): Promise<TYPE> {
const self = this;
return new Promise((resolve, reject) => {
self
.getData()
.then((value: TYPE[]) => {
for (let iii = 0; iii < value.length; iii++) {
if (value[iii][this.primaryKey] == id) {
resolve(value[iii]);
return;
}
}
reject('Not FOUND');
})
.catch((error) => reject(error));
});
}
public updateValue(value: TYPE) { const get = useCallback(
console.log(`[E] Not implemented Updater`); <MODEL>(value: MODEL, key?: string): TYPE | undefined => {
if (this.data) { const keyValue = key ?? primaryKey;
for (let iii = 0; iii < this.data.length; iii++) { for (let iii = 0; iii < data.length; iii++) {
if (this.data[iii][this.primaryKey] == value[this.primaryKey]) { if (data[iii][keyValue] === value) {
this.data[iii] = value; return data[iii];
return;
} }
} }
this.data.push(value); return undefined;
} },
} [data]
);
public delete(value: any): void { const update = useCallback(
if (this.data) { (value: TYPE, key?: string) => {
for (let iii = 0; iii < this.data.length; iii++) { const keyValue = key ?? primaryKey;
if (this.data[iii][this.primaryKey] == value) { const filterData = data.filter(
this.data.splice(iii, 1); (localData: TYPE) => localData[keyValue] === value[keyValue]
} );
} filterData.push(value);
} setData(filterData);
} },
} [data, setData]
);
return { isLoading, error, data, get }; //, update};
};

View File

@ -18,7 +18,17 @@ export enum TypeCheck {
export interface SelectModel { export interface SelectModel {
check: TypeCheck; check: TypeCheck;
key: string; key: string;
value?: bigint | number | string | boolean | bigint[] | number[] | string[]; value:
| null
| undefined
| bigint
| number
| string
| boolean
| bigint[]
| number[]
| string[]
| (bigint | number | string | boolean | undefined | null)[];
} }
/* /*
{ check: TypeCheck.EQUAL, key: sss, value: null} { check: TypeCheck.EQUAL, key: sss, value: null}
@ -68,7 +78,7 @@ export namespace DataTools {
bdd: TYPE[], bdd: TYPE[],
select: SelectModel[], select: SelectModel[],
orderByData?: string[] orderByData?: string[]
): TYPE[] | undefined { ): TYPE[] {
// console.log("[I] gets_where " + this.name + " select " + _select); // console.log("[I] gets_where " + this.name + " select " + _select);
let tmpList = getSubList(bdd, select); let tmpList = getSubList(bdd, select);
if (tmpList && orderByData) { if (tmpList && orderByData) {
@ -180,7 +190,7 @@ export namespace DataTools {
export function getSubList<TYPE>( export function getSubList<TYPE>(
values: TYPE[], values: TYPE[],
select?: SelectModel[] select?: SelectModel[]
): undefined | TYPE[] { ): TYPE[] {
let out = [] as TYPE[]; let out = [] as TYPE[];
for (let iiiElem = 0; iiiElem < values.length; iiiElem++) { for (let iiiElem = 0; iiiElem < values.length; iiiElem++) {
let find = true; let find = true;
@ -216,7 +226,7 @@ export namespace DataTools {
console.log( console.log(
'[ERROR] Internal Server Error{ unknown comparing type ...' '[ERROR] Internal Server Error{ unknown comparing type ...'
); );
return undefined; return [];
} }
} else { } else {
//console.log(" [" + control.key + "] = " + valueElement); //console.log(" [" + control.key + "] = " + valueElement);
@ -262,7 +272,7 @@ export namespace DataTools {
console.log( console.log(
'[ERROR] Internal Server Error{ unknown comparing type ...' '[ERROR] Internal Server Error{ unknown comparing type ...'
); );
return undefined; return [];
} }
} }
} }

View File

@ -0,0 +1,106 @@
/** @file
* @author Edouard DUPIN
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { string } from 'zod';
import { RESTUrl } from '@/back-api';
import { environment } from '@/environment';
import { getRestConfig } from '@/service/session';
import { isArrayOf, isString } from '@/utils/validator';
export namespace DataUrlAccess {
/**
* Retrieve the Cover URL of a specific data id
* @param coverId Id of te cover
* @returns the url of the cover
*/
export const getUrl = (dataId: string, optionalName?: string): string => {
if (!optionalName) {
const url = RESTUrl({
restModel: {
endPoint: 'data/{dataId}',
tokenInUrl: true,
},
restConfig: getRestConfig(),
params: {
dataId,
},
});
console.log(`get URL = ${url}`);
if (environment?.replaceDataToRealServer === true) {
return url.replace('http://localhost:19080', 'https://atria-soft.org');
}
return url;
}
const url = RESTUrl({
restModel: {
endPoint: 'data/{dataId}/{optionalName}',
tokenInUrl: true,
},
restConfig: getRestConfig(),
params: {
dataId,
optionalName,
},
});
if (environment?.replaceDataToRealServer === true) {
return url.replace('http://localhost:19080', 'https://atria-soft.org');
}
return url;
};
/**
* Retrieve the list Cover URL of a specific data id
* @param coverId Id of te cover
* @returns the url of the cover
*/
export const getListUrl = (dataIds?: string[]): string[] | undefined => {
if (!isArrayOf(dataIds, isString) || dataIds.length === 0) {
return undefined;
}
let covers: string[] = [];
for (let iii = 0; iii < dataIds.length; iii++) {
covers.push(getUrl(dataIds[iii]));
}
return covers;
};
/**
* Retrieve the thumbnail cover URL of a specific data id
* @param coverId Id of te cover
* @returns the url of the cover
*/
export const getThumbnailUrl = (coverId: string): string => {
const url = RESTUrl({
restModel: {
endPoint: 'data/thumbnail/{coverId}',
tokenInUrl: true,
},
restConfig: getRestConfig(),
params: {
coverId,
},
});
if (environment?.replaceDataToRealServer === true) {
return url.replace('http://localhost:19080', 'https://atria-soft.org');
}
return url;
};
/**
* Retrieve the list thumbnail cover URL of a specific data id
* @param coverId Id of te cover
* @returns the url of the cover
*/
export const getListThumbnailUrl = (
dataIds?: string[]
): string[] | undefined => {
if (!isArrayOf(dataIds, isString) || dataIds.length === 0) {
return undefined;
}
let covers: string[] = [];
for (let iii = 0; iii < dataIds.length; iii++) {
covers.push(getThumbnailUrl(dataIds[iii]));
}
return covers;
};
}

10
front2/tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.mts"]
}