123
This commit is contained in:
6
hey-api-config.ts
Normal file
6
hey-api-config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { CreateClientConfig } from '@/lib/client/client.gen';
|
||||
|
||||
export const createClientConfig: CreateClientConfig = (config) => ({
|
||||
...config,
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL,
|
||||
});
|
||||
21
LICENCE
Normal file
21
LICENCE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Vitaly Rtischev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
38
README.md
Normal file
38
README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Mantine Next.js template
|
||||
|
||||
This is a template for [Next.js](https://nextjs.org/) pages router + [Mantine](https://mantine.dev/).
|
||||
If you want to use app router instead, see [next-app-template](https://github.com/mantinedev/next-app-template).
|
||||
|
||||
## Features
|
||||
|
||||
This template comes with the following features:
|
||||
|
||||
- [PostCSS](https://postcss.org/) with [mantine-postcss-preset](https://mantine.dev/styles/postcss-preset)
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Storybook](https://storybook.js.org/)
|
||||
- [Jest](https://jestjs.io/) setup with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
|
||||
- ESLint setup with [eslint-config-mantine](https://github.com/mantinedev/eslint-config-mantine)
|
||||
|
||||
## npm scripts
|
||||
|
||||
### Build and dev scripts
|
||||
|
||||
- `dev` – start dev server
|
||||
- `build` – bundle application for production
|
||||
- `export` – exports static website to `out` folder
|
||||
- `analyze` – analyzes application bundle with [@next/bundle-analyzer](https://www.npmjs.com/package/@next/bundle-analyzer)
|
||||
|
||||
### Testing scripts
|
||||
|
||||
- `typecheck` – checks TypeScript types
|
||||
- `lint` – runs ESLint
|
||||
- `prettier:check` – checks files with Prettier
|
||||
- `jest` – runs jest tests
|
||||
- `jest:watch` – starts jest watch
|
||||
- `test` – runs `jest`, `prettier:check`, `lint` and `typecheck` scripts
|
||||
|
||||
### Other scripts
|
||||
|
||||
- `storybook` – starts storybook dev server
|
||||
- `storybook:build` – build production storybook bundle to `storybook-static`
|
||||
- `prettier:write` – formats all files with Prettier
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
23
eslint.config.mjs
Normal file
23
eslint.config.mjs
Normal file
@ -0,0 +1,23 @@
|
||||
import mantine from 'eslint-config-mantine';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
// @ts-check
|
||||
export default defineConfig(
|
||||
tseslint.configs.recommended,
|
||||
...mantine,
|
||||
{ ignores: ['**/*.{mjs,cjs,js,d.ts,d.mts}', '.next', 'src/lib/**'] },
|
||||
{
|
||||
files: ['**/*.story.tsx'],
|
||||
rules: { 'no-console': 'off' },
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: process.cwd(),
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
);
|
||||
16
jest.config.cjs
Normal file
16
jest.config.cjs
Normal file
@ -0,0 +1,16 @@
|
||||
const nextJest = require('next/jest');
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
dir: './',
|
||||
});
|
||||
|
||||
const customJestConfig = {
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.cjs'],
|
||||
moduleNameMapper: {
|
||||
'^@/components/(.*)$': '<rootDir>/components/$1',
|
||||
'^@/pages/(.*)$': '<rootDir>/pages/$1',
|
||||
},
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
};
|
||||
|
||||
module.exports = createJestConfig(customJestConfig);
|
||||
27
jest.setup.cjs
Normal file
27
jest.setup.cjs
Normal file
@ -0,0 +1,27 @@
|
||||
require('@testing-library/jest-dom');
|
||||
|
||||
const { getComputedStyle } = window;
|
||||
window.getComputedStyle = (elt) => getComputedStyle(elt);
|
||||
window.HTMLElement.prototype.scrollIntoView = () => {};
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
window.ResizeObserver = ResizeObserver;
|
||||
1342
lib/client/@tanstack/react-query.gen.ts
Normal file
1342
lib/client/@tanstack/react-query.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
35
lib/client/client.gen.ts
Normal file
35
lib/client/client.gen.ts
Normal file
@ -0,0 +1,35 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { createClientConfig } from '@/ hey-api-config';
|
||||
|
||||
import { type ClientOptions, type ClientOptions as ClientOptions3, type Config, type Config as Config2, createClient, createClient as createClient2, createConfig, createConfig as createConfig2 } from './client';
|
||||
import type { ClientOptions as ClientOptions2 } from './types.gen';
|
||||
|
||||
/**
|
||||
* The `createClientConfig()` function will be called on client initialization
|
||||
* and the returned object will become the client's initial configuration.
|
||||
*
|
||||
* You may want to initialize your client this way instead of calling
|
||||
* `setConfig()`. This is useful for example if you're using Next.js
|
||||
* to ensure your client always has the correct values.
|
||||
*/
|
||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
||||
|
||||
export const client = createClient(createConfig<ClientOptions2>({
|
||||
baseURL: '/api'
|
||||
}));
|
||||
|
||||
/**
|
||||
* The `createClientConfig()` function will be called on client initialization
|
||||
* and the returned object will become the client's initial configuration.
|
||||
*
|
||||
* You may want to initialize your client this way instead of calling
|
||||
* `setConfig()`. This is useful for example if you're using Next.js
|
||||
* to ensure your client always has the correct values.
|
||||
*/
|
||||
export type CreateClientConfig2<T extends ClientOptions3 = ClientOptions2> = (override?: Config2<ClientOptions3 & T>) => Config2<Required<ClientOptions3> & T>;
|
||||
|
||||
export const client2 = createClient2(createClientConfig(createConfig2<ClientOptions2>({
|
||||
baseURL: '/api'
|
||||
})));
|
||||
|
||||
163
lib/client/client/client.gen.ts
Normal file
163
lib/client/client/client.gen.ts
Normal file
@ -0,0 +1,163 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios';
|
||||
import axios from 'axios';
|
||||
|
||||
import { createSseClient } from '../core/serverSentEvents.gen';
|
||||
import type { HttpMethod } from '../core/types.gen';
|
||||
import { getValidRequestBody } from '../core/utils.gen';
|
||||
import type { Client, Config, RequestOptions } from './types.gen';
|
||||
import {
|
||||
buildUrl,
|
||||
createConfig,
|
||||
mergeConfigs,
|
||||
mergeHeaders,
|
||||
setAuthParams,
|
||||
} from './utils.gen';
|
||||
|
||||
export const createClient = (config: Config = {}): Client => {
|
||||
let _config = mergeConfigs(createConfig(), config);
|
||||
|
||||
let instance: AxiosInstance;
|
||||
|
||||
if (_config.axios && !('Axios' in _config.axios)) {
|
||||
instance = _config.axios;
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { auth, ...configWithoutAuth } = _config;
|
||||
instance = axios.create(configWithoutAuth);
|
||||
}
|
||||
|
||||
const getConfig = (): Config => ({ ..._config });
|
||||
|
||||
const setConfig = (config: Config): Config => {
|
||||
_config = mergeConfigs(_config, config);
|
||||
instance.defaults = {
|
||||
...instance.defaults,
|
||||
..._config,
|
||||
// @ts-expect-error
|
||||
headers: mergeHeaders(instance.defaults.headers, _config.headers),
|
||||
};
|
||||
return getConfig();
|
||||
};
|
||||
|
||||
const beforeRequest = async (options: RequestOptions) => {
|
||||
const opts = {
|
||||
..._config,
|
||||
...options,
|
||||
axios: options.axios ?? _config.axios ?? instance,
|
||||
headers: mergeHeaders(_config.headers, options.headers),
|
||||
};
|
||||
|
||||
if (opts.security) {
|
||||
await setAuthParams({
|
||||
...opts,
|
||||
security: opts.security,
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.requestValidator) {
|
||||
await opts.requestValidator(opts);
|
||||
}
|
||||
|
||||
if (opts.body !== undefined && opts.bodySerializer) {
|
||||
opts.body = opts.bodySerializer(opts.body);
|
||||
}
|
||||
|
||||
const url = buildUrl(opts);
|
||||
|
||||
return { opts, url };
|
||||
};
|
||||
|
||||
// @ts-expect-error
|
||||
const request: Client['request'] = async (options) => {
|
||||
// @ts-expect-error
|
||||
const { opts, url } = await beforeRequest(options);
|
||||
try {
|
||||
// assign Axios here for consistency with fetch
|
||||
const _axios = opts.axios!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { auth, ...optsWithoutAuth } = opts;
|
||||
const response = await _axios({
|
||||
...optsWithoutAuth,
|
||||
baseURL: '', // the baseURL is already included in `url`
|
||||
data: getValidRequestBody(opts),
|
||||
headers: opts.headers as RawAxiosRequestHeaders,
|
||||
// let `paramsSerializer()` handle query params if it exists
|
||||
params: opts.paramsSerializer ? opts.query : undefined,
|
||||
url,
|
||||
});
|
||||
|
||||
let { data } = response;
|
||||
|
||||
if (opts.responseType === 'json') {
|
||||
if (opts.responseValidator) {
|
||||
await opts.responseValidator(data);
|
||||
}
|
||||
|
||||
if (opts.responseTransformer) {
|
||||
data = await opts.responseTransformer(data);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
data: data ?? {},
|
||||
};
|
||||
} catch (error) {
|
||||
const e = error as AxiosError;
|
||||
if (opts.throwOnError) {
|
||||
throw e;
|
||||
}
|
||||
// @ts-expect-error
|
||||
e.error = e.response?.data ?? {};
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
const makeMethodFn =
|
||||
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
|
||||
request({ ...options, method });
|
||||
|
||||
const makeSseFn =
|
||||
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
||||
const { opts, url } = await beforeRequest(options);
|
||||
return createSseClient({
|
||||
...opts,
|
||||
body: opts.body as BodyInit | null | undefined,
|
||||
headers: opts.headers as Record<string, string>,
|
||||
method,
|
||||
// @ts-expect-error
|
||||
signal: opts.signal,
|
||||
url,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
buildUrl,
|
||||
connect: makeMethodFn('CONNECT'),
|
||||
delete: makeMethodFn('DELETE'),
|
||||
get: makeMethodFn('GET'),
|
||||
getConfig,
|
||||
head: makeMethodFn('HEAD'),
|
||||
instance,
|
||||
options: makeMethodFn('OPTIONS'),
|
||||
patch: makeMethodFn('PATCH'),
|
||||
post: makeMethodFn('POST'),
|
||||
put: makeMethodFn('PUT'),
|
||||
request,
|
||||
setConfig,
|
||||
sse: {
|
||||
connect: makeSseFn('CONNECT'),
|
||||
delete: makeSseFn('DELETE'),
|
||||
get: makeSseFn('GET'),
|
||||
head: makeSseFn('HEAD'),
|
||||
options: makeSseFn('OPTIONS'),
|
||||
patch: makeSseFn('PATCH'),
|
||||
post: makeSseFn('POST'),
|
||||
put: makeSseFn('PUT'),
|
||||
trace: makeSseFn('TRACE'),
|
||||
},
|
||||
trace: makeMethodFn('TRACE'),
|
||||
} as Client;
|
||||
};
|
||||
24
lib/client/client/index.ts
Normal file
24
lib/client/client/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type { Auth } from '../core/auth.gen';
|
||||
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
||||
export {
|
||||
formDataBodySerializer,
|
||||
jsonBodySerializer,
|
||||
urlSearchParamsBodySerializer,
|
||||
} from '../core/bodySerializer.gen';
|
||||
export { buildClientParams } from '../core/params.gen';
|
||||
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
|
||||
export { createClient } from './client.gen';
|
||||
export type {
|
||||
Client,
|
||||
ClientOptions,
|
||||
Config,
|
||||
CreateClientConfig,
|
||||
Options,
|
||||
OptionsLegacyParser,
|
||||
RequestOptions,
|
||||
RequestResult,
|
||||
TDataShape,
|
||||
} from './types.gen';
|
||||
export { createConfig } from './utils.gen';
|
||||
216
lib/client/client/types.gen.ts
Normal file
216
lib/client/client/types.gen.ts
Normal file
@ -0,0 +1,216 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type {
|
||||
AxiosError,
|
||||
AxiosInstance,
|
||||
AxiosRequestHeaders,
|
||||
AxiosResponse,
|
||||
AxiosStatic,
|
||||
CreateAxiosDefaults,
|
||||
} from 'axios';
|
||||
|
||||
import type { Auth } from '../core/auth.gen';
|
||||
import type {
|
||||
ServerSentEventsOptions,
|
||||
ServerSentEventsResult,
|
||||
} from '../core/serverSentEvents.gen';
|
||||
import type {
|
||||
Client as CoreClient,
|
||||
Config as CoreConfig,
|
||||
} from '../core/types.gen';
|
||||
|
||||
export interface Config<T extends ClientOptions = ClientOptions>
|
||||
extends Omit<CreateAxiosDefaults, 'auth' | 'baseURL' | 'headers' | 'method'>,
|
||||
CoreConfig {
|
||||
/**
|
||||
* Axios implementation. You can use this option to provide either an
|
||||
* `AxiosStatic` or an `AxiosInstance`.
|
||||
*
|
||||
* @default axios
|
||||
*/
|
||||
axios?: AxiosStatic | AxiosInstance;
|
||||
/**
|
||||
* Base URL for all requests made by this client.
|
||||
*/
|
||||
baseURL?: T['baseURL'];
|
||||
/**
|
||||
* An object containing any HTTP headers that you want to pre-populate your
|
||||
* `Headers` object with.
|
||||
*
|
||||
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
||||
*/
|
||||
headers?:
|
||||
| AxiosRequestHeaders
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| (string | number | boolean)[]
|
||||
| null
|
||||
| undefined
|
||||
| unknown
|
||||
>;
|
||||
/**
|
||||
* Throw an error instead of returning it in the response?
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
throwOnError?: T['throwOnError'];
|
||||
}
|
||||
|
||||
export interface RequestOptions<
|
||||
TData = unknown,
|
||||
ThrowOnError extends boolean = boolean,
|
||||
Url extends string = string,
|
||||
> extends Config<{
|
||||
throwOnError: ThrowOnError;
|
||||
}>,
|
||||
Pick<
|
||||
ServerSentEventsOptions<TData>,
|
||||
| 'onSseError'
|
||||
| 'onSseEvent'
|
||||
| 'sseDefaultRetryDelay'
|
||||
| 'sseMaxRetryAttempts'
|
||||
| 'sseMaxRetryDelay'
|
||||
> {
|
||||
/**
|
||||
* Any body that you want to add to your request.
|
||||
*
|
||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
|
||||
*/
|
||||
body?: unknown;
|
||||
path?: Record<string, unknown>;
|
||||
query?: Record<string, unknown>;
|
||||
/**
|
||||
* Security mechanism(s) to use for the request.
|
||||
*/
|
||||
security?: ReadonlyArray<Auth>;
|
||||
url: Url;
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
baseURL?: string;
|
||||
throwOnError?: boolean;
|
||||
}
|
||||
|
||||
export type RequestResult<
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = boolean,
|
||||
> = ThrowOnError extends true
|
||||
? Promise<
|
||||
AxiosResponse<
|
||||
TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||
>
|
||||
>
|
||||
: Promise<
|
||||
| (AxiosResponse<
|
||||
TData extends Record<string, unknown> ? TData[keyof TData] : TData
|
||||
> & { error: undefined })
|
||||
| (AxiosError<
|
||||
TError extends Record<string, unknown> ? TError[keyof TError] : TError
|
||||
> & {
|
||||
data: undefined;
|
||||
error: TError extends Record<string, unknown>
|
||||
? TError[keyof TError]
|
||||
: TError;
|
||||
})
|
||||
>;
|
||||
|
||||
type MethodFn = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, ThrowOnError>, 'method'>,
|
||||
) => RequestResult<TData, TError, ThrowOnError>;
|
||||
|
||||
type SseFn = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, ThrowOnError>, 'method'>,
|
||||
) => Promise<ServerSentEventsResult<TData, TError>>;
|
||||
|
||||
type RequestFn = <
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
ThrowOnError extends boolean = false,
|
||||
>(
|
||||
options: Omit<RequestOptions<TData, ThrowOnError>, 'method'> &
|
||||
Pick<Required<RequestOptions<TData, ThrowOnError>>, 'method'>,
|
||||
) => RequestResult<TData, TError, ThrowOnError>;
|
||||
|
||||
type BuildUrlFn = <
|
||||
TData extends {
|
||||
body?: unknown;
|
||||
path?: Record<string, unknown>;
|
||||
query?: Record<string, unknown>;
|
||||
url: string;
|
||||
},
|
||||
>(
|
||||
options: Pick<TData, 'url'> & Options<TData>,
|
||||
) => string;
|
||||
|
||||
export type Client = CoreClient<
|
||||
RequestFn,
|
||||
Config,
|
||||
MethodFn,
|
||||
BuildUrlFn,
|
||||
SseFn
|
||||
> & {
|
||||
instance: AxiosInstance;
|
||||
};
|
||||
|
||||
/**
|
||||
* The `createClientConfig()` function will be called on client initialization
|
||||
* and the returned object will become the client's initial configuration.
|
||||
*
|
||||
* You may want to initialize your client this way instead of calling
|
||||
* `setConfig()`. This is useful for example if you're using Next.js
|
||||
* to ensure your client always has the correct values.
|
||||
*/
|
||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
||||
override?: Config<ClientOptions & T>,
|
||||
) => Config<Required<ClientOptions> & T>;
|
||||
|
||||
export interface TDataShape {
|
||||
body?: unknown;
|
||||
headers?: unknown;
|
||||
path?: unknown;
|
||||
query?: unknown;
|
||||
url: string;
|
||||
}
|
||||
|
||||
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
export type Options<
|
||||
TData extends TDataShape = TDataShape,
|
||||
ThrowOnError extends boolean = boolean,
|
||||
TResponse = unknown,
|
||||
> = OmitKeys<
|
||||
RequestOptions<TResponse, ThrowOnError>,
|
||||
'body' | 'path' | 'query' | 'url'
|
||||
> &
|
||||
Omit<TData, 'url'>;
|
||||
|
||||
export type OptionsLegacyParser<
|
||||
TData = unknown,
|
||||
ThrowOnError extends boolean = boolean,
|
||||
> = TData extends { body?: any }
|
||||
? TData extends { headers?: any }
|
||||
? OmitKeys<
|
||||
RequestOptions<unknown, ThrowOnError>,
|
||||
'body' | 'headers' | 'url'
|
||||
> &
|
||||
TData
|
||||
: OmitKeys<RequestOptions<unknown, ThrowOnError>, 'body' | 'url'> &
|
||||
TData &
|
||||
Pick<RequestOptions<unknown, ThrowOnError>, 'headers'>
|
||||
: TData extends { headers?: any }
|
||||
? OmitKeys<RequestOptions<unknown, ThrowOnError>, 'headers' | 'url'> &
|
||||
TData &
|
||||
Pick<RequestOptions<unknown, ThrowOnError>, 'body'>
|
||||
: OmitKeys<RequestOptions<unknown, ThrowOnError>, 'url'> & TData;
|
||||
212
lib/client/client/utils.gen.ts
Normal file
212
lib/client/client/utils.gen.ts
Normal file
@ -0,0 +1,212 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { getAuthToken } from '../core/auth.gen';
|
||||
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
||||
import {
|
||||
serializeArrayParam,
|
||||
serializeObjectParam,
|
||||
serializePrimitiveParam,
|
||||
} from '../core/pathSerializer.gen';
|
||||
import { getUrl } from '../core/utils.gen';
|
||||
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
|
||||
|
||||
export const createQuerySerializer = <T = unknown>({
|
||||
allowReserved,
|
||||
array,
|
||||
object,
|
||||
}: QuerySerializerOptions = {}) => {
|
||||
const querySerializer = (queryParams: T) => {
|
||||
const search: string[] = [];
|
||||
if (queryParams && typeof queryParams === 'object') {
|
||||
for (const name in queryParams) {
|
||||
const value = queryParams[name];
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const serializedArray = serializeArrayParam({
|
||||
allowReserved,
|
||||
explode: true,
|
||||
name,
|
||||
style: 'form',
|
||||
value,
|
||||
...array,
|
||||
});
|
||||
if (serializedArray) search.push(serializedArray);
|
||||
} else if (typeof value === 'object') {
|
||||
const serializedObject = serializeObjectParam({
|
||||
allowReserved,
|
||||
explode: true,
|
||||
name,
|
||||
style: 'deepObject',
|
||||
value: value as Record<string, unknown>,
|
||||
...object,
|
||||
});
|
||||
if (serializedObject) search.push(serializedObject);
|
||||
} else {
|
||||
const serializedPrimitive = serializePrimitiveParam({
|
||||
allowReserved,
|
||||
name,
|
||||
value: value as string,
|
||||
});
|
||||
if (serializedPrimitive) search.push(serializedPrimitive);
|
||||
}
|
||||
}
|
||||
}
|
||||
return search.join('&');
|
||||
};
|
||||
return querySerializer;
|
||||
};
|
||||
|
||||
const checkForExistence = (
|
||||
options: Pick<RequestOptions, 'auth' | 'query'> & {
|
||||
headers: Record<any, unknown>;
|
||||
},
|
||||
name?: string,
|
||||
): boolean => {
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
if (name in options.headers || options.query?.[name]) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
'Cookie' in options.headers &&
|
||||
options.headers['Cookie'] &&
|
||||
typeof options.headers['Cookie'] === 'string'
|
||||
) {
|
||||
return options.headers['Cookie'].includes(`${name}=`);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const setAuthParams = async ({
|
||||
security,
|
||||
...options
|
||||
}: Pick<Required<RequestOptions>, 'security'> &
|
||||
Pick<RequestOptions, 'auth' | 'query'> & {
|
||||
headers: Record<any, unknown>;
|
||||
}) => {
|
||||
for (const auth of security) {
|
||||
if (checkForExistence(options, auth.name)) {
|
||||
continue;
|
||||
}
|
||||
const token = await getAuthToken(auth, options.auth);
|
||||
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = auth.name ?? 'Authorization';
|
||||
|
||||
switch (auth.in) {
|
||||
case 'query':
|
||||
if (!options.query) {
|
||||
options.query = {};
|
||||
}
|
||||
options.query[name] = token;
|
||||
break;
|
||||
case 'cookie': {
|
||||
const value = `${name}=${token}`;
|
||||
if ('Cookie' in options.headers && options.headers['Cookie']) {
|
||||
options.headers['Cookie'] = `${options.headers['Cookie']}; ${value}`;
|
||||
} else {
|
||||
options.headers['Cookie'] = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'header':
|
||||
default:
|
||||
options.headers[name] = token;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const buildUrl: Client['buildUrl'] = (options) => {
|
||||
const instanceBaseUrl = options.axios?.defaults?.baseURL;
|
||||
|
||||
const baseUrl =
|
||||
!!options.baseURL && typeof options.baseURL === 'string'
|
||||
? options.baseURL
|
||||
: instanceBaseUrl;
|
||||
|
||||
return getUrl({
|
||||
baseUrl: baseUrl as string,
|
||||
path: options.path,
|
||||
// let `paramsSerializer()` handle query params if it exists
|
||||
query: !options.paramsSerializer ? options.query : undefined,
|
||||
querySerializer:
|
||||
typeof options.querySerializer === 'function'
|
||||
? options.querySerializer
|
||||
: createQuerySerializer(options.querySerializer),
|
||||
url: options.url,
|
||||
});
|
||||
};
|
||||
|
||||
export const mergeConfigs = (a: Config, b: Config): Config => {
|
||||
const config = { ...a, ...b };
|
||||
config.headers = mergeHeaders(a.headers, b.headers);
|
||||
return config;
|
||||
};
|
||||
|
||||
/**
|
||||
* Special Axios headers keywords allowing to set headers by request method.
|
||||
*/
|
||||
export const axiosHeadersKeywords = [
|
||||
'common',
|
||||
'delete',
|
||||
'get',
|
||||
'head',
|
||||
'patch',
|
||||
'post',
|
||||
'put',
|
||||
] as const;
|
||||
|
||||
export const mergeHeaders = (
|
||||
...headers: Array<Required<Config>['headers'] | undefined>
|
||||
): Record<any, unknown> => {
|
||||
const mergedHeaders: Record<any, unknown> = {};
|
||||
for (const header of headers) {
|
||||
if (!header || typeof header !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const iterator = Object.entries(header);
|
||||
|
||||
for (const [key, value] of iterator) {
|
||||
if (
|
||||
axiosHeadersKeywords.includes(
|
||||
key as (typeof axiosHeadersKeywords)[number],
|
||||
) &&
|
||||
typeof value === 'object'
|
||||
) {
|
||||
mergedHeaders[key] = {
|
||||
...(mergedHeaders[key] as Record<any, unknown>),
|
||||
...value,
|
||||
};
|
||||
} else if (value === null) {
|
||||
delete mergedHeaders[key];
|
||||
} else if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
// @ts-expect-error
|
||||
mergedHeaders[key] = [...(mergedHeaders[key] ?? []), v as string];
|
||||
}
|
||||
} else if (value !== undefined) {
|
||||
// assume object headers are meant to be JSON stringified, i.e. their
|
||||
// content value in OpenAPI specification is 'application/json'
|
||||
mergedHeaders[key] =
|
||||
typeof value === 'object' ? JSON.stringify(value) : (value as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mergedHeaders;
|
||||
};
|
||||
|
||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
||||
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
||||
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||
...override,
|
||||
});
|
||||
42
lib/client/core/auth.gen.ts
Normal file
42
lib/client/core/auth.gen.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type AuthToken = string | undefined;
|
||||
|
||||
export interface Auth {
|
||||
/**
|
||||
* Which part of the request do we use to send the auth?
|
||||
*
|
||||
* @default 'header'
|
||||
*/
|
||||
in?: 'header' | 'query' | 'cookie';
|
||||
/**
|
||||
* Header or query parameter name.
|
||||
*
|
||||
* @default 'Authorization'
|
||||
*/
|
||||
name?: string;
|
||||
scheme?: 'basic' | 'bearer';
|
||||
type: 'apiKey' | 'http';
|
||||
}
|
||||
|
||||
export const getAuthToken = async (
|
||||
auth: Auth,
|
||||
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
||||
): Promise<string | undefined> => {
|
||||
const token =
|
||||
typeof callback === 'function' ? await callback(auth) : callback;
|
||||
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auth.scheme === 'bearer') {
|
||||
return `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (auth.scheme === 'basic') {
|
||||
return `Basic ${btoa(token)}`;
|
||||
}
|
||||
|
||||
return token;
|
||||
};
|
||||
92
lib/client/core/bodySerializer.gen.ts
Normal file
92
lib/client/core/bodySerializer.gen.ts
Normal file
@ -0,0 +1,92 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type {
|
||||
ArrayStyle,
|
||||
ObjectStyle,
|
||||
SerializerOptions,
|
||||
} from './pathSerializer.gen';
|
||||
|
||||
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
||||
|
||||
export type BodySerializer = (body: any) => any;
|
||||
|
||||
export interface QuerySerializerOptions {
|
||||
allowReserved?: boolean;
|
||||
array?: SerializerOptions<ArrayStyle>;
|
||||
object?: SerializerOptions<ObjectStyle>;
|
||||
}
|
||||
|
||||
const serializeFormDataPair = (
|
||||
data: FormData,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void => {
|
||||
if (typeof value === 'string' || value instanceof Blob) {
|
||||
data.append(key, value);
|
||||
} else if (value instanceof Date) {
|
||||
data.append(key, value.toISOString());
|
||||
} else {
|
||||
data.append(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
const serializeUrlSearchParamsPair = (
|
||||
data: URLSearchParams,
|
||||
key: string,
|
||||
value: unknown,
|
||||
): void => {
|
||||
if (typeof value === 'string') {
|
||||
data.append(key, value);
|
||||
} else {
|
||||
data.append(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
export const formDataBodySerializer = {
|
||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||
body: T,
|
||||
): FormData => {
|
||||
const data = new FormData();
|
||||
|
||||
Object.entries(body).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => serializeFormDataPair(data, key, v));
|
||||
} else {
|
||||
serializeFormDataPair(data, key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
},
|
||||
};
|
||||
|
||||
export const jsonBodySerializer = {
|
||||
bodySerializer: <T>(body: T): string =>
|
||||
JSON.stringify(body, (_key, value) =>
|
||||
typeof value === 'bigint' ? value.toString() : value,
|
||||
),
|
||||
};
|
||||
|
||||
export const urlSearchParamsBodySerializer = {
|
||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||
body: T,
|
||||
): string => {
|
||||
const data = new URLSearchParams();
|
||||
|
||||
Object.entries(body).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
|
||||
} else {
|
||||
serializeUrlSearchParamsPair(data, key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return data.toString();
|
||||
},
|
||||
};
|
||||
153
lib/client/core/params.gen.ts
Normal file
153
lib/client/core/params.gen.ts
Normal file
@ -0,0 +1,153 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
type Slot = 'body' | 'headers' | 'path' | 'query';
|
||||
|
||||
export type Field =
|
||||
| {
|
||||
in: Exclude<Slot, 'body'>;
|
||||
/**
|
||||
* Field name. This is the name we want the user to see and use.
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Field mapped name. This is the name we want to use in the request.
|
||||
* If omitted, we use the same value as `key`.
|
||||
*/
|
||||
map?: string;
|
||||
}
|
||||
| {
|
||||
in: Extract<Slot, 'body'>;
|
||||
/**
|
||||
* Key isn't required for bodies.
|
||||
*/
|
||||
key?: string;
|
||||
map?: string;
|
||||
};
|
||||
|
||||
export interface Fields {
|
||||
allowExtra?: Partial<Record<Slot, boolean>>;
|
||||
args?: ReadonlyArray<Field>;
|
||||
}
|
||||
|
||||
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
||||
|
||||
const extraPrefixesMap: Record<string, Slot> = {
|
||||
$body_: 'body',
|
||||
$headers_: 'headers',
|
||||
$path_: 'path',
|
||||
$query_: 'query',
|
||||
};
|
||||
const extraPrefixes = Object.entries(extraPrefixesMap);
|
||||
|
||||
type KeyMap = Map<
|
||||
string,
|
||||
{
|
||||
in: Slot;
|
||||
map?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
||||
if (!map) {
|
||||
map = new Map();
|
||||
}
|
||||
|
||||
for (const config of fields) {
|
||||
if ('in' in config) {
|
||||
if (config.key) {
|
||||
map.set(config.key, {
|
||||
in: config.in,
|
||||
map: config.map,
|
||||
});
|
||||
}
|
||||
} else if (config.args) {
|
||||
buildKeyMap(config.args, map);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
interface Params {
|
||||
body: unknown;
|
||||
headers: Record<string, unknown>;
|
||||
path: Record<string, unknown>;
|
||||
query: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const stripEmptySlots = (params: Params) => {
|
||||
for (const [slot, value] of Object.entries(params)) {
|
||||
if (value && typeof value === 'object' && !Object.keys(value).length) {
|
||||
delete params[slot as Slot];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const buildClientParams = (
|
||||
args: ReadonlyArray<unknown>,
|
||||
fields: FieldsConfig,
|
||||
) => {
|
||||
const params: Params = {
|
||||
body: {},
|
||||
headers: {},
|
||||
path: {},
|
||||
query: {},
|
||||
};
|
||||
|
||||
const map = buildKeyMap(fields);
|
||||
|
||||
let config: FieldsConfig[number] | undefined;
|
||||
|
||||
for (const [index, arg] of args.entries()) {
|
||||
if (fields[index]) {
|
||||
config = fields[index];
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ('in' in config) {
|
||||
if (config.key) {
|
||||
const field = map.get(config.key)!;
|
||||
const name = field.map || config.key;
|
||||
(params[field.in] as Record<string, unknown>)[name] = arg;
|
||||
} else {
|
||||
params.body = arg;
|
||||
}
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(arg ?? {})) {
|
||||
const field = map.get(key);
|
||||
|
||||
if (field) {
|
||||
const name = field.map || key;
|
||||
(params[field.in] as Record<string, unknown>)[name] = value;
|
||||
} else {
|
||||
const extra = extraPrefixes.find(([prefix]) =>
|
||||
key.startsWith(prefix),
|
||||
);
|
||||
|
||||
if (extra) {
|
||||
const [prefix, slot] = extra;
|
||||
(params[slot] as Record<string, unknown>)[
|
||||
key.slice(prefix.length)
|
||||
] = value;
|
||||
} else {
|
||||
for (const [slot, allowed] of Object.entries(
|
||||
config.allowExtra ?? {},
|
||||
)) {
|
||||
if (allowed) {
|
||||
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stripEmptySlots(params);
|
||||
|
||||
return params;
|
||||
};
|
||||
181
lib/client/core/pathSerializer.gen.ts
Normal file
181
lib/client/core/pathSerializer.gen.ts
Normal file
@ -0,0 +1,181 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
interface SerializeOptions<T>
|
||||
extends SerializePrimitiveOptions,
|
||||
SerializerOptions<T> {}
|
||||
|
||||
interface SerializePrimitiveOptions {
|
||||
allowReserved?: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SerializerOptions<T> {
|
||||
/**
|
||||
* @default true
|
||||
*/
|
||||
explode: boolean;
|
||||
style: T;
|
||||
}
|
||||
|
||||
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
|
||||
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
||||
type MatrixStyle = 'label' | 'matrix' | 'simple';
|
||||
export type ObjectStyle = 'form' | 'deepObject';
|
||||
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
|
||||
|
||||
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
||||
switch (style) {
|
||||
case 'label':
|
||||
return '.';
|
||||
case 'matrix':
|
||||
return ';';
|
||||
case 'simple':
|
||||
return ',';
|
||||
default:
|
||||
return '&';
|
||||
}
|
||||
};
|
||||
|
||||
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
||||
switch (style) {
|
||||
case 'form':
|
||||
return ',';
|
||||
case 'pipeDelimited':
|
||||
return '|';
|
||||
case 'spaceDelimited':
|
||||
return '%20';
|
||||
default:
|
||||
return ',';
|
||||
}
|
||||
};
|
||||
|
||||
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
||||
switch (style) {
|
||||
case 'label':
|
||||
return '.';
|
||||
case 'matrix':
|
||||
return ';';
|
||||
case 'simple':
|
||||
return ',';
|
||||
default:
|
||||
return '&';
|
||||
}
|
||||
};
|
||||
|
||||
export const serializeArrayParam = ({
|
||||
allowReserved,
|
||||
explode,
|
||||
name,
|
||||
style,
|
||||
value,
|
||||
}: SerializeOptions<ArraySeparatorStyle> & {
|
||||
value: unknown[];
|
||||
}) => {
|
||||
if (!explode) {
|
||||
const joinedValues = (
|
||||
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
|
||||
).join(separatorArrayNoExplode(style));
|
||||
switch (style) {
|
||||
case 'label':
|
||||
return `.${joinedValues}`;
|
||||
case 'matrix':
|
||||
return `;${name}=${joinedValues}`;
|
||||
case 'simple':
|
||||
return joinedValues;
|
||||
default:
|
||||
return `${name}=${joinedValues}`;
|
||||
}
|
||||
}
|
||||
|
||||
const separator = separatorArrayExplode(style);
|
||||
const joinedValues = value
|
||||
.map((v) => {
|
||||
if (style === 'label' || style === 'simple') {
|
||||
return allowReserved ? v : encodeURIComponent(v as string);
|
||||
}
|
||||
|
||||
return serializePrimitiveParam({
|
||||
allowReserved,
|
||||
name,
|
||||
value: v as string,
|
||||
});
|
||||
})
|
||||
.join(separator);
|
||||
return style === 'label' || style === 'matrix'
|
||||
? separator + joinedValues
|
||||
: joinedValues;
|
||||
};
|
||||
|
||||
export const serializePrimitiveParam = ({
|
||||
allowReserved,
|
||||
name,
|
||||
value,
|
||||
}: SerializePrimitiveParam) => {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
throw new Error(
|
||||
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
|
||||
);
|
||||
}
|
||||
|
||||
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
||||
};
|
||||
|
||||
export const serializeObjectParam = ({
|
||||
allowReserved,
|
||||
explode,
|
||||
name,
|
||||
style,
|
||||
value,
|
||||
valueOnly,
|
||||
}: SerializeOptions<ObjectSeparatorStyle> & {
|
||||
value: Record<string, unknown> | Date;
|
||||
valueOnly?: boolean;
|
||||
}) => {
|
||||
if (value instanceof Date) {
|
||||
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
||||
}
|
||||
|
||||
if (style !== 'deepObject' && !explode) {
|
||||
let values: string[] = [];
|
||||
Object.entries(value).forEach(([key, v]) => {
|
||||
values = [
|
||||
...values,
|
||||
key,
|
||||
allowReserved ? (v as string) : encodeURIComponent(v as string),
|
||||
];
|
||||
});
|
||||
const joinedValues = values.join(',');
|
||||
switch (style) {
|
||||
case 'form':
|
||||
return `${name}=${joinedValues}`;
|
||||
case 'label':
|
||||
return `.${joinedValues}`;
|
||||
case 'matrix':
|
||||
return `;${name}=${joinedValues}`;
|
||||
default:
|
||||
return joinedValues;
|
||||
}
|
||||
}
|
||||
|
||||
const separator = separatorObjectExplode(style);
|
||||
const joinedValues = Object.entries(value)
|
||||
.map(([key, v]) =>
|
||||
serializePrimitiveParam({
|
||||
allowReserved,
|
||||
name: style === 'deepObject' ? `${name}[${key}]` : key,
|
||||
value: v as string,
|
||||
}),
|
||||
)
|
||||
.join(separator);
|
||||
return style === 'label' || style === 'matrix'
|
||||
? separator + joinedValues
|
||||
: joinedValues;
|
||||
};
|
||||
136
lib/client/core/queryKeySerializer.gen.ts
Normal file
136
lib/client/core/queryKeySerializer.gen.ts
Normal file
@ -0,0 +1,136 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
/**
|
||||
* JSON-friendly union that mirrors what Pinia Colada can hash.
|
||||
*/
|
||||
export type JsonValue =
|
||||
| null
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| JsonValue[]
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
/**
|
||||
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
||||
*/
|
||||
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
typeof value === 'function' ||
|
||||
typeof value === 'symbol'
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString();
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely stringifies a value and parses it back into a JsonValue.
|
||||
*/
|
||||
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
|
||||
try {
|
||||
const json = JSON.stringify(input, queryKeyJsonReplacer);
|
||||
if (json === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(json) as JsonValue;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detects plain objects (including objects with a null prototype).
|
||||
*/
|
||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||
if (value === null || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(value as object);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
|
||||
*/
|
||||
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
||||
const entries = Array.from(params.entries()).sort(([a], [b]) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
const result: Record<string, JsonValue> = {};
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
const existing = result[key];
|
||||
if (existing === undefined) {
|
||||
result[key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(existing)) {
|
||||
(existing as string[]).push(value);
|
||||
} else {
|
||||
result[key] = [existing, value];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes any accepted value into a JSON-friendly shape for query keys.
|
||||
*/
|
||||
export const serializeQueryKeyValue = (
|
||||
value: unknown,
|
||||
): JsonValue | undefined => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean'
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (
|
||||
value === undefined ||
|
||||
typeof value === 'function' ||
|
||||
typeof value === 'symbol'
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof value === 'bigint') {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return stringifyToJsonValue(value);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof URLSearchParams !== 'undefined' &&
|
||||
value instanceof URLSearchParams
|
||||
) {
|
||||
return serializeSearchParams(value);
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
return stringifyToJsonValue(value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
264
lib/client/core/serverSentEvents.gen.ts
Normal file
264
lib/client/core/serverSentEvents.gen.ts
Normal file
@ -0,0 +1,264 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Config } from './types.gen';
|
||||
|
||||
export type ServerSentEventsOptions<TData = unknown> = Omit<
|
||||
RequestInit,
|
||||
'method'
|
||||
> &
|
||||
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
|
||||
/**
|
||||
* Fetch API implementation. You can use this option to provide a custom
|
||||
* fetch instance.
|
||||
*
|
||||
* @default globalThis.fetch
|
||||
*/
|
||||
fetch?: typeof fetch;
|
||||
/**
|
||||
* Implementing clients can call request interceptors inside this hook.
|
||||
*/
|
||||
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
|
||||
/**
|
||||
* Callback invoked when a network or parsing error occurs during streaming.
|
||||
*
|
||||
* This option applies only if the endpoint returns a stream of events.
|
||||
*
|
||||
* @param error The error that occurred.
|
||||
*/
|
||||
onSseError?: (error: unknown) => void;
|
||||
/**
|
||||
* Callback invoked when an event is streamed from the server.
|
||||
*
|
||||
* This option applies only if the endpoint returns a stream of events.
|
||||
*
|
||||
* @param event Event streamed from the server.
|
||||
* @returns Nothing (void).
|
||||
*/
|
||||
onSseEvent?: (event: StreamEvent<TData>) => void;
|
||||
serializedBody?: RequestInit['body'];
|
||||
/**
|
||||
* Default retry delay in milliseconds.
|
||||
*
|
||||
* This option applies only if the endpoint returns a stream of events.
|
||||
*
|
||||
* @default 3000
|
||||
*/
|
||||
sseDefaultRetryDelay?: number;
|
||||
/**
|
||||
* Maximum number of retry attempts before giving up.
|
||||
*/
|
||||
sseMaxRetryAttempts?: number;
|
||||
/**
|
||||
* Maximum retry delay in milliseconds.
|
||||
*
|
||||
* Applies only when exponential backoff is used.
|
||||
*
|
||||
* This option applies only if the endpoint returns a stream of events.
|
||||
*
|
||||
* @default 30000
|
||||
*/
|
||||
sseMaxRetryDelay?: number;
|
||||
/**
|
||||
* Optional sleep function for retry backoff.
|
||||
*
|
||||
* Defaults to using `setTimeout`.
|
||||
*/
|
||||
sseSleepFn?: (ms: number) => Promise<void>;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export interface StreamEvent<TData = unknown> {
|
||||
data: TData;
|
||||
event?: string;
|
||||
id?: string;
|
||||
retry?: number;
|
||||
}
|
||||
|
||||
export type ServerSentEventsResult<
|
||||
TData = unknown,
|
||||
TReturn = void,
|
||||
TNext = unknown,
|
||||
> = {
|
||||
stream: AsyncGenerator<
|
||||
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
|
||||
TReturn,
|
||||
TNext
|
||||
>;
|
||||
};
|
||||
|
||||
export const createSseClient = <TData = unknown>({
|
||||
onRequest,
|
||||
onSseError,
|
||||
onSseEvent,
|
||||
responseTransformer,
|
||||
responseValidator,
|
||||
sseDefaultRetryDelay,
|
||||
sseMaxRetryAttempts,
|
||||
sseMaxRetryDelay,
|
||||
sseSleepFn,
|
||||
url,
|
||||
...options
|
||||
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
||||
let lastEventId: string | undefined;
|
||||
|
||||
const sleep =
|
||||
sseSleepFn ??
|
||||
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||
|
||||
const createStream = async function* () {
|
||||
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
|
||||
let attempt = 0;
|
||||
const signal = options.signal ?? new AbortController().signal;
|
||||
|
||||
while (true) {
|
||||
if (signal.aborted) break;
|
||||
|
||||
attempt++;
|
||||
|
||||
const headers =
|
||||
options.headers instanceof Headers
|
||||
? options.headers
|
||||
: new Headers(options.headers as Record<string, string> | undefined);
|
||||
|
||||
if (lastEventId !== undefined) {
|
||||
headers.set('Last-Event-ID', lastEventId);
|
||||
}
|
||||
|
||||
try {
|
||||
const requestInit: RequestInit = {
|
||||
redirect: 'follow',
|
||||
...options,
|
||||
body: options.serializedBody,
|
||||
headers,
|
||||
signal,
|
||||
};
|
||||
let request = new Request(url, requestInit);
|
||||
if (onRequest) {
|
||||
request = await onRequest(url, requestInit);
|
||||
}
|
||||
// fetch must be assigned here, otherwise it would throw the error:
|
||||
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
||||
const _fetch = options.fetch ?? globalThis.fetch;
|
||||
const response = await _fetch(request);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`SSE failed: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
|
||||
if (!response.body) throw new Error('No body in SSE response');
|
||||
|
||||
const reader = response.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.getReader();
|
||||
|
||||
let buffer = '';
|
||||
|
||||
const abortHandler = () => {
|
||||
try {
|
||||
reader.cancel();
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
};
|
||||
|
||||
signal.addEventListener('abort', abortHandler);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += value;
|
||||
|
||||
const chunks = buffer.split('\n\n');
|
||||
buffer = chunks.pop() ?? '';
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const lines = chunk.split('\n');
|
||||
const dataLines: Array<string> = [];
|
||||
let eventName: string | undefined;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data:')) {
|
||||
dataLines.push(line.replace(/^data:\s*/, ''));
|
||||
} else if (line.startsWith('event:')) {
|
||||
eventName = line.replace(/^event:\s*/, '');
|
||||
} else if (line.startsWith('id:')) {
|
||||
lastEventId = line.replace(/^id:\s*/, '');
|
||||
} else if (line.startsWith('retry:')) {
|
||||
const parsed = Number.parseInt(
|
||||
line.replace(/^retry:\s*/, ''),
|
||||
10,
|
||||
);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
retryDelay = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data: unknown;
|
||||
let parsedJson = false;
|
||||
|
||||
if (dataLines.length) {
|
||||
const rawData = dataLines.join('\n');
|
||||
try {
|
||||
data = JSON.parse(rawData);
|
||||
parsedJson = true;
|
||||
} catch {
|
||||
data = rawData;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedJson) {
|
||||
if (responseValidator) {
|
||||
await responseValidator(data);
|
||||
}
|
||||
|
||||
if (responseTransformer) {
|
||||
data = await responseTransformer(data);
|
||||
}
|
||||
}
|
||||
|
||||
onSseEvent?.({
|
||||
data,
|
||||
event: eventName,
|
||||
id: lastEventId,
|
||||
retry: retryDelay,
|
||||
});
|
||||
|
||||
if (dataLines.length) {
|
||||
yield data as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
signal.removeEventListener('abort', abortHandler);
|
||||
reader.releaseLock();
|
||||
}
|
||||
|
||||
break; // exit loop on normal completion
|
||||
} catch (error) {
|
||||
// connection failed or aborted; retry after delay
|
||||
onSseError?.(error);
|
||||
|
||||
if (
|
||||
sseMaxRetryAttempts !== undefined &&
|
||||
attempt >= sseMaxRetryAttempts
|
||||
) {
|
||||
break; // stop after firing error
|
||||
}
|
||||
|
||||
// exponential backoff: double retry each attempt, cap at 30s
|
||||
const backoff = Math.min(
|
||||
retryDelay * 2 ** (attempt - 1),
|
||||
sseMaxRetryDelay ?? 30000,
|
||||
);
|
||||
await sleep(backoff);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stream = createStream();
|
||||
|
||||
return { stream };
|
||||
};
|
||||
118
lib/client/core/types.gen.ts
Normal file
118
lib/client/core/types.gen.ts
Normal file
@ -0,0 +1,118 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { Auth, AuthToken } from './auth.gen';
|
||||
import type {
|
||||
BodySerializer,
|
||||
QuerySerializer,
|
||||
QuerySerializerOptions,
|
||||
} from './bodySerializer.gen';
|
||||
|
||||
export type HttpMethod =
|
||||
| 'connect'
|
||||
| 'delete'
|
||||
| 'get'
|
||||
| 'head'
|
||||
| 'options'
|
||||
| 'patch'
|
||||
| 'post'
|
||||
| 'put'
|
||||
| 'trace';
|
||||
|
||||
export type Client<
|
||||
RequestFn = never,
|
||||
Config = unknown,
|
||||
MethodFn = never,
|
||||
BuildUrlFn = never,
|
||||
SseFn = never,
|
||||
> = {
|
||||
/**
|
||||
* Returns the final request URL.
|
||||
*/
|
||||
buildUrl: BuildUrlFn;
|
||||
getConfig: () => Config;
|
||||
request: RequestFn;
|
||||
setConfig: (config: Config) => Config;
|
||||
} & {
|
||||
[K in HttpMethod]: MethodFn;
|
||||
} & ([SseFn] extends [never]
|
||||
? { sse?: never }
|
||||
: { sse: { [K in HttpMethod]: SseFn } });
|
||||
|
||||
export interface Config {
|
||||
/**
|
||||
* Auth token or a function returning auth token. The resolved value will be
|
||||
* added to the request payload as defined by its `security` array.
|
||||
*/
|
||||
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
|
||||
/**
|
||||
* A function for serializing request body parameter. By default,
|
||||
* {@link JSON.stringify()} will be used.
|
||||
*/
|
||||
bodySerializer?: BodySerializer | null;
|
||||
/**
|
||||
* An object containing any HTTP headers that you want to pre-populate your
|
||||
* `Headers` object with.
|
||||
*
|
||||
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
||||
*/
|
||||
headers?:
|
||||
| RequestInit['headers']
|
||||
| Record<
|
||||
string,
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| (string | number | boolean)[]
|
||||
| null
|
||||
| undefined
|
||||
| unknown
|
||||
>;
|
||||
/**
|
||||
* The request method.
|
||||
*
|
||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
||||
*/
|
||||
method?: Uppercase<HttpMethod>;
|
||||
/**
|
||||
* A function for serializing request query parameters. By default, arrays
|
||||
* will be exploded in form style, objects will be exploded in deepObject
|
||||
* style, and reserved characters are percent-encoded.
|
||||
*
|
||||
* This method will have no effect if the native `paramsSerializer()` Axios
|
||||
* API function is used.
|
||||
*
|
||||
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
|
||||
*/
|
||||
querySerializer?: QuerySerializer | QuerySerializerOptions;
|
||||
/**
|
||||
* A function validating request data. This is useful if you want to ensure
|
||||
* the request conforms to the desired shape, so it can be safely sent to
|
||||
* the server.
|
||||
*/
|
||||
requestValidator?: (data: unknown) => Promise<unknown>;
|
||||
/**
|
||||
* A function transforming response data before it's returned. This is useful
|
||||
* for post-processing data, e.g. converting ISO strings into Date objects.
|
||||
*/
|
||||
responseTransformer?: (data: unknown) => Promise<unknown>;
|
||||
/**
|
||||
* A function validating response data. This is useful if you want to ensure
|
||||
* the response conforms to the desired shape, so it can be safely passed to
|
||||
* the transformers and returned to the user.
|
||||
*/
|
||||
responseValidator?: (data: unknown) => Promise<unknown>;
|
||||
}
|
||||
|
||||
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||
? true
|
||||
: [T] extends [never | undefined]
|
||||
? [undefined] extends [T]
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
|
||||
export type OmitNever<T extends Record<string, unknown>> = {
|
||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
|
||||
? never
|
||||
: K]: T[K];
|
||||
};
|
||||
143
lib/client/core/utils.gen.ts
Normal file
143
lib/client/core/utils.gen.ts
Normal file
@ -0,0 +1,143 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
|
||||
import {
|
||||
type ArraySeparatorStyle,
|
||||
serializeArrayParam,
|
||||
serializeObjectParam,
|
||||
serializePrimitiveParam,
|
||||
} from './pathSerializer.gen';
|
||||
|
||||
export interface PathSerializer {
|
||||
path: Record<string, unknown>;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
||||
|
||||
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||
let url = _url;
|
||||
const matches = _url.match(PATH_PARAM_RE);
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
let explode = false;
|
||||
let name = match.substring(1, match.length - 1);
|
||||
let style: ArraySeparatorStyle = 'simple';
|
||||
|
||||
if (name.endsWith('*')) {
|
||||
explode = true;
|
||||
name = name.substring(0, name.length - 1);
|
||||
}
|
||||
|
||||
if (name.startsWith('.')) {
|
||||
name = name.substring(1);
|
||||
style = 'label';
|
||||
} else if (name.startsWith(';')) {
|
||||
name = name.substring(1);
|
||||
style = 'matrix';
|
||||
}
|
||||
|
||||
const value = path[name];
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
url = url.replace(
|
||||
match,
|
||||
serializeArrayParam({ explode, name, style, value }),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
url = url.replace(
|
||||
match,
|
||||
serializeObjectParam({
|
||||
explode,
|
||||
name,
|
||||
style,
|
||||
value: value as Record<string, unknown>,
|
||||
valueOnly: true,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (style === 'matrix') {
|
||||
url = url.replace(
|
||||
match,
|
||||
`;${serializePrimitiveParam({
|
||||
name,
|
||||
value: value as string,
|
||||
})}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const replaceValue = encodeURIComponent(
|
||||
style === 'label' ? `.${value as string}` : (value as string),
|
||||
);
|
||||
url = url.replace(match, replaceValue);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getUrl = ({
|
||||
baseUrl,
|
||||
path,
|
||||
query,
|
||||
querySerializer,
|
||||
url: _url,
|
||||
}: {
|
||||
baseUrl?: string;
|
||||
path?: Record<string, unknown>;
|
||||
query?: Record<string, unknown>;
|
||||
querySerializer: QuerySerializer;
|
||||
url: string;
|
||||
}) => {
|
||||
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
||||
let url = (baseUrl ?? '') + pathUrl;
|
||||
if (path) {
|
||||
url = defaultPathSerializer({ path, url });
|
||||
}
|
||||
let search = query ? querySerializer(query) : '';
|
||||
if (search.startsWith('?')) {
|
||||
search = search.substring(1);
|
||||
}
|
||||
if (search) {
|
||||
url += `?${search}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export function getValidRequestBody(options: {
|
||||
body?: unknown;
|
||||
bodySerializer?: BodySerializer | null;
|
||||
serializedBody?: unknown;
|
||||
}) {
|
||||
const hasBody = options.body !== undefined;
|
||||
const isSerializedBody = hasBody && options.bodySerializer;
|
||||
|
||||
if (isSerializedBody) {
|
||||
if ('serializedBody' in options) {
|
||||
const hasSerializedBody =
|
||||
options.serializedBody !== undefined && options.serializedBody !== '';
|
||||
|
||||
return hasSerializedBody ? options.serializedBody : null;
|
||||
}
|
||||
|
||||
// not all clients implement a serializedBody property (i.e. client-axios)
|
||||
return options.body !== '' ? options.body : null;
|
||||
}
|
||||
|
||||
// plain/text body
|
||||
if (hasBody) {
|
||||
return options.body;
|
||||
}
|
||||
|
||||
// no body was provided
|
||||
return undefined;
|
||||
}
|
||||
4
lib/client/index.ts
Normal file
4
lib/client/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type * from './types.gen';
|
||||
export * from './sdk.gen';
|
||||
1316
lib/client/sdk.gen.ts
Normal file
1316
lib/client/sdk.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
4203
lib/client/types.gen.ts
Normal file
4203
lib/client/types.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
2465
lib/client/zod.gen.ts
Normal file
2465
lib/client/zod.gen.ts
Normal file
File diff suppressed because it is too large
Load Diff
6
next-env.d.ts
vendored
Normal file
6
next-env.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
12
next.config.mjs
Normal file
12
next.config.mjs
Normal file
@ -0,0 +1,12 @@
|
||||
import bundleAnalyzer from '@next/bundle-analyzer';
|
||||
|
||||
const withBundleAnalyzer = bundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
|
||||
export default withBundleAnalyzer({
|
||||
reactStrictMode: false,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
});
|
||||
31
openapi-ts.config.ts
Normal file
31
openapi-ts.config.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { defineConfig } from "@hey-api/openapi-ts";
|
||||
|
||||
export default defineConfig({
|
||||
input: "http://localhost:8000/openapi.json",
|
||||
output: "./lib/client",
|
||||
|
||||
|
||||
plugins: [
|
||||
"@hey-api/client-axios",
|
||||
"@tanstack/react-query",
|
||||
"@hey-api/typescript",
|
||||
{
|
||||
name: "zod",
|
||||
requests: true,
|
||||
definitions: true,
|
||||
metadata: true,
|
||||
dates: {
|
||||
offset: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "@hey-api/sdk",
|
||||
asClass: false,
|
||||
validator: "zod",
|
||||
},
|
||||
{
|
||||
name: "@hey-api/client-next",
|
||||
runtimeConfigPath: "@/ hey-api-config",
|
||||
},
|
||||
],
|
||||
});
|
||||
90
package.json
Normal file
90
package.json
Normal file
@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "mantine-next-template",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"analyze": "ANALYZE=true next build",
|
||||
"start": "next start",
|
||||
"export": "next build && next export",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "npm run eslint && npm run stylelint",
|
||||
"eslint": "eslint .",
|
||||
"stylelint": "stylelint '**/*.css' --cache",
|
||||
"jest": "jest",
|
||||
"jest:watch": "jest --watch",
|
||||
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
|
||||
"test": "npm run prettier:check && npm run lint && npm run typecheck && npm run jest",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build",
|
||||
"openapi-ts": "openapi-ts",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@hey-api/openapi-ts": "^0.85.2",
|
||||
"@mantine/core": "8.3.4",
|
||||
"@mantine/dates": "^8.3.5",
|
||||
"@mantine/form": "^8.3.5",
|
||||
"@mantine/hooks": "8.3.4",
|
||||
"@mantine/modals": "^8.3.5",
|
||||
"@mantine/notifications": "^8.3.5",
|
||||
"@next/bundle-analyzer": "^15.5.4",
|
||||
"@tabler/icons-react": "^3.35.0",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"axios": "^1.12.2",
|
||||
"clsx": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mantine-contextmenu": "^8.2.0",
|
||||
"mantine-datatable": "^8.2.0",
|
||||
"next": "15.5.4",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.4",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@eslint/js": "^9.37.0",
|
||||
"@hey-api/client-axios": "^0.9.1",
|
||||
"@hey-api/client-next": "^0.5.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
||||
"@storybook/addon-themes": "^9.1.10",
|
||||
"@storybook/nextjs": "^9.1.10",
|
||||
"@storybook/react": "^9.1.10",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/eslint-plugin-jsx-a11y": "^6",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.7.1",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"babel-loader": "^10.0.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-mantine": "^4.0.3",
|
||||
"eslint-config-next": "15.5.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-preset-mantine": "1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"storybook": "^9.1.10",
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint-config-standard-scss": "^16.0.0",
|
||||
"ts-jest": "^29.4.4",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "^8.46.0"
|
||||
},
|
||||
"packageManager": "yarn@4.10.3"
|
||||
}
|
||||
86
pages/_app.tsx
Normal file
86
pages/_app.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import '@mantine/core/styles.css';
|
||||
import 'mantine-datatable/styles.layer.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { AppShell, Box, Flex, MantineProvider, rem } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import Header from '@/components/Layout/Header/Header';
|
||||
import { Navbar } from '@/components/Layout/Navbar/Navbar';
|
||||
import { client } from '@/lib/client/client.gen';
|
||||
import { ReactQueryProvider } from '@/providers/ReactQueryProvider';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
client.setConfig({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
|
||||
});
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
|
||||
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
|
||||
|
||||
return (
|
||||
<ReactQueryProvider>
|
||||
<MantineProvider theme={theme}>
|
||||
<Head>
|
||||
<title>Mantine Template</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
|
||||
/>
|
||||
<link rel="shortcut icon" href="/favicon.svg" />
|
||||
</Head>
|
||||
<AppShell
|
||||
padding="md"
|
||||
header={{ height: '60px' }}
|
||||
navbar={{
|
||||
width: 250,
|
||||
breakpoint: 'sm',
|
||||
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
|
||||
}}
|
||||
>
|
||||
<AppShell.Header>
|
||||
<Header
|
||||
desktopOpened={desktopOpened}
|
||||
mobileOpened={mobileOpened}
|
||||
toggleDesktop={toggleDesktop}
|
||||
toggleMobile={toggleMobile}
|
||||
/>
|
||||
</AppShell.Header>
|
||||
<AppShell.Navbar>
|
||||
<Navbar />
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Main
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100dvh',
|
||||
minHeight: 0,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</Box>
|
||||
</Box>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
</MantineProvider>
|
||||
</ReactQueryProvider>
|
||||
);
|
||||
}
|
||||
16
pages/_document.tsx
Normal file
16
pages/_document.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { Head, Html, Main, NextScript } from 'next/document';
|
||||
import { ColorSchemeScript, mantineHtmlProps } from '@mantine/core';
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en" {...mantineHtmlProps}>
|
||||
<Head>
|
||||
<ColorSchemeScript />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
4
pages/deal-requests/index.tsx
Normal file
4
pages/deal-requests/index.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
const DealRequestsPage = () => {
|
||||
return <div>Deal Requests Page</div>;
|
||||
};
|
||||
export default DealRequestsPage;
|
||||
22
pages/index.tsx
Normal file
22
pages/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Flex } from '@mantine/core';
|
||||
import { AuthenticationForm } from '@/components/Forms/AuthenticationForm';
|
||||
|
||||
export default function HomePage() {
|
||||
const router = useRouter();
|
||||
// const [checking, setChecking] = useState(true);
|
||||
// useEffect(() => {
|
||||
// router.replace('/login').then(() => setChecking(false));
|
||||
// }, [router]);
|
||||
//
|
||||
// if (checking) {
|
||||
// return <p>Checking login status...</p>;
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
2
pages/layout.css
Normal file
2
pages/layout.css
Normal file
@ -0,0 +1,2 @@
|
||||
/* 👇 Make sure the styles are applied in the correct order */
|
||||
@layer mantine, mantine-datatable;
|
||||
4
pages/legal-entities/index.tsx
Normal file
4
pages/legal-entities/index.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
const LegalEntitiesPage = () => {
|
||||
return <div>Юр лица</div>;
|
||||
};
|
||||
export default LegalEntitiesPage;
|
||||
11
pages/login/index.tsx
Normal file
11
pages/login/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Flex } from '@mantine/core';
|
||||
import { AuthenticationForm } from '@/components/Forms/AuthenticationForm';
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<Flex justify="center" align="center" h="100vh">
|
||||
<AuthenticationForm />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
export default LoginPage;
|
||||
@ -0,0 +1,11 @@
|
||||
import { Avatar, Image, ThemeIcon } from '@mantine/core';
|
||||
import { BaseMarketplaceSchema } from '@/lib/client';
|
||||
|
||||
type Props = {
|
||||
object: BaseMarketplaceSchema;
|
||||
};
|
||||
const BaseMarketplaceColumnRender = ({ object }: Props) => {
|
||||
return <Avatar src={object.iconUrl} radius={"xs"} />;
|
||||
};
|
||||
|
||||
export default BaseMarketplaceColumnRender;
|
||||
@ -0,0 +1,41 @@
|
||||
import { IconCirclePlus } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ActionIcon, Affix, Tooltip } from '@mantine/core';
|
||||
|
||||
import BaseTable from '@/components/BaseTable/BaseTable';
|
||||
import { MarketplaceSchema } from '@/lib/client';
|
||||
import { getMarketplacesOptions } from '@/lib/client/@tanstack/react-query.gen';
|
||||
import useMarketplacesTableColumns from '@/pages/marketplaces/components/MarketplacesTable/columns';
|
||||
import { CRUDTableProps } from '@/types/CrudTable/CrudTable';
|
||||
|
||||
type Props = CRUDTableProps<MarketplaceSchema>;
|
||||
const MarketplacesTable = (props: Props) => {
|
||||
const { data, isLoading } = useQuery({
|
||||
...getMarketplacesOptions({ path: { clientId: 5 } }),
|
||||
});
|
||||
const onCreateClick = ()=>{
|
||||
|
||||
}
|
||||
|
||||
const columns = useMarketplacesTableColumns();
|
||||
return (
|
||||
<>
|
||||
<BaseTable
|
||||
scrollAreaProps={{ type: 'auto' }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
records={data?.items || []}
|
||||
columns={columns}
|
||||
fetching={isLoading}
|
||||
/>
|
||||
<Affix position={{ bottom: 40, right: 40 }}>
|
||||
<Tooltip label="Создать маркетплейс" withArrow>
|
||||
<ActionIcon onClick={() => {}} size="xl" variant="gradient" radius="xl">
|
||||
<IconCirclePlus />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Affix>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketplacesTable;
|
||||
57
pages/marketplaces/components/MarketplacesTable/columns.tsx
Normal file
57
pages/marketplaces/components/MarketplacesTable/columns.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IconSettings } from '@tabler/icons-react';
|
||||
import { DataTableColumn } from 'mantine-datatable';
|
||||
import { Center } from '@mantine/core';
|
||||
import BaseTableActions from '@/components/Tables/Actions/BaseTableActions';
|
||||
import { MarketplaceSchema } from '@/lib/client';
|
||||
import { zMarketplaceSchema } from '@/lib/client/zod.gen';
|
||||
import BaseMarketplaceColumnRender from '@/pages/marketplaces/components/BaseMarketplaceColumnRender/BaseMarketplaceColumnRender';
|
||||
|
||||
const useMarketplacesTableColumns = () => {
|
||||
return useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
accessor: 'id',
|
||||
title: 'ID',
|
||||
width: '0%',
|
||||
},
|
||||
{
|
||||
accessor: 'baseMarketplace',
|
||||
title: 'Базовый маркетплейс',
|
||||
render: ({ baseMarketplace }: MarketplaceSchema) => (
|
||||
<BaseMarketplaceColumnRender object={baseMarketplace} />
|
||||
),
|
||||
width: '0%',
|
||||
},
|
||||
{
|
||||
accessor: 'name',
|
||||
title: 'Название',
|
||||
},
|
||||
{
|
||||
width: '0%',
|
||||
accessor: 'actions',
|
||||
title: (
|
||||
<Center>
|
||||
<IconSettings size={16} />
|
||||
</Center>
|
||||
),
|
||||
render: (element) => (
|
||||
<BaseTableActions
|
||||
element={element}
|
||||
onChange={async () => {
|
||||
// const zodSchema = await zMarketplaceSchema.parseAsync(element)
|
||||
Object.entries(zMarketplaceSchema.shape).forEach(([key, val], index, arr) => {
|
||||
console.log(val.type);
|
||||
});
|
||||
}}
|
||||
onDelete={async () => {}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
] as DataTableColumn<MarketplaceSchema>[],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
export default useMarketplacesTableColumns;
|
||||
22
pages/marketplaces/index.tsx
Normal file
22
pages/marketplaces/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import ObjectForm from '@/components/ObjectForm/ObjectForm';
|
||||
import { MarketplaceSchema } from '@/lib/client';
|
||||
import { zMarketplaceSchema } from '@/lib/client/zod.gen';
|
||||
|
||||
const MarketplacesPage = () => {
|
||||
return (
|
||||
<>
|
||||
<ObjectForm<MarketplaceSchema, typeof zMarketplaceSchema>
|
||||
schema={zMarketplaceSchema}
|
||||
includeKeys={['name']}
|
||||
onCreate={(marketplace) => {}}
|
||||
labels={{
|
||||
name: 'Название',
|
||||
authData: 'Данные для авторизации',
|
||||
}}
|
||||
/>
|
||||
{/*<MarketplacesTable />*/}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketplacesPage;
|
||||
@ -0,0 +1,8 @@
|
||||
import { ContextModalProps } from '@mantine/modals';
|
||||
|
||||
type Props = {};
|
||||
|
||||
const MarketplaceFormModal = ({ context, id, innerProps }: ContextModalProps<Props>) => {
|
||||
return <div>MarketplaceFormModal</div>;
|
||||
};
|
||||
export default MarketplaceFormModal
|
||||
4
pages/products/index.tsx
Normal file
4
pages/products/index.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
const ProductsPage = () => {
|
||||
return <div>Товары</div>;
|
||||
};
|
||||
export default ProductsPage;
|
||||
14
postcss.config.cjs
Normal file
14
postcss.config.cjs
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
23
providers/MantineModalsProvider.tsx
Normal file
23
providers/MantineModalsProvider.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import MarketplaceFormModal from '@/pages/marketplaces/modals/MarketplaceFormModal/MarketplaceFormModal';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
const modals = {
|
||||
marketplaceForm: MarketplaceFormModal,
|
||||
};
|
||||
declare module '@mantine/modals' {
|
||||
export interface MantineModalsOverride {
|
||||
modals: typeof modals;
|
||||
}
|
||||
}
|
||||
const MantineModalsProvider = ({ children }: Props) => {
|
||||
return (
|
||||
<ModalsProvider labels={{ cancel: 'Отменить', confirm: 'Подтвердить' }} modals={modals}>
|
||||
{children}
|
||||
</ModalsProvider>
|
||||
);
|
||||
};
|
||||
export default MantineModalsProvider;
|
||||
34
providers/ReactQueryProvider.tsx
Normal file
34
providers/ReactQueryProvider.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useState } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function ReactQueryProvider({ children }: Props) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
1
public/favicon.svg
Normal file
1
public/favicon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><g fill="none" fill-rule="evenodd"><rect width="500" height="500" fill="#339AF0" rx="250"/><g fill="#FFF"><path fill-rule="nonzero" d="M202.055 135.706c-6.26 8.373-4.494 20.208 3.944 26.42 29.122 21.45 45.824 54.253 45.824 90.005 0 35.752-16.702 68.559-45.824 90.005-8.436 6.215-10.206 18.043-3.944 26.42 6.26 8.378 18.173 10.13 26.611 3.916a153.835 153.835 0 0024.509-22.54h53.93c10.506 0 19.023-8.455 19.023-18.885 0-10.43-8.517-18.886-19.023-18.886h-29.79c8.196-18.594 12.553-38.923 12.553-60.03s-4.357-41.436-12.552-60.03h29.79c10.505 0 19.022-8.455 19.022-18.885 0-10.43-8.517-18.886-19.023-18.886h-53.93a153.835 153.835 0 00-24.509-22.54c-8.438-6.215-20.351-4.46-26.61 3.916z"/><path d="M171.992 246.492c0-15.572 12.624-28.195 28.196-28.195 15.572 0 28.195 12.623 28.195 28.195 0 15.572-12.623 28.196-28.195 28.196-15.572 0-28.196-12.624-28.196-28.196z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 937 B |
4
public/logidex_dark.svg
Normal file
4
public/logidex_dark.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 755 844" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M345.254 26.7568C366.425 14.3739 392.647 14.4495 413.747 26.9541L703.914 198.921C724.423 211.076 737 233.148 737 256.989V588.794C737 612.793 724.257 634.986 703.532 647.086L413.368 816.491C392.459 828.698 366.614 828.772 345.636 816.685L51.3008 647.09C30.3877 635.04 17.5 612.74 17.5 588.604V257.18C17.5 233.201 30.2214 211.021 50.9199 198.914L345.254 26.7568Z" fill="white" stroke="black" stroke-width="35"/>
|
||||
<path d="M626.308 546.332C626.308 557.885 620.174 568.57 610.198 574.396L395.564 699.755C394.966 700.104 394.36 700.433 393.746 700.741C390.803 702.217 387.654 699.826 387.654 696.534V435.281C387.654 433.451 388.654 431.767 390.261 430.891L469.491 387.693C472.823 385.877 476.885 388.288 476.885 392.083V459.212C476.885 460.121 478 460.558 478.618 459.892L513.925 421.816C515.259 420.377 517.312 419.852 519.173 420.472L549.415 430.554C550.063 430.769 550.731 430.287 550.731 429.604V346.368C550.731 344.538 551.731 342.854 553.338 341.978L618.914 306.225C622.246 304.409 626.308 306.82 626.308 310.615V546.332ZM365.062 430.866C366.661 431.744 367.654 433.424 367.654 435.248V696.86C367.654 699.382 365.127 701.107 362.942 699.848L145.27 574.375C135.203 568.572 129 557.837 129 546.218V309.607C129 305.805 133.076 303.394 136.408 305.225L365.062 430.866ZM470.731 560.413C466.589 560.413 463.232 563.771 463.231 567.913V617.145C463.232 621.288 466.589 624.645 470.731 624.645C474.873 624.645 478.231 621.287 478.231 617.145V567.913C478.231 563.771 474.873 560.413 470.731 560.413ZM520.731 535.8C516.589 535.8 513.231 539.158 513.231 543.3V592.532C513.232 596.674 516.589 600.032 520.731 600.032C524.873 600.032 528.231 596.674 528.231 592.532V543.3C528.231 539.158 524.873 535.8 520.731 535.8ZM566.885 511.185C562.743 511.186 559.385 514.544 559.385 518.685V567.918C559.385 572.06 562.743 575.418 566.885 575.418C571.027 575.418 574.385 572.06 574.385 567.918V518.685C574.385 514.543 571.027 511.185 566.885 511.185ZM474.352 348.785C475.917 349.673 476.885 351.334 476.885 353.134V357.912C476.885 359.742 475.885 361.426 474.278 362.302L380.075 413.664C378.578 414.481 376.768 414.478 375.273 413.657L140.19 284.484C137.551 283.034 136.77 279.566 138.937 277.475C140.773 275.705 142.832 274.137 145.087 272.817L238.765 218.002C240.305 217.101 242.207 217.088 243.758 217.968L474.352 348.785ZM362.759 145.449C372.955 139.483 385.586 139.519 395.747 145.544L610.382 272.798C612.924 274.305 615.213 276.129 617.211 278.203C619.261 280.33 618.434 283.705 615.84 285.12L552.492 319.658C551.699 320.091 550.731 319.517 550.731 318.613C550.731 318.182 550.498 317.785 550.122 317.574L311.62 184.1C308.246 182.212 308.2 177.373 311.537 175.421L362.759 145.449Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
4
public/logidex_light.svg
Normal file
4
public/logidex_light.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="755" height="844" viewBox="0 0 755 844" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M345.254 26.7568C366.425 14.3739 392.647 14.4495 413.747 26.9541L703.914 198.921C724.423 211.076 737 233.148 737 256.989V588.794C737 612.793 724.257 634.986 703.532 647.086L413.368 816.491C392.459 828.698 366.614 828.772 345.636 816.685L51.3008 647.09C30.3877 635.04 17.5 612.74 17.5 588.604V257.18C17.5 233.201 30.2214 211.021 50.9199 198.914L345.254 26.7568Z" fill="white" stroke="#124F9A" stroke-width="35"/>
|
||||
<path d="M626.308 546.332C626.308 557.885 620.174 568.57 610.198 574.396L395.564 699.755C394.966 700.104 394.36 700.433 393.746 700.741C390.803 702.217 387.654 699.826 387.654 696.534V435.281C387.654 433.451 388.654 431.767 390.261 430.891L469.491 387.693C472.823 385.877 476.885 388.288 476.885 392.083V459.212C476.885 460.121 478 460.558 478.618 459.892L513.925 421.816C515.259 420.377 517.312 419.852 519.173 420.472L549.415 430.554C550.063 430.769 550.731 430.287 550.731 429.604V346.368C550.731 344.538 551.731 342.854 553.338 341.978L618.914 306.225C622.246 304.409 626.308 306.82 626.308 310.615V546.332ZM365.062 430.866C366.661 431.744 367.654 433.424 367.654 435.248V696.86C367.654 699.382 365.127 701.107 362.942 699.848L145.27 574.375C135.203 568.572 129 557.837 129 546.218V309.607C129 305.805 133.076 303.394 136.408 305.225L365.062 430.866ZM470.731 560.413C466.589 560.413 463.232 563.771 463.231 567.913V617.146C463.232 621.288 466.589 624.646 470.731 624.646C474.873 624.645 478.231 621.288 478.231 617.146V567.913C478.231 563.771 474.873 560.413 470.731 560.413ZM520.731 535.8C516.589 535.8 513.231 539.158 513.231 543.3V592.532C513.232 596.674 516.589 600.032 520.731 600.032C524.873 600.032 528.231 596.674 528.231 592.532V543.3C528.231 539.158 524.873 535.8 520.731 535.8ZM566.885 511.186C562.743 511.186 559.385 514.544 559.385 518.686V567.918C559.385 572.06 562.743 575.418 566.885 575.418C571.027 575.418 574.385 572.06 574.385 567.918V518.686C574.385 514.544 571.027 511.186 566.885 511.186ZM474.352 348.785C475.917 349.673 476.885 351.334 476.885 353.134V357.912C476.885 359.742 475.885 361.426 474.278 362.302L380.075 413.665C378.578 414.481 376.768 414.478 375.273 413.657L140.19 284.484C137.551 283.034 136.77 279.566 138.937 277.475C140.773 275.705 142.832 274.137 145.087 272.817L238.765 218.002C240.305 217.101 242.207 217.089 243.758 217.968L474.352 348.785ZM362.759 145.449C372.955 139.483 385.586 139.52 395.747 145.544L610.382 272.798C612.924 274.305 615.213 276.129 617.211 278.203C619.261 280.331 618.434 283.705 615.84 285.12L552.492 319.658C551.699 320.091 550.731 319.517 550.731 318.613C550.731 318.182 550.498 317.785 550.122 317.574L311.62 184.1C308.246 182.212 308.2 177.373 311.537 175.421L362.759 145.449Z" fill="#124F9A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
13
renovate.json
Normal file
13
renovate.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"schedule": ["before 5am on sunday"],
|
||||
"groupName": "all dependencies",
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackagePatterns": ["*"],
|
||||
"groupName": "all dependencies"
|
||||
}
|
||||
],
|
||||
"prHourlyLimit": 0,
|
||||
"prConcurrentLimit": 0
|
||||
}
|
||||
5
test-utils/index.ts
Normal file
5
test-utils/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export { render } from './render';
|
||||
export { userEvent };
|
||||
13
test-utils/render.tsx
Normal file
13
test-utils/render.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { render as testingLibraryRender } from '@testing-library/react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { theme } from '../theme';
|
||||
|
||||
export function render(ui: React.ReactNode) {
|
||||
return testingLibraryRender(<>{ui}</>, {
|
||||
wrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<MantineProvider theme={theme} env="test">
|
||||
{children}
|
||||
</MantineProvider>
|
||||
),
|
||||
});
|
||||
}
|
||||
5
theme.ts
Normal file
5
theme.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { createTheme } from '@mantine/core';
|
||||
|
||||
export const theme = createTheme({
|
||||
/* Put your mantine theme override here */
|
||||
});
|
||||
44
tsconfig.json
Normal file
44
tsconfig.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"node",
|
||||
"jest",
|
||||
"@testing-library/jest-dom"
|
||||
],
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".storybook/main.ts",
|
||||
".storybook/preview.tsx",
|
||||
"types"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"src/lib"
|
||||
]
|
||||
}
|
||||
5
types/CrudTable/CrudTable.tsx
Normal file
5
types/CrudTable/CrudTable.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export interface CRUDTableProps<T, C = T> {
|
||||
onCreate?: (item: C | T) => void;
|
||||
onDelete?: (item: T) => void;
|
||||
onChange?: (item: T) => void;
|
||||
}
|
||||
Reference in New Issue
Block a user