feat: modules, products, services, services kits
This commit is contained in:
108
src/components/selects/ObjectMultiSelect/ObjectMultiSelect.tsx
Normal file
108
src/components/selects/ObjectMultiSelect/ObjectMultiSelect.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { groupBy, omit } from "lodash";
|
||||
import { MultiSelect, MultiSelectProps } from "@mantine/core";
|
||||
|
||||
interface ObjectWithIdAndName {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type MultiselectObjectType<T> = T;
|
||||
|
||||
type ControlledValueProps<T> = {
|
||||
value: MultiselectObjectType<T>[];
|
||||
onChange: (value: MultiselectObjectType<T>[]) => void;
|
||||
};
|
||||
|
||||
type CustomLabelAndKeyProps<T> = {
|
||||
getLabelFn: (item: MultiselectObjectType<T>) => string;
|
||||
getValueFn: (item: MultiselectObjectType<T>) => string;
|
||||
};
|
||||
type RestProps<T> = {
|
||||
defaultValue?: MultiselectObjectType<T>[];
|
||||
onChange: (value: MultiselectObjectType<T>[]) => void;
|
||||
data: MultiselectObjectType<T>[];
|
||||
groupBy?: (item: MultiselectObjectType<T>) => string;
|
||||
filterBy?: (item: MultiselectObjectType<T>) => boolean;
|
||||
};
|
||||
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
||||
return item.name;
|
||||
};
|
||||
|
||||
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||
return item.id.toString();
|
||||
};
|
||||
export type ObjectMultiSelectProps<T> = (RestProps<T> &
|
||||
Partial<ControlledValueProps<T>>) &
|
||||
Omit<MultiSelectProps, "value" | "onChange" | "data"> &
|
||||
(T extends ObjectWithIdAndName
|
||||
? Partial<CustomLabelAndKeyProps<T>>
|
||||
: CustomLabelAndKeyProps<T>);
|
||||
|
||||
const ObjectMultiSelect = <T,>(props: ObjectMultiSelectProps<T>) => {
|
||||
const isControlled = "value" in props;
|
||||
const haveGetValueFn = "getValueFn" in props;
|
||||
const haveGetLabelFn = "getLabelFn" in props;
|
||||
|
||||
const [internalValue, setInternalValue] = useState<
|
||||
MultiselectObjectType<T>[] | undefined
|
||||
>(props.defaultValue);
|
||||
|
||||
const value = (isControlled ? props.value : internalValue) || [];
|
||||
|
||||
const getValueFn =
|
||||
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||
const getLabelFn =
|
||||
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||
|
||||
const data = useMemo(() => {
|
||||
const propsData = props.filterBy
|
||||
? props.data.filter(props.filterBy)
|
||||
: props.data;
|
||||
if (props.groupBy) {
|
||||
const groupedData = groupBy(propsData, props.groupBy);
|
||||
return Object.entries(groupedData).map(([group, items]) => ({
|
||||
group,
|
||||
items: items.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item),
|
||||
})),
|
||||
}));
|
||||
}
|
||||
return propsData.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item),
|
||||
}));
|
||||
}, [props.data, props.groupBy]);
|
||||
|
||||
const handleOnChange = (event: string[]) => {
|
||||
const objects = props.data.filter(item =>
|
||||
event.includes(getValueFn(item))
|
||||
);
|
||||
if (isControlled) {
|
||||
props.onChange(objects);
|
||||
return;
|
||||
}
|
||||
setInternalValue(objects);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled || !internalValue) return;
|
||||
props.onChange(internalValue);
|
||||
}, [internalValue]);
|
||||
|
||||
const restProps = omit(props, "getValueFn", "getLabelFn", "filterBy");
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
{...restProps}
|
||||
value={value.map(item => getValueFn(item))}
|
||||
onChange={handleOnChange}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectMultiSelect;
|
||||
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { groupBy, omit } from "lodash";
|
||||
import { Select, SelectProps } from "@mantine/core";
|
||||
@ -33,6 +35,7 @@ const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||
if (!item) return item;
|
||||
return item.id.toString();
|
||||
};
|
||||
|
||||
export type ObjectSelectProps<T> = (RestProps<T> &
|
||||
Partial<ControlledValueProps<T>>) &
|
||||
Omit<SelectProps, "value" | "onChange" | "data"> &
|
||||
|
||||
@ -4,11 +4,10 @@ import { DataTable, DataTableProps } from "mantine-datatable";
|
||||
function BaseTable<T>(props: DataTableProps<T>) {
|
||||
return (
|
||||
<DataTable
|
||||
withTableBorder={false}
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
striped={false}
|
||||
verticalAlign={"center"}
|
||||
borderRadius={"lg"}
|
||||
backgroundColor={"transparent"}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
119
src/components/ui/ImageDropzone/ImageDropzone.tsx
Normal file
119
src/components/ui/ImageDropzone/ImageDropzone.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { FC } from "react";
|
||||
import { IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||
import { omit } from "lodash";
|
||||
import {
|
||||
Button,
|
||||
Fieldset,
|
||||
Flex,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
rem,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone";
|
||||
import UseImageDropzone from "./types";
|
||||
|
||||
interface RestProps {
|
||||
imageDropzone: UseImageDropzone;
|
||||
onDrop: (files: FileWithPath[]) => void;
|
||||
}
|
||||
|
||||
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
|
||||
|
||||
const ImageDropzone: FC<Props> = (props: Props) => {
|
||||
const { showDropzone, setShowDropzone, isLoading, imageUrlInputProps } =
|
||||
props.imageDropzone;
|
||||
|
||||
const restProps = omit(props, ["imageDropzone"]);
|
||||
|
||||
const getDropzone = () => (
|
||||
<Dropzone
|
||||
{...restProps}
|
||||
accept={[
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
"image/x-icon",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
"image/heic",
|
||||
]}
|
||||
multiple={false}
|
||||
onDrop={props.onDrop}>
|
||||
<Group
|
||||
justify="center"
|
||||
gap="xl"
|
||||
style={{ pointerEvents: "none" }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload
|
||||
style={{
|
||||
width: rem(52),
|
||||
height: rem(52),
|
||||
color: "var(--mantine-color-blue-6)",
|
||||
}}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX
|
||||
style={{
|
||||
width: rem(52),
|
||||
height: rem(52),
|
||||
color: "var(--mantine-color-red-6)",
|
||||
}}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto
|
||||
style={{
|
||||
width: rem(52),
|
||||
height: rem(52),
|
||||
color: "var(--mantine-color-dimmed)",
|
||||
}}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Text
|
||||
size="xl"
|
||||
inline>
|
||||
Перенесите изображение или нажмите чтоб выбрать файл
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
);
|
||||
|
||||
const getBody = () => {
|
||||
if (imageUrlInputProps?.value && !showDropzone) {
|
||||
return <Image src={imageUrlInputProps.value} />;
|
||||
}
|
||||
return getDropzone();
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
direction={"column"}>
|
||||
<Fieldset legend={"Изображение"}>
|
||||
<Flex justify={"center"}>
|
||||
{isLoading ? <Loader /> : getBody()}
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
{!showDropzone && (
|
||||
<Button
|
||||
onClick={() => setShowDropzone(true)}
|
||||
variant={"default"}>
|
||||
Заменить изображение {}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageDropzone;
|
||||
12
src/components/ui/ImageDropzone/types.ts
Normal file
12
src/components/ui/ImageDropzone/types.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import BaseFormInputProps from "@/utils/baseFormInputProps";
|
||||
|
||||
type UseImageDropzone = {
|
||||
showDropzone: boolean;
|
||||
setShowDropzone: Dispatch<SetStateAction<boolean>>;
|
||||
isLoading: boolean;
|
||||
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||
imageUrlInputProps?: BaseFormInputProps<string>;
|
||||
};
|
||||
|
||||
export default UseImageDropzone;
|
||||
Reference in New Issue
Block a user