[FEAT] so many things ....

This commit is contained in:
Edouard DUPIN 2024-08-29 19:52:15 +02:00
parent 53c3cfefe3
commit d49e1d06fe
40 changed files with 1737 additions and 517 deletions

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 24 24"
height="250px"
width="250px"
version="1.1"
id="svg2"
sodipodi:docname="404.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="true"
inkscape:zoom="3.448"
inkscape:cx="134.28074"
inkscape:cy="125"
inkscape:window-width="1918"
inkscape:window-height="1044"
inkscape:window-x="0"
inkscape:window-y="17"
inkscape:window-maximized="1"
inkscape:current-layer="svg2">
<inkscape:grid
id="grid2"
units="px"
originx="0"
originy="0"
spacingx="0.096"
spacingy="0.096"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="true" />
</sodipodi:namedview>
<path
fill="none"
d="M0 0h24v24H0z"
id="path1" />
<path
d="M13 10h5l3-3-3-3h-5V2h-2v2H4v6h7v2H6l-3 3 3 3h5v4h2v-4h7v-6h-7z"
id="path2" />
<path
id="rect2"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.384;stroke-linecap:square"
d="M 17.394219,5.0400499 19.459554,6.9903325 17.438107,9.0722051 4.9259029,9.0569946 4.9284452,5.0338374 Z"
sodipodi:nodetypes="cccccc" />
<path
id="rect2-3"
style="fill:#f8fefb;fill-opacity:1;stroke:none;stroke-width:0.384;stroke-linecap:square"
d="m 6.5757719,13.021525 -2.065335,1.950283 2.021447,2.081873 12.5122061,-0.01521 -0.0025,-4.023157 z"
sodipodi:nodetypes="cccccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,61 +1,62 @@
/**
* Interface of the server (auto-generated code)
*/
import { z as zod } from "zod";
import { z as zod } from 'zod';
import {ZodUUID} from "./uuid";
import {ZodLong} from "./long";
import {ZodGenericDataSoftDelete, ZodGenericDataSoftDeleteWrite } from "./generic-data-soft-delete";
import {
ZodGenericDataSoftDelete,
ZodGenericDataSoftDeleteWrite,
} from './generic-data-soft-delete';
import { ZodLong } from './long';
import { ZodUUID } from './uuid';
export const ZodTrack = ZodGenericDataSoftDelete.extend({
name: zod.string().max(256).optional(),
description: zod.string().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).optional(),
genderId: ZodLong.optional(),
albumId: ZodLong.optional(),
track: ZodLong.optional(),
dataId: ZodUUID.optional(),
artists: zod.array(ZodLong),
name: zod.string().max(256).optional(),
description: zod.string().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).optional(),
genderId: ZodLong.optional(),
albumId: ZodLong.optional(),
track: ZodLong.optional(),
dataId: ZodUUID.optional(),
artists: zod.array(ZodLong),
});
export type Track = zod.infer<typeof ZodTrack>;
export function isTrack(data: any): data is Track {
try {
ZodTrack.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodTrack' error=${e}`);
return false;
}
try {
ZodTrack.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodTrack' error=${e}`);
return false;
}
}
export const ZodTrackWrite = ZodGenericDataSoftDeleteWrite.extend({
name: zod.string().max(256).nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
genderId: ZodLong.nullable().optional(),
albumId: ZodLong.nullable().optional(),
track: ZodLong.nullable().optional(),
dataId: ZodUUID.nullable().optional(),
artists: zod.array(ZodLong).optional(),
name: zod.string().max(256).nullable().optional(),
description: zod.string().nullable().optional(),
/**
* List of Id of the specific covers
*/
covers: zod.array(ZodUUID).nullable().optional(),
genderId: ZodLong.nullable().optional(),
albumId: ZodLong.nullable().optional(),
track: ZodLong.nullable().optional(),
dataId: ZodUUID.nullable().optional(),
artists: zod.array(ZodLong).optional(),
});
export type TrackWrite = zod.infer<typeof ZodTrackWrite>;
export function isTrackWrite(data: any): data is TrackWrite {
try {
ZodTrackWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodTrackWrite' error=${e}`);
return false;
}
try {
ZodTrackWrite.parse(data);
return true;
} catch (e: any) {
console.log(`Fail to parse data type='ZodTrackWrite' error=${e}`);
return false;
}
}

View File

@ -30,20 +30,20 @@ import {
} from 'react-icons/md';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificAlbum } from '@/service/Album';
import { useSpecificArtists } from '@/service/Artist';
import { useSpecificGender } from '@/service/Gender';
import { useSpecificTrack } from '@/service/Track';
import { DataUrlAccess } from '@/utils/data-url-access';
import { useThemeMode } from '@/utils/theme-tools';
import { isNullOrUndefined } from '@/utils/validator';
export enum PlayMode {
PLAY_ONE,
PLAY_ALL,
PLAY_ONE_LOOP,
PLAY_ALL_LOOP,
};
}
const playModeIcon = {
[PlayMode.PLAY_ONE]: <MdLooksOne size="30px" />,
@ -65,7 +65,7 @@ const formatTime = (time) => {
return '00:00';
};
export const AudioPlayer = ({ }: AudioPlayerProps) => {
export const AudioPlayer = ({}: AudioPlayerProps) => {
const { mode } = useThemeMode();
const { playTrackList, trackOffset, previous, next, first } =
useActivePlaylistService();
@ -77,6 +77,10 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
const { dataTrack } = useSpecificTrack(
trackOffset !== undefined ? playTrackList[trackOffset] : undefined
);
const { dataAlbum } = useSpecificAlbum(dataTrack?.albumId);
const { dataGender } = useSpecificGender(dataTrack?.genderId);
const { dataArtists } = useSpecificArtists(dataTrack?.artists);
const [mediaSource, setMediaSource] = useState<string>('');
useEffect(() => {
setMediaSource(
@ -180,7 +184,7 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
} else {
return PlayMode.PLAY_ONE;
}
})
});
};
/**
* Call when meta-data is updated
@ -198,7 +202,7 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
console.log(`onTimeUpdate ${audioRef.current.currentTime}`);
setTimeProgress(audioRef.current.currentTime);
};
const onDurationChange = (event) => { };
const onDurationChange = (event) => {};
const onChangeStateToPlay = () => {
setIsPlaying(true);
};
@ -207,124 +211,134 @@ export const AudioPlayer = ({ }: AudioPlayerProps) => {
};
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}
{!isNullOrUndefined(trackOffset) && (
<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"
>
{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}
<Text
align="left"
fontSize="20px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
<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>
{dataTrack?.name ?? '???'}
</Text>
<Text
align="left"
fontSize="16px"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
{formatTime(timeProgress)}
</Text>
<Text align="left" fontSize="16px" userSelect="none">
{formatTime(duration)}
{dataArtists.map((data) => data.name).join(', ')} /{' '}
{dataAlbum && dataAlbum?.name}
{dataGender && ` / ${dataGender.name}`}
</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"
overflow="hidden"
noOfLines={1}
>
{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={playModeIcon[playingMode]}
onClick={onTypePlay}
/>
</Flex>
</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={playModeIcon[playingMode]}
onClick={onTypePlay}
/>
</Flex>
</Flex>
)}
<audio
src={mediaSource}

View File

@ -36,5 +36,5 @@ export const Covers = ({
}
}
const url = DataUrlAccess.getThumbnailUrl(data[0]);
return <Image src={url} boxSize={size} {...rest} />;
return <Image loading="lazy" src={url} boxSize={size} {...rest} />;
};

View File

@ -0,0 +1,67 @@
import { useState } from 'react';
import React from 'react';
import {
Input,
InputGroup,
InputLeftElement,
useOutsideClick,
} from '@chakra-ui/react';
import { MdSearch } from 'react-icons/md';
export type SearchInputProps = {
onChange?: (data?: string) => void;
onSubmit?: (data?: string) => void;
};
export const SearchInput = ({
onChange: onChangeValue,
onSubmit: onSubmitValue,
}: SearchInputProps) => {
const [inputData, setInputData] = useState<string | undefined>(undefined);
const [searchInputProperty, setSearchInputProperty] =
useState<any>(undefined);
function onFocusKeep(): void {
setSearchInputProperty({
width: '70%',
maxWidth: '70%',
});
}
function onFocusLost(): void {
setSearchInputProperty({
width: '250px',
});
}
const ref = React.useRef(null);
// TODO: find a better way...
useOutsideClick({
ref: ref,
handler: onFocusLost,
});
function onChange(event): void {
const data =
event.target.value.length === 0 ? undefined : event.target.value;
setInputData(data);
if (onChangeValue) {
onChangeValue(data);
}
}
function onSubmit(): void {
if (onSubmitValue) {
onSubmitValue(inputData);
}
}
return (
<InputGroup maxWidth="200px" marginLeft="auto" {...searchInputProperty}>
<InputLeftElement pointerEvents="none">
<MdSearch color="gray.300" />
</InputLeftElement>
<Input
ref={ref}
onFocus={onFocusKeep}
onChange={onChange}
onSubmit={onSubmit}
/>
</InputGroup>
);
};

View File

@ -20,7 +20,6 @@ import {
import {
LuAlignJustify,
LuArrowBigLeft,
LuArrowRightSquare,
LuArrowUpSquare,
LuHelpCircle,
LuHome,
@ -44,9 +43,10 @@ export const TOP_BAR_HEIGHT = '50px';
export type TopBarProps = {
children?: ReactNode;
title?: string;
};
export const TopBar = ({ children }: TopBarProps) => {
export const TopBar = ({ title, children }: TopBarProps) => {
const { mode, colorMode, toggleColorMode } = useThemeMode();
const buttonProperty = {
variant: '@menu',
@ -86,69 +86,73 @@ export const TopBar = ({ children }: TopBarProps) => {
boxShadow={'0px 2px 4px ' + colors.back[900]}
zIndex={200}
>
<Button {...buttonProperty} onClick={onChangeTheme} marginRight="auto">
<Button {...buttonProperty} onClick={onChangeTheme}>
<LuAlignJustify />
<Text paddingLeft="3px" fontWeight="bold">
Menu
Karusic
</Text>
</Button>
{title && (
<Text
fontSize="20px"
fontWeight="bold"
textTransform="uppercase"
marginRight="auto"
userSelect="none"
>
{title}
</Text>
)}
{children}
<Text
fontSize="25px"
fontWeight="bold"
textTransform="uppercase"
marginRight="auto"
userSelect="none"
>
Karusic
</Text>
{session?.state !== SessionState.CONNECTED && (
<>
<Button {...buttonProperty} onClick={onSignIn}>
<LuLogIn />
<Text paddingLeft="3px" fontWeight="bold">
Sign-in
</Text>
</Button>
<Button {...buttonProperty} onClick={onSignUp} disabled={true}>
<LuPlusCircle />
<Text paddingLeft="3px" fontWeight="bold">
Sign-up
</Text>
</Button>
</>
)}
{session?.state === SessionState.CONNECTED && (
<Menu>
<MenuButton
as={IconButton}
aria-label="Options"
icon={<LuUserCircle />}
{...buttonProperty}
width={TOP_BAR_HEIGHT}
/>
<MenuList>
<MenuItem _hover={{}} color={mode('brand.800', 'brand.200')}>
Sign in as {session?.login ?? 'Fail'}
</MenuItem>
<MenuItem icon={<LuArrowUpSquare />}>Add Media</MenuItem>
<MenuItem icon={<LuSettings />}>Settings</MenuItem>
<MenuItem icon={<LuHelpCircle />}>Help</MenuItem>
<MenuItem icon={<LuLogOut onClick={onSignOut} />}>
Sign-out
</MenuItem>
{colorMode === 'light' ? (
<MenuItem icon={<LuMoon />} onClick={toggleColorMode}>
Set dark mode
<Flex right="0">
{session?.state !== SessionState.CONNECTED && (
<>
<Button {...buttonProperty} onClick={onSignIn}>
<LuLogIn />
<Text paddingLeft="3px" fontWeight="bold">
Sign-in
</Text>
</Button>
<Button {...buttonProperty} onClick={onSignUp} disabled={true}>
<LuPlusCircle />
<Text paddingLeft="3px" fontWeight="bold">
Sign-up
</Text>
</Button>
</>
)}
{session?.state === SessionState.CONNECTED && (
<Menu>
<MenuButton
as={IconButton}
aria-label="Options"
icon={<LuUserCircle />}
{...buttonProperty}
width={TOP_BAR_HEIGHT}
/>
<MenuList>
<MenuItem _hover={{}} color={mode('brand.800', 'brand.200')}>
Sign in as {session?.login ?? 'Fail'}
</MenuItem>
) : (
<MenuItem icon={<LuSun />} onClick={toggleColorMode}>
Set light mode
<MenuItem icon={<LuArrowUpSquare />}>Add Media</MenuItem>
<MenuItem icon={<LuSettings />}>Settings</MenuItem>
<MenuItem icon={<LuHelpCircle />}>Help</MenuItem>
<MenuItem icon={<LuLogOut onClick={onSignOut} />}>
Sign-out
</MenuItem>
)}
</MenuList>
</Menu>
)}
{colorMode === 'light' ? (
<MenuItem icon={<LuMoon />} onClick={toggleColorMode}>
Set dark mode
</MenuItem>
) : (
<MenuItem icon={<LuSun />} onClick={toggleColorMode}>
Set light mode
</MenuItem>
)}
</MenuList>
</Menu>
)}
</Flex>
<Drawer
placement="left"
onClose={drawerDisclose.onClose}

View File

@ -0,0 +1,62 @@
import { Flex, Text } from '@chakra-ui/react';
import { LuDisc3 } from 'react-icons/lu';
import { Gender } from '@/back-api';
import { Covers } from '@/components/Cover';
import { useCountTracksOfAGender } from '@/service/Track';
export type DisplayGenderProps = {
dataGender?: Gender;
};
export const DisplayGender = ({ dataGender }: DisplayGenderProps) => {
const { countTracksOnAGender } = useCountTracksOfAGender(dataGender?.id);
if (!dataGender) {
return (
<Flex direction="row" width="full" height="full">
Fail to retrieve Gender Data.
</Flex>
);
}
return (
<Flex direction="row" width="full" height="full">
<Covers
data={dataGender?.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]}
>
{dataGender?.name}
</Text>
<Text
as="span"
align="left"
fontSize="15px"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
>
{countTracksOnAGender} track{countTracksOnAGender >= 1 && 's'}
</Text>
</Flex>
</Flex>
);
};

View File

@ -0,0 +1,10 @@
import { DisplayGender } from '@/components/gender/DisplayGender';
import { useSpecificGender } from '@/service/Gender';
export type DisplayGenderIdProps = {
id: number;
};
export const DisplayGenderId = ({ id }: DisplayGenderIdProps) => {
const { dataGender } = useSpecificGender(id);
return <DisplayGender dataGender={dataGender} />;
};

View File

@ -1,8 +1,11 @@
import { Suspense } from 'react';
import { Flex, Text } from '@chakra-ui/react';
import { LuMusic2, LuPlay } from 'react-icons/lu';
import { Track } from '@/back-api';
import { Covers } from '@/components/Cover';
import { DisplayTrackSkeleton } from '@/components/track/DisplayTrackSkeleton';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
export type DisplayTrackProps = {
@ -10,14 +13,19 @@ export type DisplayTrackProps = {
};
export const DisplayTrack = ({ track }: DisplayTrackProps) => {
const { trackActive } = useActivePlaylistService();
console.log(`Chnage : ${trackActive?.id} == ${track.id}`);
return (
<Flex direction="row" width="full" height="full">
<Covers
data={track?.covers}
size="50"
height="full"
iconEmpty={trackActive?.id === track.id ? <LuPlay size="50" height="full" /> : <LuMusic2 size="50" height="full" />}
iconEmpty={
trackActive?.id === track.id ? (
<LuPlay size="50" height="full" />
) : (
<LuMusic2 size="50" height="full" />
)
}
/>
<Flex
direction="column"
@ -36,7 +44,7 @@ export const DisplayTrack = ({ track }: DisplayTrackProps) => {
overflow="hidden"
noOfLines={[1, 2]}
marginY="auto"
color={trackActive?.id === track.id ? "green.700" : undefined}
color={trackActive?.id === track.id ? 'green.700' : undefined}
>
[{track.track}] {track.name}
</Text>

View File

@ -0,0 +1,110 @@
import { Suspense } from 'react';
import { Flex, Text } from '@chakra-ui/react';
import { LuMusic2, LuPlay } from 'react-icons/lu';
import { Track } from '@/back-api';
import { Covers } from '@/components/Cover';
import { DisplayTrackSkeleton } from '@/components/track/DisplayTrackSkeleton';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificAlbum } from '@/service/Album';
import { useSpecificArtists } from '@/service/Artist';
import { useSpecificGender } from '@/service/Gender';
export type DisplayTrackProps = {
track: Track;
};
export const DisplayTrackFull = ({ track }: DisplayTrackProps) => {
const { trackActive } = useActivePlaylistService();
const { dataAlbum } = useSpecificAlbum(track?.albumId);
const { dataGender } = useSpecificGender(track?.genderId);
const { dataArtists } = useSpecificArtists(track?.artists);
return (
<Suspense fallback={<DisplayTrackSkeleton />}>
<Flex direction="row" width="full" height="full">
<Covers
data={track?.covers}
size="50"
//height="full"
marginY="auto"
iconEmpty={
trackActive?.id === track.id ? (
<LuPlay size="50" />
) : (
<LuMusic2 size="50" />
)
}
/>
<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}
color={trackActive?.id === track.id ? 'green.700' : undefined}
>
{track.name} {track.track && ` [${track.track}]`}
</Text>
{dataAlbum && (
<Text
as="span"
align="left"
fontSize="15px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
marginY="auto"
color={trackActive?.id === track.id ? 'green.700' : undefined}
>
Album {dataAlbum.name}
</Text>
)}
{dataArtists && (
<Text
as="span"
align="left"
fontSize="15px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
marginY="auto"
color={trackActive?.id === track.id ? 'green.700' : undefined}
>
Artist(s): {dataArtists.map((data) => data.name).join(', ')}
</Text>
)}
{dataGender && (
<Text
as="span"
align="left"
fontSize="15px"
fontWeight="bold"
userSelect="none"
marginRight="auto"
overflow="hidden"
noOfLines={1}
marginY="auto"
color={trackActive?.id === track.id ? 'green.700' : undefined}
>
Gender: {dataGender.name}
</Text>
)}
</Flex>
</Flex>
</Suspense>
);
};

View File

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

View File

@ -0,0 +1,27 @@
import { Box, Button, Center, Heading, Text } from '@chakra-ui/react';
import { MdControlCamera } from 'react-icons/md';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
export const Error401 = () => {
return (
<>
<TopBar />
<PageLayoutInfoCenter padding="25px">
<Center>
<MdControlCamera size="250px" color="red.600" />
</Center>
<Box textAlign="center">
<Heading>Erreur 401</Heading>
<Text color="red.600">
Vous n'êtes pas autorisé a accéder a ce contenu.
</Text>
<Button as="a" variant="link" href="/">
Retour à l'accueil
</Button>
</Box>
</PageLayoutInfoCenter>
</>
);
};

View File

@ -0,0 +1,25 @@
import { Box, Button, Center, Heading, Text } from '@chakra-ui/react';
import { MdDangerous } from 'react-icons/md';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
export const Error403 = () => {
return (
<>
<TopBar />
<PageLayoutInfoCenter padding="25px">
<Center>
<MdDangerous size="250px" color="orange.600" />
</Center>
<Box textAlign="center">
<Heading>Erreur 401</Heading>
<Text color="orange.600">Cette page vous est interdite</Text>
<Button as="a" variant="link" href="/">
Retour à l'accueil
</Button>
</Box>
</PageLayoutInfoCenter>
</>
);
};

View File

@ -1,128 +1,23 @@
import {
Box,
Button,
Center,
Heading,
Stack,
Text,
useTheme,
} from '@chakra-ui/react';
import { Box, Button, Center, Heading, Text } from '@chakra-ui/react';
import { MdSignpost } from 'react-icons/md';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { environment } from '@/environment';
const Illustration = ({ colorScheme = 'gray', ...rest }) => {
const theme = useTheme();
const color = theme?.colors?.[colorScheme] ?? {};
return (
<Box
as="svg"
width={400}
height={300}
maxW="full"
viewBox="0 0 400 300"
fill="none"
{...rest}
>
<path
// Left Hand
d="M65.013 104.416s-12.773-.562-13.719 11.938c-.946 12.5 16.13 8.397 13.719-11.938z"
fill={color['300']}
/>
<path
// Left Arm
d="M182.326 67.705s-35.463-20.529-67.804-13.535c-32.342 6.993-60.624 52.94-60.624 52.94l11.499 6.837s49.74-51.775 83.275-21.444c33.535 30.331 33.654-24.798 33.654-24.798z"
fill={color['800']}
/>
<path
// Search Zone
d="M334.098 220.092a14.333 14.333 0 01-9.838-7.37v-.106l-50.465-96.17-27.774 19.677 19.642 61.796a10.575 10.575 0 01-5.617 12.799 10.563 10.563 0 01-4.945.97 1037.507 1037.507 0 00-47.278-1.067c-85.178 0-154.23 9.93-154.23 22.184 0 12.255 69.052 22.195 154.23 22.195C293.001 255 362 245.07 362 232.837c0-4.756-10.328-9.14-27.902-12.745z"
fill={color['200']}
/>
<path
// Foots
d="M173.611 225.563s1.578 5.333 6.256 5.962c4.679.63 5.66 5.333 1.365 6.293-4.296.96-14.921-5.066-14.921-5.066l.671-6.773 6.629-.416zM82.518 224.657s-5.414 1.173-6.395 5.791c-.98 4.618-5.734 5.237-6.395.875-.66-4.362 6.193-14.484 6.193-14.484l6.693 1.205-.096 6.613z"
fill={color['900']}
/>
<path
// Left Leg
d="M83.5 143s-5.245 25.322 12.713 35.305c17.959 9.983 74.606-7.988 65.856 48.592h12.64s16.338-46.928-26.048-63.609l-12.864-14.601L83.5 143z"
fill={color['600']}
/>
<path
// Magnifying Glass Shadow
d="M257.632 128.216l-4.299-4.112-26.891 28.16 4.299 4.111 26.891-28.159z"
fill={color['700']}
/>
<path
// Magnifying Glass Handle
d="M255.537 126.2l-4.299-4.112-26.891 28.16 4.299 4.111 26.891-28.159z"
fill={color['500']}
/>
<path
// Magnifying Glass Shadow 2
d="M267.233 131.381c6.913-7.239 8.606-16.849 3.78-21.464-4.826-4.615-14.342-2.487-21.256 4.752-6.913 7.24-8.605 16.85-3.779 21.465 4.825 4.615 14.342 2.487 21.255-4.753z"
fill={color['700']}
/>
<path
// Magnifying Glass Ring
d="M265.133 129.382c6.914-7.24 8.606-16.849 3.78-21.464-4.825-4.615-14.342-2.487-21.255 4.752-6.913 7.24-8.606 16.849-3.78 21.464 4.826 4.615 14.342 2.487 21.255-4.752z"
fill={color['500']}
/>
<path
// Magnifying Glass
d="M262.167 126.545c4.566-4.782 5.685-11.13 2.497-14.178-3.187-3.048-9.473-1.642-14.04 3.14-4.567 4.782-5.685 11.13-2.498 14.178 3.188 3.048 9.474 1.642 14.041-3.14z"
fill={color['50']}
/>
<path
// Head
d="M217.261 74.257c1.932-2.325 3.256-4.248 3.256-4.248 3.133-4.106-.267-11.743-6.096-12.084a7.606 7.606 0 00-7.759 4.095l-.966 2.572-19.039 7.678 2.664 15.443 14.063-11.483c.418 1.245 1.052 2.35 1.7 3.26a4.269 4.269 0 004.448 1.638 4.267 4.267 0 001.51-.7 25.197 25.197 0 002.341-1.98l1.613.893a1.364 1.364 0 002.014-1.067l.256-4.02-.005.003z"
fill={color['300']}
/>
<path
// Body
d="M192.199 93.056l-1.791-10.42a25.45 25.45 0 00-15.251-19.325 45.122 45.122 0 00-10.754-2.827c-4.732-.66-11.361 0-18.779 1.952-31.953 8.394-55.4 35.484-60.25 68.141L83 145l52.797 3.687s-2.664-14.931 15.987-14.931 42.269.192 40.415-40.7z"
fill={color['700']}
/>
<path
// Right Arm
d="M169.945 95.488c4.977-16.617-14.324-30.055-28.084-19.496-11.51 8.82-24.513 24.701-25.024 51.503-.885 48.368 45.413 39.718 108.071 21.192l-1.993-14.089s-75.032 14.196-64.843-10.207c4.263-10.206 9.23-20.061 11.873-28.903z"
fill={color['800']}
/>
<path
// Right Leg
d="M143.609 163.406s1.545 53.487-60.995 63.491l-2.42-11.338s50.092-12.425 17.735-45.712l45.68-6.441z"
fill={color['600']}
/>
307s17
<path
// Right Hand
d="M223.298 137.307s17.244-1.824 18.758 4.917c1.513 6.741-1.791 10.313-17.309 5.333l-1.449-10.25z"
fill={color['300']}
/>
<path
// Hair
d="M218.673 68.942a34.11 34.11 0 01-5.649-5.44 7.094 7.094 0 01-4.934 5.994 5.752 5.752 0 01-6.821-2.655l7.034-8.319a8.644 8.644 0 018.27-3.2c1.33.23 2.643.543 3.933.939 3.197 1.066 5.425 5.567 8.942 5.717a2.044 2.044 0 011.946 2.1c-.01.354-.111.7-.294 1.003-1.727 2.87-5.499 6.517-10.114 5.056a7.933 7.933 0 01-2.313-1.195z"
fill={color['800']}
/>
</Box>
);
};
export const Error404 = () => {
return (
<>
<TopBar />
<PageLayoutInfoCenter>
<Illustration />
<Box textAlign={{ base: 'center', md: 'left' }}>
<PageLayoutInfoCenter padding="25px">
<Center>
<MdSignpost size="250px" />
</Center>
<Box textAlign="center">
<Heading>Erreur 404</Heading>
<Text color="gray.600">
Cette page n'existe plus ou l'URL a changé
</Text>
<Button as="a" variant="link" href={`/${environment.applName}`}>
<Button as="a" variant="link" href="/">
Retour à l'accueil
</Button>
</Box>

View File

@ -1 +1,4 @@
export * from './Error401';
export * from './Error403';
export * from './Error404';
export * from './ErrorBoundary';

View File

@ -1,35 +1,13 @@
import { createBrowserHistory } from 'history';
import {
unstable_HistoryRouter as HistoryRouter,
Route,
Routes,
} from 'react-router-dom';
import { AudioPlayer } from '@/components/AudioPlayer';
import { Error404 } from '@/errors';
import { ErrorBoundary } from '@/errors/ErrorBoundary';
import { AlbumRoutes } from '@/scene/album/AlbumRoutes';
import { ArtistRoutes } from '@/scene/artist/ArtistRoutes';
import { HomePage } from '@/scene/home/HomePage';
import { SSORoutes } from '@/scene/sso/SSORoutes';
import { AppRoutes } from '@/scene/AppRoutes';
import { ServiceContextProvider } from '@/service/ServiceContext';
export const App = () => {
return (
<ServiceContextProvider>
<ErrorBoundary>
<HistoryRouter
history={createBrowserHistory({ window })}
basename="/karusic"
>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="artist/*" element={<ArtistRoutes />} />
<Route path="album/*" element={<AlbumRoutes />} />
<Route path="sso/*" element={<SSORoutes />} />
<Route path="*" element={<Error404 />} />
</Routes>
</HistoryRouter>
<AppRoutes />
</ErrorBoundary>
<AudioPlayer />
</ServiceContextProvider>

View File

@ -0,0 +1,45 @@
import { createBrowserHistory } from 'history';
import {
unstable_HistoryRouter as HistoryRouter,
Route,
Routes,
} from 'react-router-dom';
import { Error401, Error404 } from '@/errors';
import { AlbumRoutes } from '@/scene/album/AlbumRoutes';
import { ArtistRoutes } from '@/scene/artist/ArtistRoutes';
import { GenderRoutes } from '@/scene/gender/GenderRoutes';
import { HelpPage } from '@/scene/home/Help';
import { HomePage } from '@/scene/home/HomePage';
import { SSORoutes } from '@/scene/sso/SSORoutes';
import { TrackRoutes } from '@/scene/track/TrackRoutes';
import { useHasRight } from '@/service/session';
export const AppRoutes = () => {
const { isReadable } = useHasRight('user');
return (
<HistoryRouter
// @ts-expect-error
history={createBrowserHistory({ window })}
basename="/karusic"
>
<Routes>
{/* Need to keep it in all case, it is the only way to log-in */}
<Route path="sso/*" element={<SSORoutes />} />
{isReadable ? (
<>
<Route path="/" element={<HomePage />} />
<Route path="/help" element={<HelpPage />} />
<Route path="artist/*" element={<ArtistRoutes />} />
<Route path="album/*" element={<AlbumRoutes />} />
<Route path="gender/*" element={<GenderRoutes />} />
<Route path="track/*" element={<TrackRoutes />} />
<Route path="*" element={<Error404 />} />
</>
) : (
<Route path="*" element={<Error401 />} />
)}
</Routes>
</HistoryRouter>
);
};

View File

@ -3,11 +3,11 @@ import { LuDisc3 } from 'react-icons/lu';
import { useParams } from 'react-router-dom';
import { Covers } from '@/components/Cover';
import { DisplayTrack } from '@/components/DisplayTrack';
import { EmptyEnd } from '@/components/EmptyEnd';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { DisplayTrack } from '@/components/track/DisplayTrack';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificAlbum } from '@/service/Album';
import { useTracksOfAnAlbum } from '@/service/Track';
@ -25,7 +25,7 @@ export const AlbumDetailPage = () => {
let currentPlay = 0;
const listTrackId: number[] = [];
if (!tracksOnAnAlbum) {
console.log("Fail to get album...");
console.log('Fail to get album...');
return;
}
for (let iii = 0; iii < tracksOnAnAlbum.length; iii++) {
@ -41,7 +41,7 @@ export const AlbumDetailPage = () => {
if (!dataAlbum) {
return (
<>
<TopBar />
<TopBar title="Album detail" />
<PageLayoutInfoCenter>
Fail to load artist id: {albumId}
</PageLayoutInfoCenter>
@ -50,7 +50,7 @@ export const AlbumDetailPage = () => {
}
return (
<>
<TopBar />
<TopBar title="Album detail" />
<PageLayout>
<Flex
direction="row"

View File

@ -1,38 +1,54 @@
import { Text, Wrap, WrapItem } from '@chakra-ui/react';
import { useState } from 'react';
import React from 'react';
import {
Input,
InputGroup,
InputLeftElement,
InputRightElement,
Text,
Wrap,
WrapItem,
useOutsideClick,
} from '@chakra-ui/react';
import { MdSearch } from 'react-icons/md';
import { useNavigate } from 'react-router-dom';
import { EmptyEnd } from '@/components/EmptyEnd';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { SearchInput } from '@/components/SearchInput';
import { TopBar } from '@/components/TopBar/TopBar';
import { DisplayAlbum } from '@/components/album/DisplayAlbum';
import { useAlbumService } from '@/service/Album';
import { useAlbumService, useOrderedAlbums } from '@/service/Album';
import { useThemeMode } from '@/utils/theme-tools';
import { EmptyEnd } from '@/components/EmptyEnd';
export const AlbumsPage = () => {
const { store } = useAlbumService();
const [filterTitle, setFilterTitle] = useState<string | undefined>(undefined);
const { isLoading, dataAlbums } = useOrderedAlbums(filterTitle);
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (albumId: number) => {
navigate(`/album/${albumId}`);
};
if (store.isLoading) {
if (isLoading) {
return (
<>
<TopBar />
<TopBar title="All Albums" />
<PageLayoutInfoCenter>No Album available</PageLayoutInfoCenter>
</>
);
}
return (
<>
<TopBar />
<TopBar title="All Albums">
<SearchInput onChange={setFilterTitle} />
</TopBar>
<PageLayout>
<Text>All Albums:</Text>
<Wrap spacing="20px" marginX="auto" padding="20px" justify="center">
{store.data?.map((data) => (
{dataAlbums.map((data) => (
<WrapItem
width="270px"
height="120px"

View File

@ -3,11 +3,11 @@ import { LuDisc3, LuUser } from 'react-icons/lu';
import { useParams } from 'react-router-dom';
import { Covers } from '@/components/Cover';
import { DisplayTrack } from '@/components/DisplayTrack';
import { EmptyEnd } from '@/components/EmptyEnd';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { DisplayTrack } from '@/components/track/DisplayTrack';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificAlbum } from '@/service/Album';
import { useSpecificArtist } from '@/service/Artist';
@ -28,7 +28,7 @@ export const ArtistAlbumDetailPage = () => {
let currentPlay = 0;
const listTrackId: number[] = [];
if (!tracksOnAnAlbum) {
console.error("Fail to get the album ...");
console.error('Fail to get the album ...');
return;
}
for (let iii = 0; iii < tracksOnAnAlbum.length; iii++) {
@ -44,7 +44,7 @@ export const ArtistAlbumDetailPage = () => {
if (!dataAlbum) {
return (
<>
<TopBar />
<TopBar title="Album detail" />
<PageLayoutInfoCenter>
Fail to load artist id: {artistId}
</PageLayoutInfoCenter>
@ -53,7 +53,7 @@ export const ArtistAlbumDetailPage = () => {
}
return (
<>
<TopBar />
<TopBar title="Album detail" />
<PageLayout>
<Flex
direction="row"

View File

@ -3,6 +3,7 @@ import { LuUser } from 'react-icons/lu';
import { useNavigate, useParams } from 'react-router-dom';
import { Covers } from '@/components/Cover';
import { EmptyEnd } from '@/components/EmptyEnd';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
@ -10,7 +11,6 @@ import { DisplayAlbumId } from '@/components/album/DisplayAlbumId';
import { useSpecificArtist } from '@/service/Artist';
import { useAlbumIdsOfAnArtist } from '@/service/Track';
import { useThemeMode } from '@/utils/theme-tools';
import { EmptyEnd } from '@/components/EmptyEnd';
export const ArtistDetailPage = () => {
const { artistId } = useParams();
@ -26,7 +26,7 @@ export const ArtistDetailPage = () => {
if (!dataArtist) {
return (
<>
<TopBar />
<TopBar title="Artist detail" />
<PageLayoutInfoCenter>
Fail to load artist id: {artistId}
</PageLayoutInfoCenter>
@ -35,7 +35,7 @@ export const ArtistDetailPage = () => {
}
return (
<>
<TopBar />
<TopBar title="Artist detail" />
<PageLayout>
<Flex
direction="row"

View File

@ -1,29 +1,34 @@
import { useState } from 'react';
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';
import { EmptyEnd } from '@/components/EmptyEnd';
import { PageLayout } from '@/components/Layout/PageLayout';
import { SearchInput } from '@/components/SearchInput';
import { TopBar } from '@/components/TopBar/TopBar';
import { useArtistService, useOrderedArtists } from '@/service/Artist';
import { useThemeMode } from '@/utils/theme-tools';
export const ArtistsPage = () => {
const { mode } = useThemeMode();
const [filterName, setFilterName] = useState<string | undefined>(undefined);
const navigate = useNavigate();
const onSelectItem = (data: Artist) => {
navigate(`/artist/${data.id}/`);
};
const { store } = useArtistService();
const { dataArtist } = useOrderedArtists(filterName);
return (
<>
<TopBar />
<TopBar title="All artists">
<SearchInput onChange={setFilterName} />
</TopBar>
<PageLayout>
<Text>All Artists:</Text>
<Wrap spacing="20px" marginX="auto" padding="20px">
{store.data?.map((data) => (
<Wrap spacing="20px" marginX="auto" padding="20px" justify="center">
{dataArtist?.map((data) => (
<WrapItem
width="270px"
height="120px"

View File

@ -0,0 +1,108 @@
import { Box, Flex, Text } from '@chakra-ui/react';
import { LuDisc3 } from 'react-icons/lu';
import { useParams } from 'react-router-dom';
import { Covers } from '@/components/Cover';
import { EmptyEnd } from '@/components/EmptyEnd';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { TopBar } from '@/components/TopBar/TopBar';
import { DisplayTrack } from '@/components/track/DisplayTrack';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useSpecificGender } from '@/service/Gender';
import { useTracksOfAGender } from '@/service/Track';
//import { useTracksOfAGender } from '@/service/Track';
import { useThemeMode } from '@/utils/theme-tools';
export const GenderDetailPage = () => {
const { genderId } = useParams();
const genderIdInt = genderId ? parseInt(genderId, 10) : undefined;
const { mode } = useThemeMode();
const { playInList } = useActivePlaylistService();
const { dataGender } = useSpecificGender(genderIdInt);
const { tracksOnAGender } = useTracksOfAGender(genderIdInt);
const onSelectItem = (trackId: number) => {
//navigate(`/artist/${artistIdInt}/gender/${genderId}`);
let currentPlay = 0;
const listTrackId: number[] = [];
if (!tracksOnAGender) {
console.log('Fail to get gender...');
return;
}
for (let iii = 0; iii < tracksOnAGender.length; iii++) {
listTrackId.push(tracksOnAGender[iii].id);
if (tracksOnAGender[iii].id === trackId) {
currentPlay = iii;
}
}
playInList(currentPlay, listTrackId);
};
console.log(`dataGender = ${JSON.stringify(dataGender, null, 2)}`);
if (!dataGender) {
return (
<>
<TopBar title="Gender detail" />
<PageLayoutInfoCenter>
Fail to load artist id: {genderId}
</PageLayoutInfoCenter>
</>
);
}
return (
<>
<TopBar title="Gender detail" />
<PageLayout>
<Flex
direction="row"
width="80%"
marginX="auto"
padding="10px"
gap="10px"
>
<Covers
data={dataGender?.covers}
iconEmpty={<LuDisc3 size="100" height="full" />}
/>
<Flex direction="column" width="80%" marginRight="auto">
<Text fontSize="24px" fontWeight="bold">
{dataGender?.name}
</Text>
{dataGender?.description && (
<Text>Description: {dataGender?.description}</Text>
)}
</Flex>
</Flex>
<Flex
direction="column"
gap="20px"
marginX="auto"
padding="20px"
width="80%"
>
{tracksOnAGender?.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,16 @@
import { Navigate, Route, Routes } from 'react-router-dom';
import { Error404 } from '@/errors';
import { GenderDetailPage } from '@/scene/gender/GenderDetailPage';
import { GendersPage } from '@/scene/gender/GendersPage';
export const GenderRoutes = () => {
return (
<Routes>
<Route path="/" element={<Navigate to="all" replace />} />
<Route path="all" element={<GendersPage />} />
<Route path=":genderId" element={<GenderDetailPage />} />
<Route path="*" element={<Error404 />} />
</Routes>
);
};

View File

@ -0,0 +1,64 @@
import { useState } from 'react';
import { Wrap, WrapItem } from '@chakra-ui/react';
import { useNavigate } from 'react-router-dom';
import { EmptyEnd } from '@/components/EmptyEnd';
import { PageLayout } from '@/components/Layout/PageLayout';
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
import { SearchInput } from '@/components/SearchInput';
import { TopBar } from '@/components/TopBar/TopBar';
import { DisplayGender } from '@/components/gender/DisplayGender';
import { useOrderedGenders } from '@/service/Gender';
import { useThemeMode } from '@/utils/theme-tools';
export const GendersPage = () => {
const [filterTitle, setFilterTitle] = useState<string | undefined>(undefined);
const { isLoading, dataGenders } = useOrderedGenders(filterTitle);
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (genderId: number) => {
navigate(`/gender/${genderId}`);
};
if (isLoading) {
return (
<>
<TopBar title="All Genders" />
<PageLayoutInfoCenter>No Gender available</PageLayoutInfoCenter>
</>
);
}
return (
<>
<TopBar title="All Genders">
<SearchInput onChange={setFilterTitle} />
</TopBar>
<PageLayout>
<Wrap spacing="20px" marginX="auto" padding="20px" justify="center">
{dataGenders.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.id)}
>
<DisplayGender dataGender={data} />
</WrapItem>
))}
</Wrap>
<EmptyEnd />
</PageLayout>
</>
);
};

View File

@ -0,0 +1,91 @@
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 { DataTools, TypeCheck } from '@/utils/data-tools';
import { useThemeMode } from '@/utils/theme-tools';
type HelpListType = {
id: number;
name: string;
icon: ReactElement;
to: string;
};
const helpList: HelpListType[] = [
{
id: 1,
name: 'plouf',
icon: <LuCrown size="60%" height="full" />,
to: 'gender',
},
];
export const HelpPage = async () => {
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (data: HelpListType) => {
navigate(data.to);
};
const testData = [
{
name: 'lkjlkj',
},
];
const result = await DataTools.getsWhere(
testData,
[
{
check: TypeCheck.STARTS_WITH,
key: 'name',
value: ['ll', 'k'],
},
],
['track', 'name']
);
console.log(`startsWith : ${JSON.stringify(result, null, 2)}`);
return (
<>
<TopBar title="Help" />
<PageLayout>
<Wrap spacing="20px" marginX="auto" padding="20px" justify="center">
{helpList.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

@ -55,9 +55,9 @@ export const HomePage = () => {
};
return (
<>
<TopBar />
<TopBar title="Home" />
<PageLayout>
<Wrap spacing="20px" marginX="auto" padding="20px">
<Wrap spacing="20px" marginX="auto" padding="20px" justify="center">
{homeList.map((data) => (
<WrapItem
width="200px"

View File

@ -25,7 +25,7 @@ export const SSOPage = () => {
clearToken();
}
}, [token, setToken, clearToken]);
const delay = isDevelopmentEnvironment() ? 20000 : 2000;
const delay = isDevelopmentEnvironment() ? 6000 : 2000;
useEffect(() => {
if (state === SessionState.CONNECTED) {
const destination = data ? b64_to_utf8(data) : '/';

View File

@ -2,19 +2,15 @@ import { Navigate, Route, Routes } from 'react-router-dom';
import { Error404 } from '@/errors';
import { ArtistAlbumDetailPage } from '@/scene/artist/ArtistAlbumDetailPage';
import { ArtistDetailPage } from '@/scene/artist/ArtistDetailPage';
import { ArtistsPage } from '@/scene/artist/ArtistsPage';
import { TrackSelectionPage } from '@/scene/track/TrackSelectionPage';
import { TracksStartLetterDetailPage } from '@/scene/track/TracksStartLetterDetailPage';
export const ArtistRoutes = () => {
export const TrackRoutes = () => {
return (
<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="all" element={<TrackSelectionPage />} />
<Route path=":startLetter" element={<TracksStartLetterDetailPage />} />
<Route path="*" element={<Error404 />} />
</Routes>
);

View File

@ -0,0 +1,85 @@
import { ReactElement } from 'react';
import { Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { useNavigate } from 'react-router-dom';
import { PageLayout } from '@/components/Layout/PageLayout';
import { TopBar } from '@/components/TopBar/TopBar';
import { DataTools, TypeCheck } from '@/utils/data-tools';
import { useThemeMode } from '@/utils/theme-tools';
export const alphabet = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
];
const possibilities = [...alphabet, '^^'];
export const TrackSelectionPage = () => {
const { mode } = useThemeMode();
const navigate = useNavigate();
const onSelectItem = (data: string) => {
navigate(`/track/${data}`);
};
return (
<>
<TopBar title="All Tracks" />
<PageLayout>
<Wrap spacing="20px" marginX="auto" padding="20px" justify="center">
{possibilities.map((data) => (
<WrapItem
width="75px"
height="75px"
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)}
>
<Flex direction="column" width="full" height="full">
<Text
margin="auto"
fontSize="25px"
fontWeight="bold"
textTransform="uppercase"
userSelect="none"
>
{data.toUpperCase()}
</Text>
</Flex>
</WrapItem>
))}
</Wrap>
</PageLayout>
</>
);
};

View File

@ -0,0 +1,103 @@
import { ReactElement } from 'react';
import { Box, Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
import { useNavigate, useParams } from 'react-router-dom';
import { EmptyEnd } from '@/components/EmptyEnd';
import { PageLayout } from '@/components/Layout/PageLayout';
import { TopBar } from '@/components/TopBar/TopBar';
import { DisplayTrack } from '@/components/track/DisplayTrack';
import { DisplayTrackFull } from '@/components/track/DisplayTrackFull';
import { DisplayTrackSkeleton } from '@/components/track/DisplayTrackSkeleton';
import { alphabet } from '@/scene/track/TrackSelectionPage';
import { useActivePlaylistService } from '@/service/ActivePlaylist';
import { useTrackService, useTracksWithStartName } from '@/service/Track';
import { useThemeMode } from '@/utils/theme-tools';
export const TracksStartLetterDetailPage = () => {
const { startLetter } = useParams();
const { mode } = useThemeMode();
const { isLoading, tracksStartsWith } = useTracksWithStartName(
startLetter !== '^^'
? startLetter
? [startLetter.toLowerCase(), startLetter.toUpperCase()]
: 'ZZZZ'
: [...alphabet, ...alphabet.map((str) => str.toUpperCase())],
startLetter === '^^'
);
const { playInList } = useActivePlaylistService();
const onSelectItem = (trackId: number) => {
//navigate(`/artist/${artistIdInt}/gender/${genderId}`);
let currentPlay = 0;
const listTrackId: number[] = [];
if (!tracksStartsWith) {
console.log('Fail to get tracks...');
return;
}
for (let iii = 0; iii < tracksStartsWith.length; iii++) {
listTrackId.push(tracksStartsWith[iii].id);
if (tracksStartsWith[iii].id === trackId) {
currentPlay = iii;
}
}
playInList(currentPlay, listTrackId);
};
console.log(
`Redraw ... isLoading=${isLoading} data=${tracksStartsWith?.length} ... ${Date()}`
);
return (
<>
<TopBar title="All Tracks" />
<PageLayout>
<Flex
direction="column"
gap="20px"
marginX="auto"
padding="20px"
width="80%"
>
{isLoading &&
[1, 2, 3, 4]?.map((data) => (
<Box
minWidth="100%"
//height="60px"
border="1px"
borderColor="brand.900"
backgroundColor={mode('#FFFFFF88', '#00000088')}
key={data}
padding="5px"
as="button"
_hover={{
boxShadow: 'outline-over',
bgColor: mode('#FFFFFFF7', '#000000F7'),
}}
>
<DisplayTrackSkeleton />
</Box>
))}
{!isLoading &&
tracksStartsWith?.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)}
>
<DisplayTrackFull track={data} />
</Box>
))}
<EmptyEnd />
</Flex>
</PageLayout>
</>
);
};

View File

@ -4,6 +4,8 @@ import { Album, AlbumResource } 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 { isNullOrUndefined } from '@/utils/validator';
export type AlbumServiceProps = {
store: DataStoreType<Album>;
@ -17,19 +19,43 @@ export const useAlbumService = (): AlbumServiceProps => {
export const useAlbumServiceWrapped = (
session: SessionServiceProps
): AlbumServiceProps => {
const store = useDataStore<Album>({
restApiName: 'ALBUM',
primaryKey: 'id',
getsCall: () => {
return AlbumResource.gets({
restConfig: session.getRestConfig(),
});
const store = useDataStore<Album>(
{
restApiName: 'ALBUM',
primaryKey: 'id',
getsCall: () => {
return AlbumResource.gets({
restConfig: session.getRestConfig(),
});
},
},
});
[session.token]
);
return { store };
};
export const useOrderedAlbums = (titleFilter: string | undefined) => {
const { store } = useAlbumService();
const dataAlbums = useMemo(async () => {
let tmpData = store.data;
if (!isNullOrUndefined(titleFilter)) {
tmpData = DataTools.getNameLike(tmpData, titleFilter);
}
return await DataTools.getsWhere(
tmpData,
[
{
check: TypeCheck.NOT_EQUAL,
key: 'id',
value: [undefined, null],
},
],
['name', 'id']
);
}, [store.data, titleFilter]);
return { isLoading: store.isLoading, dataAlbums };
};
export const useSpecificAlbum = (id: number | undefined) => {
const { store } = useAlbumService();
const dataAlbum = useMemo(() => {

View File

@ -1,9 +1,11 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo } 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';
import { DataTools, TypeCheck } from '@/utils/data-tools';
import { isNullOrUndefined } from '@/utils/validator';
export type ArtistServiceProps = {
store: DataStoreType<Artist>;
@ -17,19 +19,44 @@ export const useArtistService = (): ArtistServiceProps => {
export const useArtistServiceWrapped = (
session: SessionServiceProps
): ArtistServiceProps => {
const store = useDataStore<Artist>({
restApiName: 'ARTIST',
primaryKey: 'id',
getsCall: () => {
return ArtistResource.gets({
restConfig: session.getRestConfig(),
});
const store = useDataStore<Artist>(
{
restApiName: 'ARTIST',
primaryKey: 'id',
getsCall: () => {
return ArtistResource.gets({
restConfig: session.getRestConfig(),
});
},
},
});
[session.token]
);
return { store };
};
export const useOrderedArtists = (nameFilter: string | undefined) => {
const { store } = useArtistService();
const dataArtist = useMemo(async () => {
let tmpData = store.data;
if (!isNullOrUndefined(nameFilter)) {
tmpData = DataTools.getNameLike(tmpData, nameFilter);
}
return await DataTools.getsWhere(
tmpData,
[
{
check: TypeCheck.NOT_EQUAL,
key: 'id',
value: [undefined, null],
},
],
['name', 'id']
);
}, [store.data, nameFilter]);
return { isLoading: store.isLoading, dataArtist };
};
export const useSpecificArtist = (id: number | undefined) => {
const { store } = useArtistService();
const dataArtist = useMemo(() => {
@ -37,3 +64,11 @@ export const useSpecificArtist = (id: number | undefined) => {
}, [store.data, id]);
return { dataArtist };
};
export const useSpecificArtists = (ids: number[] | undefined) => {
const { store } = useArtistService();
const dataArtists = useMemo(() => {
return store.gets(ids);
}, [store.data, ids]);
return { dataArtists };
};

View File

@ -0,0 +1,73 @@
import { useMemo } from 'react';
import { Gender, GenderResource } 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 { isNullOrUndefined } from '@/utils/validator';
export type GenderServiceProps = {
store: DataStoreType<Gender>;
};
export const useGenderService = (): GenderServiceProps => {
const { gender } = useServiceContext();
return gender;
};
export const useGenderServiceWrapped = (
session: SessionServiceProps
): GenderServiceProps => {
const store = useDataStore<Gender>(
{
restApiName: 'GENDER',
primaryKey: 'id',
getsCall: () => {
return GenderResource.gets({
restConfig: session.getRestConfig(),
});
},
},
[session.token]
);
return { store };
};
export const useOrderedGenders = (titleFilter: string | undefined) => {
const { store } = useGenderService();
const dataGenders = useMemo(async () => {
let tmpData = store.data;
if (!isNullOrUndefined(titleFilter)) {
tmpData = DataTools.getNameLike(tmpData, titleFilter);
}
return await DataTools.getsWhere(
tmpData,
[
{
check: TypeCheck.NOT_EQUAL,
key: 'id',
value: [undefined, null],
},
],
['name', 'id']
);
}, [store.data, titleFilter]);
return { isLoading: store.isLoading, dataGenders };
};
export const useSpecificGender = (id: number | undefined) => {
const { store } = useGenderService();
const dataGender = useMemo(() => {
return store.get(id);
}, [store.data, id]);
return { dataGender };
};
export const useGenderOfAnArtist = (idArtist: number | undefined) => {
const { store } = useGenderService();
const dataGender = useMemo(() => {
return store.get(idArtist);
}, [store.data, idArtist]);
return { dataGender };
};

View File

@ -34,8 +34,8 @@ export class GenericDataService<TYPE> {
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.getsWhere(response, [
.then(async (response: TYPE[]) => {
let data = await DataTools.getsWhere(response, [
{
check: TypeCheck.EQUAL,
key: 'id',
@ -76,8 +76,8 @@ export class GenericDataService<TYPE> {
return new Promise((resolve, reject) => {
self
.gets()
.then((response: TYPE[]) => {
let data = DataTools.getsWhere(
.then(async (response: TYPE[]) => {
let data = await DataTools.getsWhere(
response,
[
{

View File

@ -6,6 +6,7 @@ import {
} from '@/service/ActivePlaylist';
import { AlbumServiceProps, useAlbumServiceWrapped } from '@/service/Album';
import { ArtistServiceProps, useArtistServiceWrapped } from '@/service/Artist';
import { useGenderServiceWrapped } from '@/service/Gender';
import { SessionState } from '@/service/SessionState';
import { TrackServiceProps, useTrackServiceWrapped } from '@/service/Track';
import {
@ -20,13 +21,14 @@ export type ServiceContextType = {
track: TrackServiceProps;
artist: ArtistServiceProps;
album: AlbumServiceProps;
gender: AlbumServiceProps;
activePlaylist: ActivePlaylistServiceProps;
};
export const ServiceContext = createContext<ServiceContextType>({
session: {
setToken: (token: string) => { },
clearToken: () => { },
setToken: (token: string) => {},
clearToken: () => {},
hasReadRight: (part: RightPart) => false,
hasWriteRight: (part: RightPart) => false,
state: SessionState.NO_USER,
@ -36,7 +38,14 @@ export const ServiceContext = createContext<ServiceContextType>({
store: {
data: [],
isLoading: true,
get: (value, key) => { console.error("!!! WTF !!!"); return undefined; },
get: (_value, _key) => {
console.error('!!! WTF !!!');
return undefined;
},
gets: (_value, _key) => {
console.error('!!! WTF !!!');
return [];
},
error: undefined,
},
},
@ -44,7 +53,14 @@ export const ServiceContext = createContext<ServiceContextType>({
store: {
data: [],
isLoading: true,
get: (value, key) => { console.error("!!! WTF !!!"); return undefined; },
get: (_value, _key) => {
console.error('!!! WTF !!!');
return undefined;
},
gets: (_value, _key) => {
console.error('!!! WTF !!!');
return [];
},
error: undefined,
},
},
@ -52,7 +68,29 @@ export const ServiceContext = createContext<ServiceContextType>({
store: {
data: [],
isLoading: true,
get: (value, key) => { console.error("!!! WTF !!!"); return undefined; },
get: (_value, _key) => {
console.error('!!! WTF !!!');
return undefined;
},
gets: (_value, _key) => {
console.error('!!! WTF !!!');
return [];
},
error: undefined,
},
},
gender: {
store: {
data: [],
isLoading: true,
get: (_value, _key) => {
console.error('!!! WTF !!!');
return undefined;
},
gets: (_value, _key) => {
console.error('!!! WTF !!!');
return [];
},
error: undefined,
},
},
@ -60,13 +98,27 @@ export const ServiceContext = createContext<ServiceContextType>({
playTrackList: [],
trackOffset: undefined,
trackActive: undefined,
setNewPlaylist: (_listIds: number[]) => { console.error("!!! WTF !!!"); },
setNewPlaylistShuffle: (_listIds: number[]) => { console.error("!!! WTF !!!"); },
playInList: (_id: number, _listIds: number[]) => { console.error("!!! WTF !!!"); },
play: (_id: number) => { console.error("!!! WTF !!!"); },
previous: () => { console.error("!!! WTF !!!"); },
next: () => { console.error("!!! WTF !!!"); },
first: () => { console.error("!!! WTF !!!"); },
setNewPlaylist: (_listIds: number[]) => {
console.error('!!! WTF !!!');
},
setNewPlaylistShuffle: (_listIds: number[]) => {
console.error('!!! WTF !!!');
},
playInList: (_id: number, _listIds: number[]) => {
console.error('!!! WTF !!!');
},
play: (_id: number) => {
console.error('!!! WTF !!!');
},
previous: () => {
console.error('!!! WTF !!!');
},
next: () => {
console.error('!!! WTF !!!');
},
first: () => {
console.error('!!! WTF !!!');
},
},
});
@ -81,6 +133,7 @@ export const ServiceContextProvider = ({
const track = useTrackServiceWrapped(session);
const artist = useArtistServiceWrapped(session);
const album = useAlbumServiceWrapped(session);
const gender = useGenderServiceWrapped(session);
const activePlaylist = useActivePlaylistServiceWrapped(track);
const contextObjectData = useMemo(
() => ({
@ -89,6 +142,7 @@ export const ServiceContextProvider = ({
artist,
album,
activePlaylist,
gender,
}),
[session, track, artist, album]
);

View File

@ -19,15 +19,18 @@ export const useTrackService = (): TrackServiceProps => {
export const useTrackServiceWrapped = (
session: SessionServiceProps
): TrackServiceProps => {
const store = useDataStore<Track>({
restApiName: 'TRACK',
primaryKey: 'id',
getsCall: () => {
return TrackResource.gets({
restConfig: session.getRestConfig(),
});
const store = useDataStore<Track>(
{
restApiName: 'TRACK',
primaryKey: 'id',
getsCall: () => {
return TrackResource.gets({
restConfig: session.getRestConfig(),
});
},
},
});
[session.token]
);
return { store };
};
@ -46,23 +49,29 @@ export const useSpecificTrack = (id: number | undefined) => {
*/
export const useTracksOfAnArtist = (idArtist?: number) => {
const { store } = useTrackService();
const tracksOnAnArtist = useMemo(() => {
const [tracksOnAnArtist, setTracksOnAnArtist] = useState<Track[]>([]);
useEffect(() => {
if (idArtist) {
return DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.CONTAINS,
key: 'artists',
value: idArtist,
},
],
['track', 'name']
)
const tmpFunction = async () => {
const data = await DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.CONTAINS,
key: 'artists',
value: idArtist,
},
],
['track', 'name']
);
setTracksOnAnArtist(data);
};
tmpFunction();
} else {
setTracksOnAnArtist([]);
}
return [];
}, [store.data, idArtist]);
return { tracksOnAnArtist };
}, [store.data, idArtist, setTracksOnAnArtist]);
return { isLoading: store.isLoading, tracksOnAnArtist };
};
/**
@ -72,10 +81,59 @@ export const useTracksOfAnArtist = (idArtist?: number) => {
*/
export const useCountTracksOfAnArtist = (idArtist: number) => {
const { tracksOnAnAlbum } = useTracksOfAnAlbum(idArtist);
const countTracksOnAnArtist = useMemo(() => tracksOnAnAlbum?.length ?? 0, [tracksOnAnAlbum]);
const countTracksOnAnArtist = useMemo(
() => tracksOnAnAlbum?.length ?? 0,
[tracksOnAnAlbum]
);
return { countTracksOnAnArtist };
};
/**
* Get all the track for a specific artist
* @param idGender - Id of the artist.
* @returns a promise on the list of track elements
*/
export const useTracksOfAGender = (idGender?: number) => {
const { store } = useTrackService();
const [tracksOnAGender, setTracksOnAGender] = useState<Track[]>([]);
useEffect(() => {
if (idGender) {
const tmpFunction = async () => {
const ret = await DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.CONTAINS,
key: 'genderId',
value: idGender,
},
],
['name', 'track']
);
setTracksOnAGender(ret);
};
tmpFunction();
} else {
setTracksOnAGender([]);
}
}, [store.data, idGender]);
return { tracksOnAGender };
};
/**
* Get the number of track in this artist ID
* @param id - Id of the gender.
* @returns The number of track present in this artist
*/
export const useCountTracksOfAGender = (idGender?: number) => {
const { tracksOnAGender } = useTracksOfAGender(idGender);
const countTracksOnAGender = useMemo(
() => tracksOnAGender?.length ?? 0,
[tracksOnAGender]
);
return { countTracksOnAGender };
};
/**
* Get all the album of a specific artist
* @param artistId - ID of the artist
@ -100,19 +158,26 @@ export const useAlbumIdsOfAnArtist = (idArtist?: number) => {
export const useTracksOfAnAlbum = (idAlbum?: number) => {
const { store } = useTrackService();
const tracksOnAnAlbum = useMemo(() => {
const [tracksOnAnAlbum, setTracksOnAnAlbum] = useState<Track[]>();
useEffect(() => {
if (idAlbum) {
return DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.EQUAL,
key: 'albumId',
value: idAlbum,
},
],
['track', 'name', 'id']
);
const tmpFunction = async () => {
const ret = await DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.EQUAL,
key: 'albumId',
value: idAlbum,
},
],
['track', 'name', 'id']
);
setTracksOnAnAlbum(ret);
};
tmpFunction();
} else {
setTracksOnAnAlbum([]);
}
}, [store.data, idAlbum]);
return { tracksOnAnAlbum };
@ -125,7 +190,10 @@ export const useTracksOfAnAlbum = (idAlbum?: number) => {
*/
export const useCountTracksWithAlbumId = (albumId?: number) => {
const { tracksOnAnAlbum } = useTracksOfAnAlbum(albumId);
const countTracksOfAnAlbum = useMemo(() => tracksOnAnAlbum?.length ?? 0, [tracksOnAnAlbum]);
const countTracksOfAnAlbum = useMemo(
() => tracksOnAnAlbum?.length ?? 0,
[tracksOnAnAlbum]
);
return { countTracksOfAnAlbum };
};
@ -136,26 +204,64 @@ export const useCountTracksWithAlbumId = (albumId?: number) => {
*/
export const useTracksOfArtistWithNoAlbum = (idArtist: number) => {
const { store } = useTrackService();
const tracksOnAnArtistWithNoAlbum = useMemo(() => {
const [tracksOnAnArtistWithNoAlbum, setTracksOnAnArtistWithNoAlbum] =
useState<Track[]>([]);
useEffect(() => {
if (idArtist) {
return DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.CONTAINS,
key: 'artists',
value: idArtist,
},
{
check: TypeCheck.EQUAL,
key: 'albumId',
value: undefined,
},
],
['track', 'name']
);
const tmpFunction = async () => {
const ret = await DataTools.getsWhere(
store.data,
[
{
check: TypeCheck.CONTAINS,
key: 'artists',
value: idArtist,
},
{
check: TypeCheck.EQUAL,
key: 'albumId',
value: undefined,
},
],
['track', 'name']
);
setTracksOnAnArtistWithNoAlbum(ret);
};
tmpFunction();
} else {
setTracksOnAnArtistWithNoAlbum([]);
}
return [];
}, [store.data, idArtist]);
return { tracksOnAnArtistWithNoAlbum };
};
/**
* Get all the track for a specific artist
* @param startName - basic starting name.
* @returns a promise on the list of track elements
*/
export const useTracksWithStartName = (
startName: string | string[],
invert: boolean
) => {
const { store } = useTrackService();
const [tracksStartsWith, setTracksStartsWith] = useState<Track[]>([]);
useEffect(() => {
const tmpFunction = async () => {
const ret = await DataTools.getsWhere(
store.data,
[
{
check: invert ? TypeCheck.STARTS_NOT_WITH : TypeCheck.STARTS_WITH,
key: 'name',
value: startName,
},
],
['name', 'track']
);
setTracksStartsWith(ret);
};
tmpFunction();
}, [store.data, startName, invert]);
return { isLoading: store.isLoading, tracksStartsWith };
};

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { PartRight, RESTConfig, UserMe, UserResource } from '@/back-api';
import { environment, getApiUrl } from '@/environment';
@ -10,7 +10,7 @@ import { parseToken } from '@/utils/sso';
const TOKEN_KEY = 'karusic-token-key-storage';
export const USERS = {
ADMIN:
admin:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIiwiYXBwbGljYXRpb24iOiJrYXJ1c2ljIiwiaXNzIjoiS2FyQXV0aCIsInJpZ2h0Ijp7ImthcnVzaWMiOnsiQURNSU4iOnRydWUsIlVTRVIiOnRydWV9fSwibG9naW4iOiJIZWVyb1l1aSIsImV4cCI6MTcyNDIwNjc5NCwiaWF0IjoxNzI0MTY2ODM0fQ.TEST_SIGNATURE_FOR_LOCAL_TEST_AND_TEST_E2E',
NO_USER: '',
} as const;
@ -18,7 +18,7 @@ export const USERS = {
export const getUserToken = () => {
return localStorage.getItem(TOKEN_KEY);
};
export type RightPart = 'ADMIN' | 'USER';
export type RightPart = 'admin' | 'user';
export function getRestConfig(): RESTConfig {
return {
@ -71,13 +71,13 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
restConfig: getRestConfig(),
})
.then((response: UserMe) => {
console.log(` ==> New right arrived to '${response.login}'`);
//console.log(` ==> New right arrived to '${response.login}'`);
setState(SessionState.CONNECTED);
setConfig(response);
})
.catch((error) => {
setState(SessionState.CONNECTION_FAIL);
console.log(` ==> Fail to get right: '${error}'`);
//console.log(` ==> Fail to get right: '${error}'`);
localStorage.removeItem(TOKEN_KEY);
});
}
@ -96,6 +96,7 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
}, [updateRight, setToken]);
const hasReadRight = useCallback(
(part: RightPart) => {
//console.log(`config = ${JSON.stringify(config, null, 2)}`);
const right = config?.rights[environment.applName];
if (right === undefined) {
return false;
@ -137,3 +138,15 @@ export const useSessionServiceWrapped = (): SessionServiceProps => {
getRestConfig,
};
};
export const useHasRight = (part: RightPart) => {
const { token, hasReadRight, hasWriteRight } = useSessionService();
const isReadable = useMemo(() => {
console.log(`get is read for: ${part} ==> ${hasReadRight(part)}`);
return hasReadRight(part);
}, [token, hasReadRight, part]);
const isWritable = useMemo(() => {
return hasWriteRight(part);
}, [token, hasWriteRight, part]);
return { isReadable, isWritable };
};

View File

@ -3,7 +3,7 @@
* @copyright 2024, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { useCallback, useEffect, useState } from 'react';
import { DependencyList, useCallback, useEffect, useState } from 'react';
import { RestErrorResponse } from '@/back-api';
import { isNullOrUndefined } from '@/utils/validator';
@ -13,17 +13,25 @@ export type DataStoreType<TYPE> = {
error: RestErrorResponse | undefined;
data: TYPE[];
get: <MODEL>(value: MODEL, key?: string) => TYPE | undefined;
gets: <MODEL>(value: MODEL[] | undefined, key?: string) => TYPE[];
};
export const useDataStore = <TYPE>({
primaryKey = 'id',
restApiName,
getsCall,
}: {
restApiName?: string;
primaryKey?: string;
getsCall: () => Promise<TYPE[]>;
}): DataStoreType<TYPE> => {
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export const useDataStore = <TYPE>(
{
primaryKey = 'id',
restApiName,
getsCall,
}: {
restApiName?: string;
primaryKey?: string;
getsCall: () => Promise<TYPE[]>;
},
deps: DependencyList = []
): DataStoreType<TYPE> => {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<RestErrorResponse | undefined>(undefined);
const [data, setData] = useState<TYPE[]>([]);
@ -34,10 +42,13 @@ export const useDataStore = <TYPE>({
setError(undefined);
setIsLoading(true);
getsCall()
.then((response: TYPE[]) => {
console.log(
.then(async (response: TYPE[]) => {
/*console.log(
`[${restApiName}] getData Response: ${JSON.stringify(response, null, 2)}`
);
);*/
console.log(`[${restApiName}] ${Date()} Receive data done (wait 10s)`);
await delay(10000);
console.log(`[${restApiName}] ${Date()} Ready`);
setData(response);
setError(undefined);
setIsLoading(false);
@ -49,7 +60,7 @@ export const useDataStore = <TYPE>({
setError(error);
setIsLoading(false);
});
}, [setIsLoading, setData]);
}, [setIsLoading, setData, ...deps]);
const get = useCallback(
<MODEL>(value: MODEL, key?: string): TYPE | undefined => {
@ -63,6 +74,22 @@ export const useDataStore = <TYPE>({
},
[data]
);
const gets = useCallback(
<MODEL>(value: MODEL[] | undefined, key?: string): TYPE[] => {
const keyValue = key ?? primaryKey;
const out: TYPE[] = [];
if (isNullOrUndefined(value)) {
return out;
}
for (let iii = 0; iii < data.length; iii++) {
if (value.includes(data[iii][keyValue])) {
out.push(data[iii]);
}
}
return out;
},
[data]
);
const update = useCallback(
(value: TYPE, key?: string) => {
@ -76,5 +103,5 @@ export const useDataStore = <TYPE>({
[data, setData]
);
return { isLoading, error, data, get }; //, update};
return { isLoading, error, data, get, gets }; //, update};
};

View File

@ -3,7 +3,12 @@
* @copyright 2018, Edouard DUPIN, all right reserved
* @license PROPRIETARY (see license file)
*/
import { isArray, isNullOrUndefined } from '@/utils/validator';
import {
isArray,
isArrayOf,
isNullOrUndefined,
isString,
} from '@/utils/validator';
export enum TypeCheck {
CONTAINS = 'C',
@ -13,6 +18,8 @@ export enum TypeCheck {
LESS_EQUAL = '<=',
GREATER = '>',
GREATER_EQUAL = '>=',
STARTS_WITH = 'START',
STARTS_NOT_WITH = '!START',
}
export interface SelectModel {
@ -78,13 +85,15 @@ export namespace DataTools {
bdd: TYPE[],
select: SelectModel[],
orderByData?: string[]
): TYPE[] {
// console.log("[I] gets_where " + this.name + " select " + _select);
let tmpList = getSubList(bdd, select);
if (tmpList && orderByData) {
tmpList = orderBy(tmpList, orderByData);
}
return tmpList;
): Promise<TYPE[]> {
return new Promise((resolve, _reject) => {
// console.log("[I] gets_where " + this.name + " select " + _select);
let tmpList = getSubList(bdd, select);
if (tmpList && orderByData) {
tmpList = orderBy(tmpList, orderByData);
}
resolve(tmpList);
});
}
export function get<TYPE>(
@ -123,7 +132,7 @@ export namespace DataTools {
let nameLower = name.toLowerCase();
for (const item of bdd) {
// console.log("compare '" + _name + "' ??? '" + v['name'] + "'");
if (item['name'].toLowerCase() === nameLower) {
if (item['name'].toLowerCase().includes(nameLower)) {
out.push(item);
}
}
@ -157,7 +166,26 @@ export namespace DataTools {
}
return tmp.length;
}
export function startsWith<TYPE>(
value: TYPE,
listValues: TYPE | TYPE[]
): boolean {
if (!isString(value)) {
return false;
}
if (isArrayOf(listValues, isString)) {
for (const item of listValues) {
if (value.startsWith(item)) {
return true;
}
}
return false;
}
if (isString(listValues)) {
return value.startsWith(listValues);
}
return false;
}
export function existIn<TYPE>(
value: TYPE,
listValues: TYPE | TYPE[]
@ -203,8 +231,8 @@ export namespace DataTools {
let valueElement = values[iiiElem][control.key];
//console.log(" valueElement : " + JSON.stringify(valueElement, null, 2));
if (isArray(control.value)) {
//console.log(`check ${control.check} ... ${valueElement} in ${control.value}`);
if (control.check === TypeCheck.CONTAINS) {
//console.log("check contains ... " + valueElement + " contains one element in " + control.value);
if (
DataTools.containsOneIn(valueElement, control.value) === false
) {
@ -222,6 +250,16 @@ export namespace DataTools {
find = true;
break;
}
} else if (control.check === TypeCheck.STARTS_WITH) {
if (DataTools.startsWith(valueElement, control.value) === false) {
find = false;
break;
}
} else if (control.check === TypeCheck.STARTS_NOT_WITH) {
if (DataTools.startsWith(valueElement, control.value) === true) {
find = false;
break;
}
} else {
console.log(
'[ERROR] Internal Server Error{ unknown comparing type ...'
@ -268,6 +306,19 @@ export namespace DataTools {
find = false;
break;
}
} else if (control.check === TypeCheck.STARTS_WITH) {
console.log(
`AA start with : ${valueElement} with ${control.value}`
);
if (DataTools.startsWith(valueElement, control.value) === false) {
find = false;
break;
}
} else if (control.check === TypeCheck.STARTS_NOT_WITH) {
if (DataTools.startsWith(valueElement, control.value) === true) {
find = false;
break;
}
} else {
console.log(
'[ERROR] Internal Server Error{ unknown comparing type ...'