Work on form and basic tools I need
This commit is contained in:
parent
093994b4b3
commit
d1352f520a
@ -53,7 +53,7 @@
|
|||||||
"react-popper": "2.3.0",
|
"react-popper": "2.3.0",
|
||||||
"react-router-dom": "6.26.1",
|
"react-router-dom": "6.26.1",
|
||||||
"react-select": "5.8.0",
|
"react-select": "5.8.0",
|
||||||
"react-simple-keyboard": "3.7.147",
|
"react-simple-keyboard": "3.7.148",
|
||||||
"react-sticky-el": "2.1.1",
|
"react-sticky-el": "2.1.1",
|
||||||
"react-use": "17.5.1",
|
"react-use": "17.5.1",
|
||||||
"react-use-draggable-scroll": "0.4.7",
|
"react-use-draggable-scroll": "0.4.7",
|
||||||
@ -74,16 +74,16 @@
|
|||||||
"@storybook/react-vite": "8.2.9",
|
"@storybook/react-vite": "8.2.9",
|
||||||
"@storybook/theming": "8.2.9",
|
"@storybook/theming": "8.2.9",
|
||||||
"@testing-library/jest-dom": "6.5.0",
|
"@testing-library/jest-dom": "6.5.0",
|
||||||
"@testing-library/react": "16.0.0",
|
"@testing-library/react": "16.0.1",
|
||||||
"@testing-library/user-event": "14.5.2",
|
"@testing-library/user-event": "14.5.2",
|
||||||
"@trivago/prettier-plugin-sort-imports": "4.3.0",
|
"@trivago/prettier-plugin-sort-imports": "4.3.0",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "29.5.12",
|
||||||
"@types/node": "22.5.0",
|
"@types/node": "22.5.1",
|
||||||
"@types/react": "18.3.4",
|
"@types/react": "18.3.5",
|
||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
"@types/react-sticky-el": "1.0.7",
|
"@types/react-sticky-el": "1.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "8.2.0",
|
"@typescript-eslint/eslint-plugin": "8.3.0",
|
||||||
"@typescript-eslint/parser": "8.2.0",
|
"@typescript-eslint/parser": "8.3.0",
|
||||||
"@vitejs/plugin-react": "4.3.1",
|
"@vitejs/plugin-react": "4.3.1",
|
||||||
"eslint": "9.9.1",
|
"eslint": "9.9.1",
|
||||||
"eslint-plugin-codeceptjs": "1.3.0",
|
"eslint-plugin-codeceptjs": "1.3.0",
|
||||||
@ -93,16 +93,16 @@
|
|||||||
"eslint-plugin-storybook": "0.8.0",
|
"eslint-plugin-storybook": "0.8.0",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "29.7.0",
|
"jest-environment-jsdom": "29.7.0",
|
||||||
"knip": "5.27.4",
|
"knip": "5.27.5",
|
||||||
"lint-staged": "15.2.9",
|
"lint-staged": "15.2.9",
|
||||||
|
"npm-check-updates": "^17.1.0",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"puppeteer": "23.1.1",
|
"puppeteer": "23.2.1",
|
||||||
"react-is": "18.3.1",
|
"react-is": "18.3.1",
|
||||||
"storybook": "8.2.9",
|
"storybook": "8.2.9",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.5.4",
|
||||||
"vite": "5.4.2",
|
"vite": "5.4.2",
|
||||||
"vitest": "2.0.5",
|
"vitest": "2.0.5"
|
||||||
"npm-check-updates": "^17.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1195
front2/pnpm-lock.yaml
generated
1195
front2/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -32,12 +32,6 @@ export const SearchInput = ({
|
|||||||
width: '250px',
|
width: '250px',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const ref = React.useRef(null);
|
|
||||||
// TODO: find a better way...
|
|
||||||
useOutsideClick({
|
|
||||||
ref: ref,
|
|
||||||
handler: onFocusLost,
|
|
||||||
});
|
|
||||||
function onChange(event): void {
|
function onChange(event): void {
|
||||||
const data =
|
const data =
|
||||||
event.target.value.length === 0 ? undefined : event.target.value;
|
event.target.value.length === 0 ? undefined : event.target.value;
|
||||||
@ -57,8 +51,8 @@ export const SearchInput = ({
|
|||||||
<MdSearch color="gray.300" />
|
<MdSearch color="gray.300" />
|
||||||
</InputLeftElement>
|
</InputLeftElement>
|
||||||
<Input
|
<Input
|
||||||
ref={ref}
|
|
||||||
onFocus={onFocusKeep}
|
onFocus={onFocusKeep}
|
||||||
|
onBlur={() => setTimeout(() => onFocusLost(), 200)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
|
@ -41,6 +41,11 @@ import { useThemeMode } from '@/utils/theme-tools';
|
|||||||
|
|
||||||
export const TOP_BAR_HEIGHT = '50px';
|
export const TOP_BAR_HEIGHT = '50px';
|
||||||
|
|
||||||
|
export const BUTTON_TOP_BAR_PROPERTY = {
|
||||||
|
variant: '@menu',
|
||||||
|
height: TOP_BAR_HEIGHT,
|
||||||
|
};
|
||||||
|
|
||||||
export type TopBarProps = {
|
export type TopBarProps = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
title?: string;
|
title?: string;
|
||||||
@ -48,10 +53,7 @@ export type TopBarProps = {
|
|||||||
|
|
||||||
export const TopBar = ({ title, children }: TopBarProps) => {
|
export const TopBar = ({ title, children }: TopBarProps) => {
|
||||||
const { mode, colorMode, toggleColorMode } = useThemeMode();
|
const { mode, colorMode, toggleColorMode } = useThemeMode();
|
||||||
const buttonProperty = {
|
|
||||||
variant: '@menu',
|
|
||||||
height: TOP_BAR_HEIGHT,
|
|
||||||
};
|
|
||||||
const { session } = useServiceContext();
|
const { session } = useServiceContext();
|
||||||
const backColor = mode('back.100', 'back.800');
|
const backColor = mode('back.100', 'back.800');
|
||||||
const drawerDisclose = useDisclosure();
|
const drawerDisclose = useDisclosure();
|
||||||
@ -86,7 +88,7 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
|||||||
boxShadow={'0px 2px 4px ' + colors.back[900]}
|
boxShadow={'0px 2px 4px ' + colors.back[900]}
|
||||||
zIndex={200}
|
zIndex={200}
|
||||||
>
|
>
|
||||||
<Button {...buttonProperty} onClick={onChangeTheme}>
|
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onChangeTheme}>
|
||||||
<LuAlignJustify />
|
<LuAlignJustify />
|
||||||
<Text paddingLeft="3px" fontWeight="bold">
|
<Text paddingLeft="3px" fontWeight="bold">
|
||||||
Karusic
|
Karusic
|
||||||
@ -107,13 +109,17 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
|||||||
<Flex right="0">
|
<Flex right="0">
|
||||||
{session?.state !== SessionState.CONNECTED && (
|
{session?.state !== SessionState.CONNECTED && (
|
||||||
<>
|
<>
|
||||||
<Button {...buttonProperty} onClick={onSignIn}>
|
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onSignIn}>
|
||||||
<LuLogIn />
|
<LuLogIn />
|
||||||
<Text paddingLeft="3px" fontWeight="bold">
|
<Text paddingLeft="3px" fontWeight="bold">
|
||||||
Sign-in
|
Sign-in
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...buttonProperty} onClick={onSignUp} disabled={true}>
|
<Button
|
||||||
|
{...BUTTON_TOP_BAR_PROPERTY}
|
||||||
|
onClick={onSignUp}
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
<LuPlusCircle />
|
<LuPlusCircle />
|
||||||
<Text paddingLeft="3px" fontWeight="bold">
|
<Text paddingLeft="3px" fontWeight="bold">
|
||||||
Sign-up
|
Sign-up
|
||||||
@ -127,14 +133,13 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
|||||||
as={IconButton}
|
as={IconButton}
|
||||||
aria-label="Options"
|
aria-label="Options"
|
||||||
icon={<LuUserCircle />}
|
icon={<LuUserCircle />}
|
||||||
{...buttonProperty}
|
{...BUTTON_TOP_BAR_PROPERTY}
|
||||||
width={TOP_BAR_HEIGHT}
|
width={TOP_BAR_HEIGHT}
|
||||||
/>
|
/>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem _hover={{}} color={mode('brand.800', 'brand.200')}>
|
<MenuItem _hover={{}} color={mode('brand.800', 'brand.200')}>
|
||||||
Sign in as {session?.login ?? 'Fail'}
|
Sign in as {session?.login ?? 'Fail'}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem icon={<LuArrowUpSquare />}>Add Media</MenuItem>
|
|
||||||
<MenuItem icon={<LuSettings />}>Settings</MenuItem>
|
<MenuItem icon={<LuSettings />}>Settings</MenuItem>
|
||||||
<MenuItem icon={<LuHelpCircle />}>Help</MenuItem>
|
<MenuItem icon={<LuHelpCircle />}>Help</MenuItem>
|
||||||
<MenuItem icon={<LuLogOut onClick={onSignOut} />}>
|
<MenuItem icon={<LuLogOut onClick={onSignOut} />}>
|
||||||
@ -176,16 +181,30 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<DrawerBody>
|
<DrawerBody paddingX="0px">
|
||||||
<Button {...buttonProperty} onClick={onSelectHome} width="fill">
|
<Button
|
||||||
|
background="#00000000"
|
||||||
|
borderRadius="0px"
|
||||||
|
onClick={onSelectHome}
|
||||||
|
width="full"
|
||||||
|
>
|
||||||
<LuHome />
|
<LuHome />
|
||||||
<Text paddingLeft="3px" fontWeight="bold">
|
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
|
||||||
Home
|
Home
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
<p>Some contents...</p>
|
<hr />
|
||||||
<p>Some contents...</p>
|
<Button
|
||||||
<p>Some contents...</p>
|
background="#00000000"
|
||||||
|
borderRadius="0px"
|
||||||
|
onClick={onSelectHome}
|
||||||
|
width="full"
|
||||||
|
>
|
||||||
|
<LuArrowUpSquare />
|
||||||
|
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
|
||||||
|
Add Media
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
</DrawerBody>
|
</DrawerBody>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
42
front2/src/components/contextMenu/ContextMenu.tsx
Normal file
42
front2/src/components/contextMenu/ContextMenu.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { LuMenu } from 'react-icons/lu';
|
||||||
|
|
||||||
|
export type MenuElement = {
|
||||||
|
name: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContextMenuProps = {
|
||||||
|
elements?: MenuElement[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ContextMenu = ({ elements }: ContextMenuProps) => {
|
||||||
|
if (!elements) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
aria-label="Options"
|
||||||
|
icon={<LuMenu />}
|
||||||
|
marginY="auto"
|
||||||
|
/>
|
||||||
|
<MenuList>
|
||||||
|
{elements?.map((data) => (
|
||||||
|
<MenuItem key={data.name} onClick={data.onClick}>
|
||||||
|
{data.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
35
front2/src/components/form/FormGroup.tsx
Normal file
35
front2/src/components/form/FormGroup.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormControlProps,
|
||||||
|
FormErrorMessage,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { MdLabelImportant } from 'react-icons/md';
|
||||||
|
|
||||||
|
export type FormGroupProps = FormControlProps & {
|
||||||
|
error?: ReactNode;
|
||||||
|
help?: ReactNode;
|
||||||
|
label?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormGroup = ({
|
||||||
|
children,
|
||||||
|
error,
|
||||||
|
help,
|
||||||
|
label,
|
||||||
|
...props
|
||||||
|
}: FormGroupProps) => (
|
||||||
|
<FormControl marginTop="4px" {...props}>
|
||||||
|
{!!label && <FormLabel>{label}</FormLabel>}
|
||||||
|
{children}
|
||||||
|
{!!help && <FormHelperText>{help}</FormHelperText>}
|
||||||
|
|
||||||
|
<FormErrorMessage>
|
||||||
|
<MdLabelImportant />
|
||||||
|
{error}
|
||||||
|
</FormErrorMessage>
|
||||||
|
</FormControl>
|
||||||
|
);
|
87
front2/src/components/form/FormSelectList.tsx
Normal file
87
front2/src/components/form/FormSelectList.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { Box, Button, Flex, Text } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { isNullOrUndefined } from '@/utils/validator';
|
||||||
|
|
||||||
|
export type SelectMultipleValueDisplayType = {
|
||||||
|
id: any;
|
||||||
|
name: any;
|
||||||
|
isSelected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionToOptionDisplay = (
|
||||||
|
data: SelectMultipleValueDisplayType[] | undefined,
|
||||||
|
selectedOptions: SelectMultipleValueDisplayType[],
|
||||||
|
search?: string
|
||||||
|
): SelectMultipleValueDisplayType[] => {
|
||||||
|
if (isNullOrUndefined(data)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out: SelectMultipleValueDisplayType[] = [];
|
||||||
|
data.forEach((element) => {
|
||||||
|
if (search) {
|
||||||
|
if (!element.name.toLowerCase().includes(search.toLowerCase())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push({
|
||||||
|
...element,
|
||||||
|
isSelected:
|
||||||
|
selectedOptions.find((elem) => elem.id === element.id) !== undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormSelectListProps = {
|
||||||
|
options?: SelectMultipleValueDisplayType[];
|
||||||
|
selected: SelectMultipleValueDisplayType[];
|
||||||
|
onSelectValue: (data: SelectMultipleValueDisplayType) => void;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
export const FormSelectList = ({
|
||||||
|
options,
|
||||||
|
selected,
|
||||||
|
onSelectValue,
|
||||||
|
search,
|
||||||
|
}: FormSelectListProps) => {
|
||||||
|
const displayedValue = optionToOptionDisplay(options, selected, search);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box position="relative">
|
||||||
|
<Flex
|
||||||
|
direction="column"
|
||||||
|
width="full"
|
||||||
|
position="absolute"
|
||||||
|
border="1px"
|
||||||
|
borderColor="black"
|
||||||
|
backgroundColor="gray.700"
|
||||||
|
overflowY="auto"
|
||||||
|
overflowX="hidden"
|
||||||
|
maxHeight="300px"
|
||||||
|
zIndex={300}
|
||||||
|
transform="translateY(1px)"
|
||||||
|
>
|
||||||
|
{displayedValue.length === 0 && (
|
||||||
|
<Text marginX="auto" color="red.500" fontWeight="bold" marginY="10px">
|
||||||
|
... No element found...
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{displayedValue.map((data) => (
|
||||||
|
<Button
|
||||||
|
key={data.id}
|
||||||
|
marginY="1px"
|
||||||
|
borderRadius="0px"
|
||||||
|
autoFocus={false}
|
||||||
|
backgroundColor={data.isSelected ? 'green.800' : '0x00000000'}
|
||||||
|
_hover={{ backgroundColor: 'gray.400' }}
|
||||||
|
onClick={() => onSelectValue(data)}
|
||||||
|
>
|
||||||
|
<Text marginRight="auto" autoFocus={false}>
|
||||||
|
{data.name}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
126
front2/src/components/form/SelectMultiple.tsx
Normal file
126
front2/src/components/form/SelectMultiple.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Flex,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
Spinner,
|
||||||
|
Tag,
|
||||||
|
TagCloseButton,
|
||||||
|
TagLabel,
|
||||||
|
Wrap,
|
||||||
|
WrapItem,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { MdSearch } from 'react-icons/md';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormSelectList,
|
||||||
|
SelectMultipleValueDisplayType,
|
||||||
|
} from '@/components/form/FormSelectList';
|
||||||
|
import { isNullOrUndefined } from '@/utils/validator';
|
||||||
|
|
||||||
|
export type SelectMultipleProps = {
|
||||||
|
options?: object[];
|
||||||
|
values?: (number | string)[];
|
||||||
|
onChange?: (value: (number | string)[] | undefined) => void;
|
||||||
|
keyKey?: string;
|
||||||
|
keyValue?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectMultiple = ({
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
values,
|
||||||
|
keyKey = 'id',
|
||||||
|
keyValue = keyKey,
|
||||||
|
}: SelectMultipleProps) => {
|
||||||
|
const [showList, setShowList] = useState(false);
|
||||||
|
const transformedOption = useMemo(() => {
|
||||||
|
return options?.map((element) => {
|
||||||
|
return {
|
||||||
|
id: element[keyKey],
|
||||||
|
name: element[keyValue],
|
||||||
|
isSelected: false,
|
||||||
|
} as SelectMultipleValueDisplayType;
|
||||||
|
});
|
||||||
|
}, [options, keyKey, keyValue]);
|
||||||
|
const [currentSearch, setCurrentSearch] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedOptions = useMemo(() => {
|
||||||
|
if (isNullOrUndefined(values) || !transformedOption) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return transformedOption.filter((element) => {
|
||||||
|
return values.includes(element[keyKey]);
|
||||||
|
});
|
||||||
|
}, [values, transformedOption]);
|
||||||
|
|
||||||
|
const selectValue = (data: SelectMultipleValueDisplayType) => {
|
||||||
|
const newValues = values?.includes(data.id)
|
||||||
|
? values.filter((elem) => data.id === elem)
|
||||||
|
: [...(values ?? []), data.id];
|
||||||
|
setShowList(false);
|
||||||
|
if (onChange) {
|
||||||
|
if (newValues.length == 0) {
|
||||||
|
onChange(undefined);
|
||||||
|
} else {
|
||||||
|
onChange(newValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!options) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
function onChangeInput(value: string): void {
|
||||||
|
if (value === '') {
|
||||||
|
setCurrentSearch(undefined);
|
||||||
|
} else {
|
||||||
|
setCurrentSearch(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" width="full" gap="0px">
|
||||||
|
{selectedOptions && (
|
||||||
|
<Wrap spacing="5px" justify="left" width="full" marginBottom="2px">
|
||||||
|
{selectedOptions.map((data) => (
|
||||||
|
<WrapItem key={data[keyKey]}>
|
||||||
|
<Tag
|
||||||
|
size="md"
|
||||||
|
key="md"
|
||||||
|
borderRadius="5px"
|
||||||
|
variant="solid"
|
||||||
|
backgroundColor="green.500"
|
||||||
|
>
|
||||||
|
<TagLabel>{data[keyValue] ?? `id=${data[keyKey]}`}</TagLabel>
|
||||||
|
<TagCloseButton onClick={() => selectValue(data)} />
|
||||||
|
</Tag>
|
||||||
|
</WrapItem>
|
||||||
|
))}
|
||||||
|
</Wrap>
|
||||||
|
)}
|
||||||
|
<InputGroup minWidth="50%" marginLeft="auto">
|
||||||
|
<InputRightElement pointerEvents="none">
|
||||||
|
<MdSearch color="gray.300" />
|
||||||
|
</InputRightElement>
|
||||||
|
<Input
|
||||||
|
onChange={(e) => onChangeInput(e.target.value)}
|
||||||
|
//onSubmit={onSubmit}
|
||||||
|
onFocus={() => setShowList(true)}
|
||||||
|
onBlur={() => setTimeout(() => setShowList(false), 200)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
{showList && (
|
||||||
|
<FormSelectList
|
||||||
|
options={transformedOption}
|
||||||
|
selected={selectedOptions}
|
||||||
|
search={currentSearch}
|
||||||
|
onSelectValue={selectValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
137
front2/src/components/form/SelectSingle.tsx
Normal file
137
front2/src/components/form/SelectSingle.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
Spinner,
|
||||||
|
Tag,
|
||||||
|
TagCloseButton,
|
||||||
|
TagLabel,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
MdClose,
|
||||||
|
MdKeyboardArrowDown,
|
||||||
|
MdKeyboardArrowUp,
|
||||||
|
MdSearch,
|
||||||
|
} from 'react-icons/md';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormSelectList,
|
||||||
|
SelectMultipleValueDisplayType,
|
||||||
|
} from '@/components/form/FormSelectList';
|
||||||
|
import { isNullOrUndefined } from '@/utils/validator';
|
||||||
|
|
||||||
|
export type SelectSingleProps = {
|
||||||
|
options?: object[];
|
||||||
|
value?: number | string;
|
||||||
|
onChange?: (value: number | string | undefined) => void;
|
||||||
|
keyKey?: string;
|
||||||
|
keyValue?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectSingle = ({
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
keyKey = 'id',
|
||||||
|
keyValue = keyKey,
|
||||||
|
}: SelectSingleProps) => {
|
||||||
|
const [showList, setShowList] = useState(false);
|
||||||
|
const transformedOption = useMemo(() => {
|
||||||
|
return options?.map((element) => {
|
||||||
|
return {
|
||||||
|
id: element[keyKey],
|
||||||
|
name: element[keyValue],
|
||||||
|
isSelected: false,
|
||||||
|
} as SelectMultipleValueDisplayType;
|
||||||
|
});
|
||||||
|
}, [options, keyKey, keyValue]);
|
||||||
|
const [currentSearch, setCurrentSearch] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
|
const selectedOptions = useMemo(() => {
|
||||||
|
if (isNullOrUndefined(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return transformedOption?.find((data) => data.id === value[keyKey]);
|
||||||
|
}, [value, transformedOption]);
|
||||||
|
|
||||||
|
const selectValue = (data?: SelectMultipleValueDisplayType) => {
|
||||||
|
const tmpData = data?.id == selectedOptions?.id ? undefined : data;
|
||||||
|
setShowList(false);
|
||||||
|
if (onChange) {
|
||||||
|
onChange(tmpData?.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!transformedOption) {
|
||||||
|
return <Spinner />;
|
||||||
|
}
|
||||||
|
function onChangeInput(value: string): void {
|
||||||
|
if (value === '') {
|
||||||
|
setCurrentSearch(undefined);
|
||||||
|
} else {
|
||||||
|
setCurrentSearch(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onRemoveItem = () => {
|
||||||
|
if (showList || !selectedOptions) {
|
||||||
|
ref?.current?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Remove item ...');
|
||||||
|
if (onChange) {
|
||||||
|
onChange(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" width="full" gap="0px">
|
||||||
|
<Flex>
|
||||||
|
<Input
|
||||||
|
ref={ref}
|
||||||
|
width="full"
|
||||||
|
onChange={(e) => onChangeInput(e.target.value)}
|
||||||
|
//onSubmit={onSubmit}
|
||||||
|
onFocus={() => setShowList(true)}
|
||||||
|
onBlur={() => setTimeout(() => setShowList(false), 200)}
|
||||||
|
value={
|
||||||
|
showList ? (currentSearch ?? '') : (selectedOptions?.name ?? '')
|
||||||
|
}
|
||||||
|
backgroundColor={
|
||||||
|
showList || !selectedOptions ? undefined : 'green.500'
|
||||||
|
}
|
||||||
|
borderRadius="5px 0 0 5px"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={onRemoveItem}
|
||||||
|
variant="outline"
|
||||||
|
borderRadius="0 5px 5px 0"
|
||||||
|
borderWidth="1px 1px 1px 0"
|
||||||
|
isDisabled={showList}
|
||||||
|
>
|
||||||
|
{selectedOptions ? (
|
||||||
|
<MdClose color="gray.300" />
|
||||||
|
) : showList ? (
|
||||||
|
<MdKeyboardArrowUp color="gray.300" />
|
||||||
|
) : (
|
||||||
|
<MdKeyboardArrowDown color="gray.300" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
{showList && (
|
||||||
|
<FormSelectList
|
||||||
|
options={transformedOption}
|
||||||
|
selected={selectedOptions ? [selectedOptions] : []}
|
||||||
|
search={currentSearch}
|
||||||
|
onSelectValue={selectValue}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
179
front2/src/components/popup/TrackEditPopUp.tsx
Normal file
179
front2/src/components/popup/TrackEditPopUp.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
NumberInput,
|
||||||
|
Select,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Album, Track } from '@/back-api';
|
||||||
|
import { FormGroup } from '@/components/form/FormGroup';
|
||||||
|
import { SelectMultiple } from '@/components/form/SelectMultiple';
|
||||||
|
import { SelectSingle } from '@/components/form/SelectSingle';
|
||||||
|
import { useOrderedAlbums } from '@/service/Album';
|
||||||
|
import { useOrderedArtists } from '@/service/Artist';
|
||||||
|
import { useOrderedGenders } from '@/service/Gender';
|
||||||
|
import { useSpecificTrack } from '@/service/Track';
|
||||||
|
import { isNullOrUndefined } from '@/utils/validator';
|
||||||
|
|
||||||
|
export type TrackEditPopUpProps = {};
|
||||||
|
|
||||||
|
const getDifferences = (obj1: any, obj2: any): { [key: string]: boolean } => {
|
||||||
|
const result: { [key: string]: boolean } = {};
|
||||||
|
for (const key in obj1) {
|
||||||
|
if (obj1.hasOwnProperty(key)) {
|
||||||
|
result[key] = obj1[key] !== obj2[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasAnyTrue = (obj: { [key: string]: boolean }): boolean => {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key) && obj[key] === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TrackEditPopUp = ({}: TrackEditPopUpProps) => {
|
||||||
|
const { trackId } = useParams();
|
||||||
|
const { dataGenders } = useOrderedGenders(undefined);
|
||||||
|
const { dataArtist } = useOrderedArtists(undefined);
|
||||||
|
const { dataAlbums } = useOrderedAlbums(undefined);
|
||||||
|
const { dataTrack } = useSpecificTrack(
|
||||||
|
isNullOrUndefined(trackId) ? undefined : parseInt(trackId, 10)
|
||||||
|
);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const onClose = () => {
|
||||||
|
navigate('../../', { relative: 'path' });
|
||||||
|
};
|
||||||
|
const initialRef = useRef(null);
|
||||||
|
const finalRef = useRef(null);
|
||||||
|
|
||||||
|
const [newData, setNewData] = useState<Track>();
|
||||||
|
const [changeData, setChangeData] = useState<{ [key: string]: boolean }>();
|
||||||
|
const [changeOneData, setChangeOneData] = useState<boolean>(false);
|
||||||
|
// initialize value when ready
|
||||||
|
useEffect(() => {
|
||||||
|
setNewData(dataTrack);
|
||||||
|
}, [dataTrack, setNewData, setChangeData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!newData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ret = getDifferences(dataTrack, newData);
|
||||||
|
setChangeData(ret);
|
||||||
|
setChangeOneData(hasAnyTrue(ret));
|
||||||
|
console.log(
|
||||||
|
`ChangeData=${JSON.stringify(ret, null, 2)} ChangeOneData=${hasAnyTrue(ret)}`
|
||||||
|
);
|
||||||
|
}, [dataTrack, newData, setChangeData, setChangeOneData]);
|
||||||
|
|
||||||
|
const onChangeValue = (key: string, data) => {
|
||||||
|
console.log(`[${key}] data: ${data}`);
|
||||||
|
setNewData((previous) => {
|
||||||
|
if (previous === undefined) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
if (previous[key] === data) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
const tmp = { ...previous };
|
||||||
|
tmp[key] = data;
|
||||||
|
console.log(`data = ${JSON.stringify(tmp, null, 2)}`);
|
||||||
|
return tmp;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
initialFocusRef={initialRef}
|
||||||
|
finalFocusRef={finalRef}
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
onClose={onClose}
|
||||||
|
isOpen={true}
|
||||||
|
>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Edit Track</ModalHeader>
|
||||||
|
<ModalCloseButton ref={finalRef} />
|
||||||
|
|
||||||
|
<ModalBody pb={6}>
|
||||||
|
<FormGroup isRequired>
|
||||||
|
<FormLabel>Title</FormLabel>
|
||||||
|
<Input
|
||||||
|
ref={initialRef}
|
||||||
|
value={dataTrack?.name}
|
||||||
|
onChange={(e) => onChangeValue('name', e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<label>Description</label>
|
||||||
|
<Textarea
|
||||||
|
value={dataTrack?.description}
|
||||||
|
onChange={(e) => onChangeValue('description', e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<label>Gender</label>
|
||||||
|
<SelectSingle
|
||||||
|
value={dataTrack?.genderId}
|
||||||
|
options={dataGenders}
|
||||||
|
onChange={(value) => onChangeValue('genderId', value)}
|
||||||
|
keyValue="name"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<label>Artist(s)</label>
|
||||||
|
<SelectMultiple
|
||||||
|
options={dataArtist}
|
||||||
|
onChange={(value) => onChangeValue('artists', value)}
|
||||||
|
keyValue="name"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup>
|
||||||
|
<label>Album</label>
|
||||||
|
<SelectSingle
|
||||||
|
options={dataAlbums}
|
||||||
|
onChange={(value) => onChangeValue('albumId', value)}
|
||||||
|
keyValue="name"
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>track n°</FormLabel>
|
||||||
|
<NumberInput
|
||||||
|
value={dataTrack?.track}
|
||||||
|
onChange={(e) => onChangeValue('track', e)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
{changeOneData && (
|
||||||
|
<Button colorScheme="blue" mr={3}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -5,6 +5,7 @@ import { LuMusic2, LuPlay } from 'react-icons/lu';
|
|||||||
|
|
||||||
import { Track } from '@/back-api';
|
import { Track } from '@/back-api';
|
||||||
import { Covers } from '@/components/Cover';
|
import { Covers } from '@/components/Cover';
|
||||||
|
import { ContextMenu, MenuElement } from '@/components/contextMenu/ContextMenu';
|
||||||
import { DisplayTrackSkeleton } from '@/components/track/DisplayTrackSkeleton';
|
import { DisplayTrackSkeleton } from '@/components/track/DisplayTrackSkeleton';
|
||||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||||
import { useSpecificAlbum } from '@/service/Album';
|
import { useSpecificAlbum } from '@/service/Album';
|
||||||
@ -14,8 +15,13 @@ import { useSpecificGender } from '@/service/Gender';
|
|||||||
export type DisplayTrackProps = {
|
export type DisplayTrackProps = {
|
||||||
track: Track;
|
track: Track;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
contextMenu?: MenuElement[];
|
||||||
};
|
};
|
||||||
export const DisplayTrackFull = ({ track, onClick }: DisplayTrackProps) => {
|
export const DisplayTrackFull = ({
|
||||||
|
track,
|
||||||
|
onClick,
|
||||||
|
contextMenu,
|
||||||
|
}: DisplayTrackProps) => {
|
||||||
const { trackActive } = useActivePlaylistService();
|
const { trackActive } = useActivePlaylistService();
|
||||||
const { dataAlbum } = useSpecificAlbum(track?.albumId);
|
const { dataAlbum } = useSpecificAlbum(track?.albumId);
|
||||||
const { dataGender } = useSpecificGender(track?.genderId);
|
const { dataGender } = useSpecificGender(track?.genderId);
|
||||||
@ -106,6 +112,7 @@ export const DisplayTrackFull = ({ track, onClick }: DisplayTrackProps) => {
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<ContextMenu elements={contextMenu} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||||
import { LuDisc3 } from 'react-icons/lu';
|
import { LuDisc3 } from 'react-icons/lu';
|
||||||
import { useParams } from 'react-router-dom';
|
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Covers } from '@/components/Cover';
|
import { Covers } from '@/components/Cover';
|
||||||
import { EmptyEnd } from '@/components/EmptyEnd';
|
import { EmptyEnd } from '@/components/EmptyEnd';
|
||||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||||
import { TopBar } from '@/components/TopBar/TopBar';
|
import { TopBar } from '@/components/TopBar/TopBar';
|
||||||
|
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
||||||
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
||||||
|
import { DisplayTrackFull } from '@/components/track/DisplayTrackFull';
|
||||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||||
import { useSpecificAlbum } from '@/service/Album';
|
import { useSpecificAlbum } from '@/service/Album';
|
||||||
import { useTracksOfAnAlbum } from '@/service/Track';
|
import { useTracksOfAnAlbum } from '@/service/Track';
|
||||||
@ -20,6 +22,7 @@ export const AlbumDetailPage = () => {
|
|||||||
const { playInList } = useActivePlaylistService();
|
const { playInList } = useActivePlaylistService();
|
||||||
const { dataAlbum } = useSpecificAlbum(albumIdInt);
|
const { dataAlbum } = useSpecificAlbum(albumIdInt);
|
||||||
const { tracksOnAnAlbum } = useTracksOfAnAlbum(albumIdInt);
|
const { tracksOnAnAlbum } = useTracksOfAnAlbum(albumIdInt);
|
||||||
|
const navigate = useNavigate();
|
||||||
const onSelectItem = (trackId: number) => {
|
const onSelectItem = (trackId: number) => {
|
||||||
//navigate(`/artist/${artistIdInt}/album/${albumId}`);
|
//navigate(`/artist/${artistIdInt}/album/${albumId}`);
|
||||||
let currentPlay = 0;
|
let currentPlay = 0;
|
||||||
@ -86,7 +89,7 @@ export const AlbumDetailPage = () => {
|
|||||||
{tracksOnAnAlbum?.map((data) => (
|
{tracksOnAnAlbum?.map((data) => (
|
||||||
<Box
|
<Box
|
||||||
minWidth="100%"
|
minWidth="100%"
|
||||||
height="60px"
|
//height="60px"
|
||||||
border="1px"
|
border="1px"
|
||||||
borderColor="brand.900"
|
borderColor="brand.900"
|
||||||
backgroundColor={mode('#FFFFFF88', '#00000088')}
|
backgroundColor={mode('#FFFFFF88', '#00000088')}
|
||||||
@ -98,14 +101,26 @@ export const AlbumDetailPage = () => {
|
|||||||
bgColor: mode('#FFFFFFF7', '#000000F7'),
|
bgColor: mode('#FFFFFFF7', '#000000F7'),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DisplayTrack
|
<DisplayTrackFull
|
||||||
track={data}
|
track={data}
|
||||||
onClick={() => onSelectItem(data.id)}
|
onClick={() => onSelectItem(data.id)}
|
||||||
|
contextMenu={[
|
||||||
|
{
|
||||||
|
name: 'Edit',
|
||||||
|
onClick: () => {
|
||||||
|
navigate(`/album/${albumId}/edit/${data.id}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: 'Add Playlist', onClick: () => {} },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
<EmptyEnd />
|
<EmptyEnd />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Routes>
|
||||||
|
<Route path="edit/:trackId" element={<TrackEditPopUp />} />
|
||||||
|
</Routes>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ export const AlbumRoutes = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="all" replace />} />
|
<Route path="/" element={<Navigate to="all" replace />} />
|
||||||
<Route path="all" element={<AlbumsPage />} />
|
<Route path="all" element={<AlbumsPage />} />
|
||||||
<Route path=":albumId" element={<AlbumDetailPage />} />
|
<Route path=":albumId/*" element={<AlbumDetailPage />} />
|
||||||
<Route path="*" element={<Error404 />} />
|
<Route path="*" element={<Error404 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
@ -1,17 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
import { Wrap, WrapItem } from '@chakra-ui/react';
|
||||||
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 { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { EmptyEnd } from '@/components/EmptyEnd';
|
import { EmptyEnd } from '@/components/EmptyEnd';
|
||||||
@ -20,7 +9,7 @@ import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
|||||||
import { SearchInput } from '@/components/SearchInput';
|
import { SearchInput } from '@/components/SearchInput';
|
||||||
import { TopBar } from '@/components/TopBar/TopBar';
|
import { TopBar } from '@/components/TopBar/TopBar';
|
||||||
import { DisplayAlbum } from '@/components/album/DisplayAlbum';
|
import { DisplayAlbum } from '@/components/album/DisplayAlbum';
|
||||||
import { useAlbumService, useOrderedAlbums } from '@/service/Album';
|
import { useOrderedAlbums } from '@/service/Album';
|
||||||
import { useThemeMode } from '@/utils/theme-tools';
|
import { useThemeMode } from '@/utils/theme-tools';
|
||||||
|
|
||||||
export const AlbumsPage = () => {
|
export const AlbumsPage = () => {
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
import { Box, Button, Flex, Text } from '@chakra-ui/react';
|
||||||
import { LuDisc3, LuUser } from 'react-icons/lu';
|
import { LuDisc3, LuUser } from 'react-icons/lu';
|
||||||
|
import { MdPerson } from 'react-icons/md';
|
||||||
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Covers } from '@/components/Cover';
|
import { Covers } from '@/components/Cover';
|
||||||
import { EmptyEnd } from '@/components/EmptyEnd';
|
import { EmptyEnd } from '@/components/EmptyEnd';
|
||||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||||
import { TopBar } from '@/components/TopBar/TopBar';
|
import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar';
|
||||||
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
||||||
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
||||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||||
@ -54,34 +55,26 @@ export const ArtistAlbumDetailPage = () => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar title="Album detail" />
|
<TopBar title={dataArtist ? undefined : 'Album detail'}>
|
||||||
<PageLayout>
|
{dataArtist && (
|
||||||
<Flex
|
<Button
|
||||||
direction="row"
|
{...BUTTON_TOP_BAR_PROPERTY}
|
||||||
width="80%"
|
marginRight="auto"
|
||||||
marginX="auto"
|
onClick={() => navigate(`/artist/${dataArtist.id}`)}
|
||||||
padding="10px"
|
>
|
||||||
gap="10px"
|
<Covers
|
||||||
>
|
data={dataArtist?.covers}
|
||||||
<Covers
|
size="35px"
|
||||||
data={dataArtist?.covers}
|
borderRadius="full"
|
||||||
iconEmpty={<LuUser size="100" height="full" />}
|
iconEmpty={<MdPerson height="full" />}
|
||||||
/>
|
/>
|
||||||
<Flex direction="column" width="80%" marginRight="auto">
|
|
||||||
<Text fontSize="24px" fontWeight="bold">
|
<Text fontSize="24px" fontWeight="bold">
|
||||||
{dataArtist?.name}
|
{dataArtist?.name}
|
||||||
</Text>
|
</Text>
|
||||||
{dataArtist?.description && (
|
</Button>
|
||||||
<Text>Description: {dataArtist?.description}</Text>
|
)}
|
||||||
)}
|
</TopBar>
|
||||||
{dataArtist?.firstName && (
|
<PageLayout>
|
||||||
<Text>first name: {dataArtist?.firstName}</Text>
|
|
||||||
)}
|
|
||||||
{dataArtist?.surname && <Text>surname: {dataArtist?.surname}</Text>}
|
|
||||||
{dataArtist?.birth && <Text>birth: {dataArtist?.birth}</Text>}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction="row"
|
direction="row"
|
||||||
width="80%"
|
width="80%"
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
|
import { Button, Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
|
||||||
import { LuUser } from 'react-icons/lu';
|
import { LuUser } from 'react-icons/lu';
|
||||||
|
import { MdGroup } from 'react-icons/md';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Covers } from '@/components/Cover';
|
import { Covers } from '@/components/Cover';
|
||||||
import { EmptyEnd } from '@/components/EmptyEnd';
|
import { EmptyEnd } from '@/components/EmptyEnd';
|
||||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||||
import { TopBar } from '@/components/TopBar/TopBar';
|
import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar';
|
||||||
import { DisplayAlbumId } from '@/components/album/DisplayAlbumId';
|
import { DisplayAlbumId } from '@/components/album/DisplayAlbumId';
|
||||||
import { useSpecificArtist } from '@/service/Artist';
|
import { useSpecificArtist } from '@/service/Artist';
|
||||||
import { useAlbumIdsOfAnArtist } from '@/service/Track';
|
import { useAlbumIdsOfAnArtist } from '@/service/Track';
|
||||||
@ -35,7 +36,18 @@ export const ArtistDetailPage = () => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar title="Artist detail" />
|
<TopBar>
|
||||||
|
<Button
|
||||||
|
{...BUTTON_TOP_BAR_PROPERTY}
|
||||||
|
marginRight="auto"
|
||||||
|
onClick={() => navigate(`/artist/all`)}
|
||||||
|
>
|
||||||
|
<MdGroup height="full" />
|
||||||
|
<Text fontSize="24px" fontWeight="bold">
|
||||||
|
Artists
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</TopBar>
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<Flex
|
<Flex
|
||||||
direction="row"
|
direction="row"
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||||
import { LuDisc3 } from 'react-icons/lu';
|
import { LuDisc3 } from 'react-icons/lu';
|
||||||
import { useParams } from 'react-router-dom';
|
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { Covers } from '@/components/Cover';
|
import { Covers } from '@/components/Cover';
|
||||||
import { EmptyEnd } from '@/components/EmptyEnd';
|
import { EmptyEnd } from '@/components/EmptyEnd';
|
||||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||||
import { TopBar } from '@/components/TopBar/TopBar';
|
import { TopBar } from '@/components/TopBar/TopBar';
|
||||||
|
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
||||||
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
||||||
|
import { DisplayTrackFull } from '@/components/track/DisplayTrackFull';
|
||||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||||
import { useSpecificGender } from '@/service/Gender';
|
import { useSpecificGender } from '@/service/Gender';
|
||||||
import { useTracksOfAGender } from '@/service/Track';
|
import { useTracksOfAGender } from '@/service/Track';
|
||||||
@ -21,6 +23,7 @@ export const GenderDetailPage = () => {
|
|||||||
const { playInList } = useActivePlaylistService();
|
const { playInList } = useActivePlaylistService();
|
||||||
const { dataGender } = useSpecificGender(genderIdInt);
|
const { dataGender } = useSpecificGender(genderIdInt);
|
||||||
const { tracksOnAGender } = useTracksOfAGender(genderIdInt);
|
const { tracksOnAGender } = useTracksOfAGender(genderIdInt);
|
||||||
|
const navigate = useNavigate();
|
||||||
const onSelectItem = (trackId: number) => {
|
const onSelectItem = (trackId: number) => {
|
||||||
//navigate(`/artist/${artistIdInt}/gender/${genderId}`);
|
//navigate(`/artist/${artistIdInt}/gender/${genderId}`);
|
||||||
let currentPlay = 0;
|
let currentPlay = 0;
|
||||||
@ -84,7 +87,7 @@ export const GenderDetailPage = () => {
|
|||||||
{tracksOnAGender?.map((data) => (
|
{tracksOnAGender?.map((data) => (
|
||||||
<Box
|
<Box
|
||||||
minWidth="100%"
|
minWidth="100%"
|
||||||
height="60px"
|
//height="60px"
|
||||||
border="1px"
|
border="1px"
|
||||||
borderColor="brand.900"
|
borderColor="brand.900"
|
||||||
backgroundColor={mode('#FFFFFF88', '#00000088')}
|
backgroundColor={mode('#FFFFFF88', '#00000088')}
|
||||||
@ -96,14 +99,26 @@ export const GenderDetailPage = () => {
|
|||||||
bgColor: mode('#FFFFFFF7', '#000000F7'),
|
bgColor: mode('#FFFFFFF7', '#000000F7'),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DisplayTrack
|
<DisplayTrackFull
|
||||||
track={data}
|
track={data}
|
||||||
onClick={() => onSelectItem(data.id)}
|
onClick={() => onSelectItem(data.id)}
|
||||||
|
contextMenu={[
|
||||||
|
{
|
||||||
|
name: 'Edit',
|
||||||
|
onClick: () => {
|
||||||
|
navigate(`/gender/${genderId}/edit/${data.id}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: 'Add Playlist', onClick: () => {} },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
<EmptyEnd />
|
<EmptyEnd />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
<Routes>
|
||||||
|
<Route path="edit/:trackId" element={<TrackEditPopUp />} />
|
||||||
|
</Routes>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ export const GenderRoutes = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="all" replace />} />
|
<Route path="/" element={<Navigate to="all" replace />} />
|
||||||
<Route path="all" element={<GendersPage />} />
|
<Route path="all" element={<GendersPage />} />
|
||||||
<Route path=":genderId" element={<GenderDetailPage />} />
|
<Route path=":genderId/*" element={<GenderDetailPage />} />
|
||||||
<Route path="*" element={<Error404 />} />
|
<Route path="*" element={<Error404 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ import { ReactElement } from 'react';
|
|||||||
|
|
||||||
import { Center, Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
|
import { Center, Flex, Text, Wrap, WrapItem } from '@chakra-ui/react';
|
||||||
import { LuCrown, LuDisc3, LuEar, LuFileAudio, LuUser } from 'react-icons/lu';
|
import { LuCrown, LuDisc3, LuEar, LuFileAudio, LuUser } from 'react-icons/lu';
|
||||||
|
import { MdGroup } from 'react-icons/md';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||||
@ -24,7 +25,7 @@ const homeList: HomeListType[] = [
|
|||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Artists',
|
name: 'Artists',
|
||||||
icon: <LuUser size="60%" height="full" />,
|
icon: <MdGroup size="60%" height="full" />,
|
||||||
to: 'artist',
|
to: 'artist',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user