This commit is contained in:
2025-10-18 01:46:46 +03:00
commit 9baa68258e
74 changed files with 29125 additions and 0 deletions

6
hey-api-config.ts Normal file
View 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
View 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
View 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

View 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;

View File

@ -0,0 +1,5 @@
// helpers.ts (или рядом)
import type { DataTableProps } from 'mantine-datatable';
// Берём ИМЕННО ту ветку union, где есть `columns`
export type ColumnsMode<T> = Extract<DataTableProps<T>, { columns: unknown }>;

View 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>
);
}

View 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>
);
}

View 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} />;
}

View 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;

View 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;
}

View 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;

View 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>>;
};

View File

@ -0,0 +1,2 @@
type Props = {
}

View File

@ -0,0 +1,8 @@
import { Checkbox, CheckboxProps } from '@mantine/core';
type Props = CheckboxProps;
const BooleanField = (props: Props) => {
return <Checkbox {...props} />;
};
export default BooleanField;

View 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;

View File

@ -0,0 +1,9 @@
import { TextInput, TextInputProps } from '@mantine/core';
type Props = TextInputProps;
const StringField = (props: Props) => {
return <TextInput {...props} />;
};
export default StringField

View 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;

View 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;

View 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);
}
}

View File

@ -0,0 +1,7 @@
import { Welcome } from './Welcome';
export default {
title: 'Welcome',
};
export const Usage = () => <Welcome />;

View 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/'
);
});
});

View 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
View 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
View 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
View 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;

File diff suppressed because it is too large Load Diff

35
lib/client/client.gen.ts Normal file
View 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'
})));

View 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;
};

View 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';

View 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;

View 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,
});

View 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;
};

View 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();
},
};

View 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;
};

View 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 arent 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;
};

View 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;
};

View 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 };
};

View 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];
};

View 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
View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

6
next-env.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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>
);
}

View File

@ -0,0 +1,4 @@
const DealRequestsPage = () => {
return <div>Deal Requests Page</div>;
};
export default DealRequestsPage;

22
pages/index.tsx Normal file
View 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
View File

@ -0,0 +1,2 @@
/* 👇 Make sure the styles are applied in the correct order */
@layer mantine, mantine-datatable;

View File

@ -0,0 +1,4 @@
const LegalEntitiesPage = () => {
return <div>Юр лица</div>;
};
export default LegalEntitiesPage;

11
pages/login/index.tsx Normal file
View 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;

View File

@ -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;

View File

@ -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;

View 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;

View 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;

View File

@ -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
View File

@ -0,0 +1,4 @@
const ProductsPage = () => {
return <div>Товары</div>;
};
export default ProductsPage;

14
postcss.config.cjs Normal file
View 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',
},
},
},
};

View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"
]
}

View File

@ -0,0 +1,5 @@
export interface CRUDTableProps<T, C = T> {
onCreate?: (item: C | T) => void;
onDelete?: (item: T) => void;
onChange?: (item: T) => void;
}

16843
yarn.lock Normal file

File diff suppressed because it is too large Load Diff