Work on form and basic tools I need
This commit is contained in:
parent
873f0acff5
commit
ec93c31acb
@ -53,7 +53,7 @@
|
||||
"react-popper": "2.3.0",
|
||||
"react-router-dom": "6.26.1",
|
||||
"react-select": "5.8.0",
|
||||
"react-simple-keyboard": "3.7.147",
|
||||
"react-simple-keyboard": "3.7.148",
|
||||
"react-sticky-el": "2.1.1",
|
||||
"react-use": "17.5.1",
|
||||
"react-use-draggable-scroll": "0.4.7",
|
||||
@ -74,16 +74,16 @@
|
||||
"@storybook/react-vite": "8.2.9",
|
||||
"@storybook/theming": "8.2.9",
|
||||
"@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",
|
||||
"@trivago/prettier-plugin-sort-imports": "4.3.0",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/node": "22.5.0",
|
||||
"@types/react": "18.3.4",
|
||||
"@types/node": "22.5.1",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/react-sticky-el": "1.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "8.2.0",
|
||||
"@typescript-eslint/parser": "8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.3.0",
|
||||
"@typescript-eslint/parser": "8.3.0",
|
||||
"@vitejs/plugin-react": "4.3.1",
|
||||
"eslint": "9.9.1",
|
||||
"eslint-plugin-codeceptjs": "1.3.0",
|
||||
@ -93,16 +93,16 @@
|
||||
"eslint-plugin-storybook": "0.8.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"knip": "5.27.4",
|
||||
"knip": "5.27.5",
|
||||
"lint-staged": "15.2.9",
|
||||
"npm-check-updates": "^17.1.0",
|
||||
"prettier": "3.3.3",
|
||||
"puppeteer": "23.1.1",
|
||||
"puppeteer": "23.2.1",
|
||||
"react-is": "18.3.1",
|
||||
"storybook": "8.2.9",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.5.4",
|
||||
"vite": "5.4.2",
|
||||
"vitest": "2.0.5",
|
||||
"npm-check-updates": "^17.1.0"
|
||||
"vitest": "2.0.5"
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -32,12 +32,6 @@ export const SearchInput = ({
|
||||
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;
|
||||
@ -57,8 +51,8 @@ export const SearchInput = ({
|
||||
<MdSearch color="gray.300" />
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
ref={ref}
|
||||
onFocus={onFocusKeep}
|
||||
onBlur={() => setTimeout(() => onFocusLost(), 200)}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
|
@ -41,6 +41,11 @@ import { useThemeMode } from '@/utils/theme-tools';
|
||||
|
||||
export const TOP_BAR_HEIGHT = '50px';
|
||||
|
||||
export const BUTTON_TOP_BAR_PROPERTY = {
|
||||
variant: '@menu',
|
||||
height: TOP_BAR_HEIGHT,
|
||||
};
|
||||
|
||||
export type TopBarProps = {
|
||||
children?: ReactNode;
|
||||
title?: string;
|
||||
@ -48,10 +53,7 @@ export type TopBarProps = {
|
||||
|
||||
export const TopBar = ({ title, children }: TopBarProps) => {
|
||||
const { mode, colorMode, toggleColorMode } = useThemeMode();
|
||||
const buttonProperty = {
|
||||
variant: '@menu',
|
||||
height: TOP_BAR_HEIGHT,
|
||||
};
|
||||
|
||||
const { session } = useServiceContext();
|
||||
const backColor = mode('back.100', 'back.800');
|
||||
const drawerDisclose = useDisclosure();
|
||||
@ -86,7 +88,7 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
||||
boxShadow={'0px 2px 4px ' + colors.back[900]}
|
||||
zIndex={200}
|
||||
>
|
||||
<Button {...buttonProperty} onClick={onChangeTheme}>
|
||||
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onChangeTheme}>
|
||||
<LuAlignJustify />
|
||||
<Text paddingLeft="3px" fontWeight="bold">
|
||||
Karusic
|
||||
@ -107,13 +109,17 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
||||
<Flex right="0">
|
||||
{session?.state !== SessionState.CONNECTED && (
|
||||
<>
|
||||
<Button {...buttonProperty} onClick={onSignIn}>
|
||||
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onSignIn}>
|
||||
<LuLogIn />
|
||||
<Text paddingLeft="3px" fontWeight="bold">
|
||||
Sign-in
|
||||
</Text>
|
||||
</Button>
|
||||
<Button {...buttonProperty} onClick={onSignUp} disabled={true}>
|
||||
<Button
|
||||
{...BUTTON_TOP_BAR_PROPERTY}
|
||||
onClick={onSignUp}
|
||||
disabled={true}
|
||||
>
|
||||
<LuPlusCircle />
|
||||
<Text paddingLeft="3px" fontWeight="bold">
|
||||
Sign-up
|
||||
@ -127,14 +133,13 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
||||
as={IconButton}
|
||||
aria-label="Options"
|
||||
icon={<LuUserCircle />}
|
||||
{...buttonProperty}
|
||||
{...BUTTON_TOP_BAR_PROPERTY}
|
||||
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} />}>
|
||||
@ -176,16 +181,30 @@ export const TopBar = ({ title, children }: TopBarProps) => {
|
||||
</Text>
|
||||
</HStack>
|
||||
</DrawerHeader>
|
||||
<DrawerBody>
|
||||
<Button {...buttonProperty} onClick={onSelectHome} width="fill">
|
||||
<DrawerBody paddingX="0px">
|
||||
<Button
|
||||
background="#00000000"
|
||||
borderRadius="0px"
|
||||
onClick={onSelectHome}
|
||||
width="full"
|
||||
>
|
||||
<LuHome />
|
||||
<Text paddingLeft="3px" fontWeight="bold">
|
||||
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
|
||||
Home
|
||||
</Text>
|
||||
</Button>
|
||||
<p>Some contents...</p>
|
||||
<p>Some contents...</p>
|
||||
<p>Some contents...</p>
|
||||
<hr />
|
||||
<Button
|
||||
background="#00000000"
|
||||
borderRadius="0px"
|
||||
onClick={onSelectHome}
|
||||
width="full"
|
||||
>
|
||||
<LuArrowUpSquare />
|
||||
<Text paddingLeft="3px" fontWeight="bold" marginRight="auto">
|
||||
Add Media
|
||||
</Text>
|
||||
</Button>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</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 { Covers } from '@/components/Cover';
|
||||
import { ContextMenu, MenuElement } from '@/components/contextMenu/ContextMenu';
|
||||
import { DisplayTrackSkeleton } from '@/components/track/DisplayTrackSkeleton';
|
||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||
import { useSpecificAlbum } from '@/service/Album';
|
||||
@ -14,8 +15,13 @@ import { useSpecificGender } from '@/service/Gender';
|
||||
export type DisplayTrackProps = {
|
||||
track: Track;
|
||||
onClick?: () => void;
|
||||
contextMenu?: MenuElement[];
|
||||
};
|
||||
export const DisplayTrackFull = ({ track, onClick }: DisplayTrackProps) => {
|
||||
export const DisplayTrackFull = ({
|
||||
track,
|
||||
onClick,
|
||||
contextMenu,
|
||||
}: DisplayTrackProps) => {
|
||||
const { trackActive } = useActivePlaylistService();
|
||||
const { dataAlbum } = useSpecificAlbum(track?.albumId);
|
||||
const { dataGender } = useSpecificGender(track?.genderId);
|
||||
@ -106,6 +112,7 @@ export const DisplayTrackFull = ({ track, onClick }: DisplayTrackProps) => {
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<ContextMenu elements={contextMenu} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
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 { EmptyEnd } from '@/components/EmptyEnd';
|
||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
import { TopBar } from '@/components/TopBar/TopBar';
|
||||
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
||||
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
||||
import { DisplayTrackFull } from '@/components/track/DisplayTrackFull';
|
||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||
import { useSpecificAlbum } from '@/service/Album';
|
||||
import { useTracksOfAnAlbum } from '@/service/Track';
|
||||
@ -20,6 +22,7 @@ export const AlbumDetailPage = () => {
|
||||
const { playInList } = useActivePlaylistService();
|
||||
const { dataAlbum } = useSpecificAlbum(albumIdInt);
|
||||
const { tracksOnAnAlbum } = useTracksOfAnAlbum(albumIdInt);
|
||||
const navigate = useNavigate();
|
||||
const onSelectItem = (trackId: number) => {
|
||||
//navigate(`/artist/${artistIdInt}/album/${albumId}`);
|
||||
let currentPlay = 0;
|
||||
@ -86,7 +89,7 @@ export const AlbumDetailPage = () => {
|
||||
{tracksOnAnAlbum?.map((data) => (
|
||||
<Box
|
||||
minWidth="100%"
|
||||
height="60px"
|
||||
//height="60px"
|
||||
border="1px"
|
||||
borderColor="brand.900"
|
||||
backgroundColor={mode('#FFFFFF88', '#00000088')}
|
||||
@ -98,14 +101,26 @@ export const AlbumDetailPage = () => {
|
||||
bgColor: mode('#FFFFFFF7', '#000000F7'),
|
||||
}}
|
||||
>
|
||||
<DisplayTrack
|
||||
<DisplayTrackFull
|
||||
track={data}
|
||||
onClick={() => onSelectItem(data.id)}
|
||||
contextMenu={[
|
||||
{
|
||||
name: 'Edit',
|
||||
onClick: () => {
|
||||
navigate(`/album/${albumId}/edit/${data.id}`);
|
||||
},
|
||||
},
|
||||
{ name: 'Add Playlist', onClick: () => {} },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
<EmptyEnd />
|
||||
</Flex>
|
||||
<Routes>
|
||||
<Route path="edit/:trackId" element={<TrackEditPopUp />} />
|
||||
</Routes>
|
||||
</PageLayout>
|
||||
</>
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ export const AlbumRoutes = () => {
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="all" replace />} />
|
||||
<Route path="all" element={<AlbumsPage />} />
|
||||
<Route path=":albumId" element={<AlbumDetailPage />} />
|
||||
<Route path=":albumId/*" element={<AlbumDetailPage />} />
|
||||
<Route path="*" element={<Error404 />} />
|
||||
</Routes>
|
||||
);
|
||||
|
@ -1,17 +1,6 @@
|
||||
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 { Wrap, WrapItem } from '@chakra-ui/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { EmptyEnd } from '@/components/EmptyEnd';
|
||||
@ -20,7 +9,7 @@ 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, useOrderedAlbums } from '@/service/Album';
|
||||
import { useOrderedAlbums } from '@/service/Album';
|
||||
import { useThemeMode } from '@/utils/theme-tools';
|
||||
|
||||
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 { MdPerson } from 'react-icons/md';
|
||||
import { Route, Routes, 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';
|
||||
import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar';
|
||||
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
||||
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||
@ -54,34 +55,26 @@ export const ArtistAlbumDetailPage = () => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<TopBar title="Album detail" />
|
||||
<PageLayout>
|
||||
<Flex
|
||||
direction="row"
|
||||
width="80%"
|
||||
marginX="auto"
|
||||
padding="10px"
|
||||
gap="10px"
|
||||
>
|
||||
<Covers
|
||||
data={dataArtist?.covers}
|
||||
iconEmpty={<LuUser size="100" height="full" />}
|
||||
/>
|
||||
<Flex direction="column" width="80%" marginRight="auto">
|
||||
<TopBar title={dataArtist ? undefined : 'Album detail'}>
|
||||
{dataArtist && (
|
||||
<Button
|
||||
{...BUTTON_TOP_BAR_PROPERTY}
|
||||
marginRight="auto"
|
||||
onClick={() => navigate(`/artist/${dataArtist.id}`)}
|
||||
>
|
||||
<Covers
|
||||
data={dataArtist?.covers}
|
||||
size="35px"
|
||||
borderRadius="full"
|
||||
iconEmpty={<MdPerson height="full" />}
|
||||
/>
|
||||
<Text fontSize="24px" fontWeight="bold">
|
||||
{dataArtist?.name}
|
||||
</Text>
|
||||
{dataArtist?.description && (
|
||||
<Text>Description: {dataArtist?.description}</Text>
|
||||
)}
|
||||
{dataArtist?.firstName && (
|
||||
<Text>first name: {dataArtist?.firstName}</Text>
|
||||
)}
|
||||
{dataArtist?.surname && <Text>surname: {dataArtist?.surname}</Text>}
|
||||
{dataArtist?.birth && <Text>birth: {dataArtist?.birth}</Text>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
</Button>
|
||||
)}
|
||||
</TopBar>
|
||||
<PageLayout>
|
||||
<Flex
|
||||
direction="row"
|
||||
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 { MdGroup } from 'react-icons/md';
|
||||
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';
|
||||
import { BUTTON_TOP_BAR_PROPERTY, TopBar } from '@/components/TopBar/TopBar';
|
||||
import { DisplayAlbumId } from '@/components/album/DisplayAlbumId';
|
||||
import { useSpecificArtist } from '@/service/Artist';
|
||||
import { useAlbumIdsOfAnArtist } from '@/service/Track';
|
||||
@ -35,7 +36,18 @@ export const ArtistDetailPage = () => {
|
||||
}
|
||||
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>
|
||||
<Flex
|
||||
direction="row"
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
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 { EmptyEnd } from '@/components/EmptyEnd';
|
||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
import { TopBar } from '@/components/TopBar/TopBar';
|
||||
import { TrackEditPopUp } from '@/components/popup/TrackEditPopUp';
|
||||
import { DisplayTrack } from '@/components/track/DisplayTrack';
|
||||
import { DisplayTrackFull } from '@/components/track/DisplayTrackFull';
|
||||
import { useActivePlaylistService } from '@/service/ActivePlaylist';
|
||||
import { useSpecificGender } from '@/service/Gender';
|
||||
import { useTracksOfAGender } from '@/service/Track';
|
||||
@ -21,6 +23,7 @@ export const GenderDetailPage = () => {
|
||||
const { playInList } = useActivePlaylistService();
|
||||
const { dataGender } = useSpecificGender(genderIdInt);
|
||||
const { tracksOnAGender } = useTracksOfAGender(genderIdInt);
|
||||
const navigate = useNavigate();
|
||||
const onSelectItem = (trackId: number) => {
|
||||
//navigate(`/artist/${artistIdInt}/gender/${genderId}`);
|
||||
let currentPlay = 0;
|
||||
@ -84,7 +87,7 @@ export const GenderDetailPage = () => {
|
||||
{tracksOnAGender?.map((data) => (
|
||||
<Box
|
||||
minWidth="100%"
|
||||
height="60px"
|
||||
//height="60px"
|
||||
border="1px"
|
||||
borderColor="brand.900"
|
||||
backgroundColor={mode('#FFFFFF88', '#00000088')}
|
||||
@ -96,14 +99,26 @@ export const GenderDetailPage = () => {
|
||||
bgColor: mode('#FFFFFFF7', '#000000F7'),
|
||||
}}
|
||||
>
|
||||
<DisplayTrack
|
||||
<DisplayTrackFull
|
||||
track={data}
|
||||
onClick={() => onSelectItem(data.id)}
|
||||
contextMenu={[
|
||||
{
|
||||
name: 'Edit',
|
||||
onClick: () => {
|
||||
navigate(`/gender/${genderId}/edit/${data.id}`);
|
||||
},
|
||||
},
|
||||
{ name: 'Add Playlist', onClick: () => {} },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
<EmptyEnd />
|
||||
</Flex>
|
||||
<Routes>
|
||||
<Route path="edit/:trackId" element={<TrackEditPopUp />} />
|
||||
</Routes>
|
||||
</PageLayout>
|
||||
</>
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ export const GenderRoutes = () => {
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="all" replace />} />
|
||||
<Route path="all" element={<GendersPage />} />
|
||||
<Route path=":genderId" element={<GenderDetailPage />} />
|
||||
<Route path=":genderId/*" element={<GenderDetailPage />} />
|
||||
<Route path="*" element={<Error404 />} />
|
||||
</Routes>
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ 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 { MdGroup } from 'react-icons/md';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { PageLayout } from '@/components/Layout/PageLayout';
|
||||
@ -24,7 +25,7 @@ const homeList: HomeListType[] = [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Artists',
|
||||
icon: <LuUser size="60%" height="full" />,
|
||||
icon: <MdGroup size="60%" height="full" />,
|
||||
to: 'artist',
|
||||
},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user