123
This commit is contained in:
8
components/BaseTable/BaseTable.tsx
Normal file
8
components/BaseTable/BaseTable.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { DataTable, type DataTableProps } from 'mantine-datatable';
|
||||
|
||||
// важна стрелочная форма для корректного дженерика в JSX
|
||||
const BaseTable = <T,>(props: DataTableProps<T>) => {
|
||||
return <DataTable withRowBorders withColumnBorders withTableBorder {...props} />;
|
||||
};
|
||||
|
||||
export default BaseTable;
|
||||
5
components/BaseTable/helpers.ts
Normal file
5
components/BaseTable/helpers.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// helpers.ts (или рядом)
|
||||
import type { DataTableProps } from 'mantine-datatable';
|
||||
|
||||
// Берём ИМЕННО ту ветку union, где есть `columns`
|
||||
export type ColumnsMode<T> = Extract<DataTableProps<T>, { columns: unknown }>;
|
||||
13
components/ColorSchemeToggle/ColorSchemeToggle.tsx
Normal file
13
components/ColorSchemeToggle/ColorSchemeToggle.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Button, Group, useMantineColorScheme } from '@mantine/core';
|
||||
|
||||
export function ColorSchemeToggle() {
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
|
||||
return (
|
||||
<Group justify="center" mt="xl">
|
||||
<Button onClick={() => setColorScheme('light')}>Light</Button>
|
||||
<Button onClick={() => setColorScheme('dark')}>Dark</Button>
|
||||
<Button onClick={() => setColorScheme('auto')}>Auto</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
45
components/Forms/AuthenticationForm.tsx
Normal file
45
components/Forms/AuthenticationForm.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Group,
|
||||
Paper,
|
||||
PaperProps,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { upperFirst, useToggle } from '@mantine/hooks';
|
||||
import { LogidexButton } from '@/components/Forms/LogidexButton';
|
||||
|
||||
export function AuthenticationForm(props: PaperProps) {
|
||||
const [type, toggle] = useToggle(['login', 'register']);
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
terms: true,
|
||||
},
|
||||
|
||||
validate: {
|
||||
email: (val) => (/^\S+@\S+$/.test(val) ? null : 'Invalid email'),
|
||||
password: (val) => (val.length <= 6 ? 'Password should include at least 6 characters' : null),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Paper radius="md" p="lg" withBorder {...props}>
|
||||
<Text size="lg" fw={500} ta="center">
|
||||
Добро пожаловать в Logidex
|
||||
</Text>
|
||||
|
||||
<Group grow mb="md" mt="md">
|
||||
<LogidexButton radius="xl">Войти с помощью LogidexID</LogidexButton>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
29
components/Forms/LogidexButton.tsx
Normal file
29
components/Forms/LogidexButton.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { Button, ButtonProps } from '@mantine/core';
|
||||
|
||||
function LogidexIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 685 789"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ width: 24, height: 24 }}
|
||||
>
|
||||
<path
|
||||
d="M0 561.604V230.18C0 212.417 9.42351 195.988 24.7558 187.02L319.089 14.863C334.772 5.69023 354.196 5.74604 369.825 15.0088L659.992 186.975C675.184 195.979 684.5 212.329 684.5 229.989V561.794C684.5 579.57 675.061 596.011 659.709 604.973L369.544 774.378C354.056 783.421 334.912 783.475 319.373 774.522L25.0375 604.927C9.5463 596.001 0 579.482 0 561.604Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path
|
||||
d="M62.5 525.821V265.415C62.5 247.647 71.9298 231.213 87.2702 222.247L318.944 86.8432C334.62 77.6817 354.029 77.7374 369.651 86.9889L597.977 222.202C613.178 231.204 622.5 247.558 622.5 265.224V526.011C622.5 543.793 613.055 560.238 597.695 569.199L369.371 702.398C353.89 711.429 334.76 711.484 319.228 702.541L87.5518 569.152C72.0526 560.228 62.5 543.705 62.5 525.821Z"
|
||||
stroke="white"
|
||||
stroke-width="35"
|
||||
/>
|
||||
<path
|
||||
d="M503.632 320.536C504.247 320.201 505 320.626 505 321.326V468.944C505 480.497 498.866 491.181 488.891 497.008L359.844 572.379C359.452 572.608 359.057 572.828 358.658 573.039C355.715 574.597 352.5 572.205 352.5 568.876V405.905C352.5 404.075 353.5 402.391 355.107 401.515L399.607 377.253C402.938 375.436 407 377.848 407 381.643V417.451C407 418.36 408.115 418.797 408.733 418.131L430.309 394.863C431.643 393.424 433.695 392.898 435.557 393.519L453.684 399.562C454.331 399.777 455 399.295 455 398.612V350.02C455 348.19 456 346.506 457.607 345.63L503.632 320.536ZM329.908 401.489C331.507 402.367 332.5 404.047 332.5 405.871V569.09C332.5 571.926 329.68 573.888 327.223 572.472L196.27 496.987C186.36 491.275 180.195 480.783 180.005 469.374L180 468.83V321.441C180 320.376 181.146 319.748 182.079 320.261L329.908 401.489ZM403 481.499C398.858 481.499 395.5 484.857 395.5 488.999V521C395.5 525.142 398.858 528.5 403 528.5C407.142 528.5 410.5 525.142 410.5 521V488.999C410.5 484.857 407.142 481.499 403 481.499ZM435.5 465.5C431.358 465.5 428 468.858 428 473V505.001C428 509.143 431.358 512.501 435.5 512.501C439.642 512.501 443 509.143 443 505.001V473C443 468.858 439.642 465.5 435.5 465.5ZM465.5 449.501C461.358 449.501 458 452.859 458 457.001V489.002C458 493.144 461.358 496.502 465.5 496.502C469.642 496.502 473 493.144 473 489.002V457.001C473 452.859 469.642 449.501 465.5 449.501ZM405.733 346.78C406.516 347.224 407 348.055 407 348.956C407 349.872 406.5 350.714 405.696 351.152L344.92 384.289C343.423 385.105 341.613 385.102 340.119 384.281L192.992 303.438C190.226 301.918 189.541 298.221 191.989 296.23C193.269 295.189 194.637 294.239 196.086 293.391L250.472 261.566C252.012 260.666 253.914 260.653 255.465 261.533L405.733 346.78ZM327.038 216.766C337.234 210.8 349.865 210.836 360.026 216.86L489.074 293.37C490.705 294.337 492.231 295.433 493.643 296.643C496.011 298.671 495.287 302.306 492.55 303.798L455.407 324.048C453.898 324.871 452.072 324.861 450.572 324.021L301.361 240.518C297.988 238.63 297.941 233.791 301.278 231.839L327.038 216.766Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function LogidexButton(props: ButtonProps & React.ComponentPropsWithoutRef<'button'>) {
|
||||
return <Button leftSection={<LogidexIcon />} variant="default" {...props} />;
|
||||
}
|
||||
41
components/Layout/Header/Header.tsx
Normal file
41
components/Layout/Header/Header.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { IconSun } from '@tabler/icons-react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Burger,
|
||||
Flex,
|
||||
Group, Image,
|
||||
MantineColorScheme,
|
||||
useMantineColorScheme,
|
||||
} from '@mantine/core';
|
||||
import { useToggle } from '@mantine/hooks';
|
||||
|
||||
type HeaderProps = {
|
||||
toggleMobile: () => void;
|
||||
toggleDesktop: () => void;
|
||||
|
||||
mobileOpened: boolean;
|
||||
desktopOpened: boolean;
|
||||
};
|
||||
const Header = ({ toggleMobile, toggleDesktop, mobileOpened, desktopOpened }: HeaderProps) => {
|
||||
const { setColorScheme } = useMantineColorScheme();
|
||||
const [theme, toggle] = useToggle(['light', 'dark']);
|
||||
const onChangeThemeClick = () => {
|
||||
toggle();
|
||||
setColorScheme(theme as MantineColorScheme);
|
||||
};
|
||||
return (
|
||||
<Flex align="center" justify="space-between" h="100%">
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={mobileOpened} onClick={toggleMobile} hiddenFrom="sm" size="sm" />
|
||||
<Burger opened={desktopOpened} onClick={toggleDesktop} visibleFrom="sm" size="sm" />
|
||||
</Group>
|
||||
к <Group h="100%" px="md">
|
||||
<ActionIcon onClick={() => onChangeThemeClick()} variant="default">
|
||||
<IconSun />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
63
components/Layout/Navbar/Navbar.module.css
Normal file
63
components/Layout/Navbar/Navbar.module.css
Normal file
@ -0,0 +1,63 @@
|
||||
.navbar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.navbarMain {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-bottom: var(--mantine-spacing-md);
|
||||
margin-bottom: calc(var(--mantine-spacing-md) * 1.5);
|
||||
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: var(--mantine-spacing-md);
|
||||
margin-top: var(--mantine-spacing-md);
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm);
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
font-weight: 500;
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
|
||||
.linkIcon {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
}
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
&,
|
||||
&:hover {
|
||||
background-color: var(--mantine-color-blue-light);
|
||||
color: var(--mantine-color-blue-light-color);
|
||||
|
||||
.linkIcon {
|
||||
color: var(--mantine-color-blue-light-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
|
||||
margin-right: var(--mantine-spacing-sm);
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
58
components/Layout/Navbar/Navbar.tsx
Normal file
58
components/Layout/Navbar/Navbar.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
IconBox,
|
||||
IconBriefcase,
|
||||
IconCheck,
|
||||
IconLogout,
|
||||
IconShoppingBag,
|
||||
} from '@tabler/icons-react';
|
||||
import { NavbarLink, NavbarLinkKey } from '@/components/Layout/Navbar/types';
|
||||
import classes from './Navbar.module.css';
|
||||
|
||||
const data: NavbarLink[] = [
|
||||
{ href: '/products', key: 'products', label: 'Товары', icon: IconBox },
|
||||
{ href: '/marketplaces', key: 'marketplaces', label: 'Маркетплейсы', icon: IconShoppingBag },
|
||||
{ href: '/legal-entities', key: 'legal-entities', label: 'Юр. лица', icon: IconBriefcase },
|
||||
{ href: '/deal-requests', key: 'deal-requests', label: 'Заявки на сделку', icon: IconCheck },
|
||||
];
|
||||
const hrefToKeyMap: Record<string, NavbarLinkKey> = data.reduce(
|
||||
(acc, item) => ({ ...acc, [`${item.href}`]: `${item.key}` }),
|
||||
{ '/': 'index' }
|
||||
);
|
||||
export function Navbar() {
|
||||
const router = useRouter();
|
||||
const currentPath = router.pathname;
|
||||
const active = hrefToKeyMap[currentPath];
|
||||
const links = data.map((item) => {
|
||||
return (
|
||||
<a
|
||||
className={classes.link}
|
||||
data-active={item.key === active || undefined}
|
||||
href={item.href}
|
||||
key={item.label}
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
await router.push(item.href);
|
||||
}}
|
||||
>
|
||||
<item.icon className={classes.linkIcon} stroke={1.5} />
|
||||
<span>{item.label}</span>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<nav className={classes.navbar}>
|
||||
<div className={classes.navbarMain}>{links}</div>
|
||||
|
||||
<div className={classes.footer}>
|
||||
<a href="#" className={classes.link} onClick={(event) => event.preventDefault()}>
|
||||
<IconLogout className={classes.linkIcon} stroke={1.5} />
|
||||
<span>Выйти</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navbar;
|
||||
11
components/Layout/Navbar/types.ts
Normal file
11
components/Layout/Navbar/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import * as react from 'react';
|
||||
import { Icon, IconProps } from '@tabler/icons-react';
|
||||
|
||||
|
||||
export type NavbarLinkKey = 'index' | 'products' | 'marketplaces' | 'legal-entities' | 'deal-requests';
|
||||
export type NavbarLink = {
|
||||
href: string;
|
||||
key: NavbarLinkKey;
|
||||
label: string;
|
||||
icon: react.ForwardRefExoticComponent<IconProps & react.RefAttributes<Icon>>;
|
||||
};
|
||||
2
components/Modals/BaseFormModal/BaseFormModal
Normal file
2
components/Modals/BaseFormModal/BaseFormModal
Normal file
@ -0,0 +1,2 @@
|
||||
type Props = {
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { Checkbox, CheckboxProps } from '@mantine/core';
|
||||
|
||||
type Props = CheckboxProps;
|
||||
|
||||
const BooleanField = (props: Props) => {
|
||||
return <Checkbox {...props} />;
|
||||
};
|
||||
export default BooleanField;
|
||||
9
components/ObjectForm/Fields/NumberField/NumberField.tsx
Normal file
9
components/ObjectForm/Fields/NumberField/NumberField.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { NumberInput, NumberInputProps } from '@mantine/core';
|
||||
|
||||
type Props = NumberInputProps;
|
||||
|
||||
const NumberField = (props: Props) => {
|
||||
return <NumberInput hideControls {...props} />;
|
||||
};
|
||||
|
||||
export default NumberField;
|
||||
9
components/ObjectForm/Fields/StringField/StringField.tsx
Normal file
9
components/ObjectForm/Fields/StringField/StringField.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { TextInput, TextInputProps } from '@mantine/core';
|
||||
|
||||
type Props = TextInputProps;
|
||||
|
||||
const StringField = (props: Props) => {
|
||||
return <TextInput {...props} />;
|
||||
};
|
||||
|
||||
export default StringField
|
||||
74
components/ObjectForm/ObjectForm.tsx
Normal file
74
components/ObjectForm/ObjectForm.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { ZodObject } from 'zod';
|
||||
import { Flex, rem } from '@mantine/core';
|
||||
import BooleanField from '@/components/ObjectForm/Fields/BooleanField/BooleanField';
|
||||
import NumberField from '@/components/ObjectForm/Fields/NumberField/NumberField';
|
||||
import StringField from '@/components/ObjectForm/Fields/StringField/StringField';
|
||||
|
||||
type EditProps<T> = {
|
||||
object: T;
|
||||
onChange: (item: T) => void;
|
||||
};
|
||||
type CreateProps<T> = {
|
||||
onCreate: (item: T) => void;
|
||||
};
|
||||
type SchemaLabels<S extends ZodObject<any>> = Partial<Record<keyof S['shape'], string>>;
|
||||
type RestProps<S extends ZodObject<any>> =
|
||||
| {
|
||||
schema: S;
|
||||
labels?: SchemaLabels<S>;
|
||||
excludeKeys?: (keyof S['shape'])[];
|
||||
includeKeys?: never;
|
||||
}
|
||||
| {
|
||||
schema: S;
|
||||
labels?: SchemaLabels<S>;
|
||||
includeKeys?: (keyof S['shape'])[];
|
||||
excludeKeys?: never;
|
||||
};
|
||||
|
||||
type Props<ObjectType, ObjectSchema extends ZodObject> = (
|
||||
| EditProps<ObjectType>
|
||||
| CreateProps<ObjectType>
|
||||
) &
|
||||
RestProps<ObjectSchema>;
|
||||
|
||||
const RenderField = (key: string, value: any, label?: string) => {
|
||||
const getField = () => {
|
||||
switch (value.type) {
|
||||
case 'string':
|
||||
return <StringField label={label ? label : key} />;
|
||||
case 'number':
|
||||
return <NumberField label={label ? label : key} />;
|
||||
case 'boolean':
|
||||
return <BooleanField label={label ? label : key} />;
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
return <div key={key}>{getField()}</div>;
|
||||
};
|
||||
|
||||
function ObjectForm<ObjectType, ObjectSchema extends ZodObject<any, any>>(
|
||||
props: Props<ObjectType, ObjectSchema>
|
||||
) {
|
||||
const processKeyValue = (key: string, val: any) => {
|
||||
const keyTyped = key as keyof typeof props.schema.shape;
|
||||
if (props.excludeKeys && props.excludeKeys.includes(keyTyped)) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (props.includeKeys && !props.includeKeys.includes(keyTyped)) {
|
||||
return <></>;
|
||||
}
|
||||
const label = props.labels ? props.labels[keyTyped] : undefined;
|
||||
return RenderField(key, val, label);
|
||||
};
|
||||
return (
|
||||
<Flex gap={rem(10)} direction="column">
|
||||
{Object.entries(props.schema.shape).map(([key, val]) => {
|
||||
return processKeyValue(key, val);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default ObjectForm;
|
||||
23
components/Tables/Actions/BaseTableActions.tsx
Normal file
23
components/Tables/Actions/BaseTableActions.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconPencil, IconTrash } from '@tabler/icons-react';
|
||||
import { ActionIcon, Flex, rem } from '@mantine/core';
|
||||
|
||||
type Props<T> = {
|
||||
element: T;
|
||||
onChange: (element: T) => Promise<void>;
|
||||
onDelete: (element: T) => Promise<void>;
|
||||
};
|
||||
|
||||
const BaseTableActions = <T,>({ element, onChange, onDelete }: Props<T>) => {
|
||||
return (
|
||||
<Flex gap={rem(10)} align="center" justify="center">
|
||||
<ActionIcon variant="default" onClick={async () => await onChange(element)}>
|
||||
<IconPencil />
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="default" onClick={async () => await onDelete(element)}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseTableActions;
|
||||
10
components/Welcome/Welcome.module.css
Normal file
10
components/Welcome/Welcome.module.css
Normal file
@ -0,0 +1,10 @@
|
||||
.title {
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-size: rem(100px);
|
||||
font-weight: 900;
|
||||
letter-spacing: rem(-2px);
|
||||
|
||||
@media (max-width: $mantine-breakpoint-md) {
|
||||
font-size: rem(50px);
|
||||
}
|
||||
}
|
||||
7
components/Welcome/Welcome.story.tsx
Normal file
7
components/Welcome/Welcome.story.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { Welcome } from './Welcome';
|
||||
|
||||
export default {
|
||||
title: 'Welcome',
|
||||
};
|
||||
|
||||
export const Usage = () => <Welcome />;
|
||||
12
components/Welcome/Welcome.test.tsx
Normal file
12
components/Welcome/Welcome.test.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { render, screen } from '@/test-utils';
|
||||
import { Welcome } from './Welcome';
|
||||
|
||||
describe('Welcome component', () => {
|
||||
it('has correct Next.js theming section link', () => {
|
||||
render(<Welcome />);
|
||||
expect(screen.getByText('this guide')).toHaveAttribute(
|
||||
'href',
|
||||
'https://mantine.dev/guides/next/'
|
||||
);
|
||||
});
|
||||
});
|
||||
23
components/Welcome/Welcome.tsx
Normal file
23
components/Welcome/Welcome.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Anchor, Text, Title } from '@mantine/core';
|
||||
import classes from './Welcome.module.css';
|
||||
|
||||
export function Welcome() {
|
||||
return (
|
||||
<>
|
||||
<Title className={classes.title} ta="center" mt={100}>
|
||||
Welcome to{' '}
|
||||
<Text inherit variant="gradient" component="span" gradient={{ from: 'pink', to: 'yellow' }}>
|
||||
Mantine
|
||||
</Text>
|
||||
</Title>
|
||||
<Text color="dimmed" ta="center" size="lg" maw={580} mx="auto" mt="xl">
|
||||
This starter Next.js project includes a minimal setup for server side rendering, if you want
|
||||
to learn more on Mantine + Next.js integration follow{' '}
|
||||
<Anchor href="https://mantine.dev/guides/next/" size="lg">
|
||||
this guide
|
||||
</Anchor>
|
||||
. To get started edit index.tsx file.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user