[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"
]
}
}
}

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>
<link rel="icon" href="/favicon.ico" />
</head>
<body className="flex flex-col">
<div id="root"></div>
<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>

View File

@ -38,9 +38,6 @@ importers:
'@emotion/styled':
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)
'@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:
specifier: 1.20.2
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'
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':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -3113,10 +3022,6 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@ -8677,89 +8582,6 @@ snapshots:
'@types/react': 18.3.3
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':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -10092,8 +9914,6 @@ snapshots:
clone@1.0.4: {}
clsx@2.1.1: {}
co@4.6.0: {}
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 { Flex } from '@chakra-ui/react';
import { Flex, Image } from '@chakra-ui/react';
import { useLocation } from 'react-router-dom';
import background from '@/assets/images/ikon.svg';
import { TOP_BAR_HEIGHT } from '@/components/TopBar/TopBar';
export type LayoutProps = React.PropsWithChildren<unknown> & {
@ -17,19 +18,33 @@ export const PageLayout = ({ children }: LayoutProps) => {
}, [pathname]);
return (
<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>
<>
<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}
zIndex={-1}
>
<Image src={background} boxSize="90%" margin="auto" opacity="30%" />
</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 { colors } from '@/theme/foundations/colors';
import { useThemeMode } from '@/utils/theme-tools';
export type LayoutProps = FlexProps & {
children: ReactNode;
@ -21,6 +22,7 @@ export const PageLayoutInfoCenter = ({
window.scrollTo(0, 0);
}, [pathname]);
const { mode } = useThemeMode();
return (
<PageLayout>
<Flex
@ -32,6 +34,7 @@ export const PageLayoutInfoCenter = ({
borderRadius="8px"
padding="10px"
boxShadow={'0px 0px 16px ' + colors.back[900]}
backgroundColor={mode('#FFFFFF', '#000000')}
{...rest}
>
{children}

View File

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

View File

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

View File

@ -5,12 +5,13 @@ import {
Routes,
} from 'react-router-dom';
import { AudioPlayer } from '@/components/AudioPlayer';
import { Error404 } from '@/errors';
import { ErrorBoundary } from '@/errors/ErrorBoundary';
import { ServiceContextProvider } from '@/service/ServiceContext';
import { ArtistRoutes } from '@/scene/artist/ArtistRoutes';
import { HomePage } from '@/scene/home/HomePage';
import { SSORoutes } from '@/scene/sso/SSORoutes';
import { ServiceContextProvider } from '@/service/ServiceContext';
export const App = () => {
return (
@ -28,6 +29,7 @@ export const App = () => {
</Routes>
</HistoryRouter>
</ErrorBoundary>
<AudioPlayer />
</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 { TopBar } from '@/components/TopBar/TopBar';
import { Error404 } from '@/errors';
import { ArtistAlbumDetailPage } from '@/scene/artist/ArtistAlbumDetailPage';
import { ArtistDetailPage } from '@/scene/artist/ArtistDetailPage';
import { ArtistsPage } from '@/scene/artist/ArtistsPage';
export const ArtistRoutes = () => {
return (
<>
<TopBar />
<PageLayout>
<Text>artist Page</Text>;
</PageLayout>
</>
<Routes>
<Route path="/" element={<Navigate to="all" replace />} />
<Route path="all" element={<ArtistsPage />} />
<Route path=":artistId" element={<ArtistDetailPage />} />
<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 { 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 = () => {
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (data: HomeListType) => {
navigate(data.to);
};
return (
<>
<TopBar />
<PageLayout>
<Text>Home Page 1</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 3Home 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 3Home 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 3Home Page 3Home Page 3Home Page 3Home Page
3Home Page 3Home 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 />;<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 />;
<Wrap spacing="20px" marginX="auto" padding="20px">
{homeList.map((data) => (
<WrapItem
width="200px"
height="190px"
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="column" width="full" height="full">
<Center height="full">{data.icon}</Center>
<Center>
<Text
fontSize="25px"
fontWeight="bold"
textTransform="uppercase"
userSelect="none"
>
{data.name}
</Text>
</Center>
</Flex>
</WrapItem>
))}
</Wrap>
</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 { ArtistRoutes } from '@/scene/artist/ArtistRoutes';
import { SSOEmpty } from '@/scene/sso/SSOEmpty';
import { Error404 } from '@/errors';
import { SSOPage } from '@/scene/sso/SSOPage';
export const SSORoutes = () => {
return (
<Routes>
<Route path=":data/:keepConnected/:token" element={<SSOPage />} />
<Route path="*" element={<SSOEmpty />} />
<Route path="*" element={<Error404 />} />
</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 {
ActivePlaylistServiceProps,
useActivePlaylistServiceWrapped,
} from '@/service/ActivePlaylist';
import { AlbumServiceProps, useAlbumServiceWrapped } from '@/service/Album';
import { ArtistServiceProps, useArtistServiceWrapped } from '@/service/Artist';
import { SessionState } from '@/service/SessionState';
import { TrackServiceProps, useTrackServiceWrapped } from '@/service/Track';
import {
RightPart,
SessionServiceProps,
useSessionService,
getRestConfig,
useSessionServiceWrapped,
} from '@/service/session';
export type ServiceContextType = {
session: SessionServiceProps;
track: TrackServiceProps;
artist: ArtistServiceProps;
album: AlbumServiceProps;
activePlaylist: ActivePlaylistServiceProps;
};
export const ServiceContext = createContext<ServiceContextType>({
@ -19,6 +30,39 @@ export const ServiceContext = createContext<ServiceContextType>({
hasReadRight: (part: RightPart) => false,
hasWriteRight: (part: RightPart) => false,
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;
}) => {
const session = useSessionServiceWrapped();
const track = useTrackServiceWrapped(session);
const artist = useArtistServiceWrapped(session);
const album = useAlbumServiceWrapped(session);
const activePlaylist = useActivePlaylistServiceWrapped();
const contextObjectData = useMemo(
() => ({
session,
track,
artist,
album,
activePlaylist,
}),
[session]
[session, track, artist, album]
);
return (
<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 { environment, getApiUrl } from '@/environment';
@ -35,12 +35,14 @@ export type SessionServiceProps = {
hasReadRight: (part: RightPart) => boolean;
hasWriteRight: (part: RightPart) => boolean;
state: SessionState;
getRestConfig: () => RESTConfig;
};
export const useSessionService = (): SessionServiceProps => {
const { session } = useServiceContext();
return session;
};
export const useSessionServiceWrapped = (): SessionServiceProps => {
const [token, setToken] = useState<string | undefined>(
isBrowser ? (localStorage.getItem(TOKEN_KEY) ?? undefined) : undefined
@ -113,6 +115,17 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
[config]
);
const getRestConfig = useCallback((): RESTConfig => {
return {
server: getApiUrl(),
token: token ?? '',
};
}, [token]);
useEffect(() => {
updateRight();
}, [updateRight]);
return {
token,
setToken: setTokenLocal,
@ -121,5 +134,6 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
hasReadRight,
hasWriteRight,
state,
getRestConfig,
};
};

View File

@ -7,6 +7,7 @@ export const styles: Styles = {
overflowY: 'none',
bg: mode('back.50', 'back.700')(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
* @license PROPRIETARY (see license file)
*/
import { useCallback, useEffect, useState } from 'react';
import { RestErrorResponse } from '@/back-api';
import { isNullOrUndefined } from '@/utils/validator';
export class DataStore<TYPE> {
private data?: TYPE[];
private dataPromise?: {
resolve: (response: TYPE[]) => void;
reject: (error: Error) => void;
}[];
export type DataStoreType<TYPE> = {
isLoading: boolean;
error: RestErrorResponse | undefined;
data: TYPE[];
get: <MODEL>(value: MODEL, key?: string) => TYPE | undefined;
};
constructor(
private _gets: () => Promise<TYPE[]>,
private primaryKey: string = 'id'
) {}
export const useDataStore = <TYPE>({
primaryKey = '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[]> {
let self = this;
if (!isNullOrUndefined(this.data)) {
return new Promise((resolve, reject) => {
resolve(self.data ?? []);
// on instantiation ==> call the request of the data...
useEffect(() => {
console.log(`[${restApiName}] call data ...`);
setError(undefined);
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);
});
}
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));
});
}
}, [setIsLoading, setData]);
public updateValue(value: TYPE) {
console.log(`[E] Not implemented Updater`);
if (this.data) {
for (let iii = 0; iii < this.data.length; iii++) {
if (this.data[iii][this.primaryKey] == value[this.primaryKey]) {
this.data[iii] = value;
return;
const get = useCallback(
<MODEL>(value: MODEL, key?: string): TYPE | undefined => {
const keyValue = key ?? primaryKey;
for (let iii = 0; iii < data.length; iii++) {
if (data[iii][keyValue] === value) {
return data[iii];
}
}
this.data.push(value);
}
}
return undefined;
},
[data]
);
public delete(value: any): void {
if (this.data) {
for (let iii = 0; iii < this.data.length; iii++) {
if (this.data[iii][this.primaryKey] == value) {
this.data.splice(iii, 1);
}
}
}
}
}
const update = useCallback(
(value: TYPE, key?: string) => {
const keyValue = key ?? primaryKey;
const filterData = data.filter(
(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 {
check: TypeCheck;
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}
@ -68,7 +78,7 @@ export namespace DataTools {
bdd: TYPE[],
select: SelectModel[],
orderByData?: string[]
): TYPE[] | undefined {
): TYPE[] {
// console.log("[I] gets_where " + this.name + " select " + _select);
let tmpList = getSubList(bdd, select);
if (tmpList && orderByData) {
@ -180,7 +190,7 @@ export namespace DataTools {
export function getSubList<TYPE>(
values: TYPE[],
select?: SelectModel[]
): undefined | TYPE[] {
): TYPE[] {
let out = [] as TYPE[];
for (let iiiElem = 0; iiiElem < values.length; iiiElem++) {
let find = true;
@ -216,7 +226,7 @@ export namespace DataTools {
console.log(
'[ERROR] Internal Server Error{ unknown comparing type ...'
);
return undefined;
return [];
}
} else {
//console.log(" [" + control.key + "] = " + valueElement);
@ -262,7 +272,7 @@ export namespace DataTools {
console.log(
'[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"]
}