crap
This commit is contained in:
3
.env
Normal file
3
.env
Normal file
@@ -0,0 +1,3 @@
|
||||
VITE_BOT_NAME=DencoFulfillmentTestBot
|
||||
VITE_BOT_ID=6467915685
|
||||
VITE_API_URL=http://test.crm.denco.store/api
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icons/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>DENCO: Fulfillment</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"generate-client": "openapi --input http://test.crm.denco.store/api/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hello-pangea/dnd": "^16.5.0",
|
||||
"@mantine/core": "^7.5.3",
|
||||
"@mantine/dates": "^7.5.3",
|
||||
"@mantine/dropzone": "^7.5.3",
|
||||
@@ -28,7 +30,8 @@
|
||||
"mantine-react-table": "^2.0.0-beta.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^9.1.0"
|
||||
"react-redux": "^9.1.0",
|
||||
"reactflow": "^11.10.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.56",
|
||||
@@ -39,6 +42,7 @@
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"openapi-typescript-codegen": "^0.27.0",
|
||||
"postcss": "^8.4.35",
|
||||
"postcss-preset-mantine": "^1.13.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
|
||||
BIN
public/icons/favicon.png
Normal file
BIN
public/icons/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 360 KiB |
BIN
public/icons/icon.png
Normal file
BIN
public/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
public/icons/logo-light.png
Normal file
BIN
public/icons/logo-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/icons/logo.png
Normal file
BIN
public/icons/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
42
src/App.css
42
src/App.css
@@ -1,42 +0,0 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import '@mantine/core/styles.css';
|
||||
|
||||
|
||||
25
src/client/core/ApiError.ts
Normal file
25
src/client/core/ApiError.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly url: string;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly body: any;
|
||||
public readonly request: ApiRequestOptions;
|
||||
|
||||
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
||||
super(message);
|
||||
|
||||
this.name = 'ApiError';
|
||||
this.url = response.url;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.body = response.body;
|
||||
this.request = request;
|
||||
}
|
||||
}
|
||||
17
src/client/core/ApiRequestOptions.ts
Normal file
17
src/client/core/ApiRequestOptions.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiRequestOptions = {
|
||||
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
|
||||
readonly url: string;
|
||||
readonly path?: Record<string, any>;
|
||||
readonly cookies?: Record<string, any>;
|
||||
readonly headers?: Record<string, any>;
|
||||
readonly query?: Record<string, any>;
|
||||
readonly formData?: Record<string, any>;
|
||||
readonly body?: any;
|
||||
readonly mediaType?: string;
|
||||
readonly responseHeader?: string;
|
||||
readonly errors?: Record<number, string>;
|
||||
};
|
||||
11
src/client/core/ApiResult.ts
Normal file
11
src/client/core/ApiResult.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiResult = {
|
||||
readonly url: string;
|
||||
readonly ok: boolean;
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly body: any;
|
||||
};
|
||||
131
src/client/core/CancelablePromise.ts
Normal file
131
src/client/core/CancelablePromise.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export class CancelError extends Error {
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'CancelError';
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OnCancel {
|
||||
readonly isResolved: boolean;
|
||||
readonly isRejected: boolean;
|
||||
readonly isCancelled: boolean;
|
||||
|
||||
(cancelHandler: () => void): void;
|
||||
}
|
||||
|
||||
export class CancelablePromise<T> implements Promise<T> {
|
||||
#isResolved: boolean;
|
||||
#isRejected: boolean;
|
||||
#isCancelled: boolean;
|
||||
readonly #cancelHandlers: (() => void)[];
|
||||
readonly #promise: Promise<T>;
|
||||
#resolve?: (value: T | PromiseLike<T>) => void;
|
||||
#reject?: (reason?: any) => void;
|
||||
|
||||
constructor(
|
||||
executor: (
|
||||
resolve: (value: T | PromiseLike<T>) => void,
|
||||
reject: (reason?: any) => void,
|
||||
onCancel: OnCancel
|
||||
) => void
|
||||
) {
|
||||
this.#isResolved = false;
|
||||
this.#isRejected = false;
|
||||
this.#isCancelled = false;
|
||||
this.#cancelHandlers = [];
|
||||
this.#promise = new Promise<T>((resolve, reject) => {
|
||||
this.#resolve = resolve;
|
||||
this.#reject = reject;
|
||||
|
||||
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isResolved = true;
|
||||
if (this.#resolve) this.#resolve(value);
|
||||
};
|
||||
|
||||
const onReject = (reason?: any): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isRejected = true;
|
||||
if (this.#reject) this.#reject(reason);
|
||||
};
|
||||
|
||||
const onCancel = (cancelHandler: () => void): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#cancelHandlers.push(cancelHandler);
|
||||
};
|
||||
|
||||
Object.defineProperty(onCancel, 'isResolved', {
|
||||
get: (): boolean => this.#isResolved,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isRejected', {
|
||||
get: (): boolean => this.#isRejected,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isCancelled', {
|
||||
get: (): boolean => this.#isCancelled,
|
||||
});
|
||||
|
||||
return executor(onResolve, onReject, onCancel as OnCancel);
|
||||
});
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return "Cancellable Promise";
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.#promise.then(onFulfilled, onRejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
|
||||
): Promise<T | TResult> {
|
||||
return this.#promise.catch(onRejected);
|
||||
}
|
||||
|
||||
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||
return this.#promise.finally(onFinally);
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isCancelled = true;
|
||||
if (this.#cancelHandlers.length) {
|
||||
try {
|
||||
for (const cancelHandler of this.#cancelHandlers) {
|
||||
cancelHandler();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Cancellation threw an error', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.#cancelHandlers.length = 0;
|
||||
if (this.#reject) this.#reject(new CancelError('Request aborted'));
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return this.#isCancelled;
|
||||
}
|
||||
}
|
||||
32
src/client/core/OpenAPI.ts
Normal file
32
src/client/core/OpenAPI.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
||||
TOKEN?: string | Resolver<string> | undefined;
|
||||
USERNAME?: string | Resolver<string> | undefined;
|
||||
PASSWORD?: string | Resolver<string> | undefined;
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: '',
|
||||
VERSION: '0.1.0',
|
||||
WITH_CREDENTIALS: false,
|
||||
CREDENTIALS: 'include',
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
ENCODE_PATH: undefined,
|
||||
};
|
||||
322
src/client/core/request.ts
Normal file
322
src/client/core/request.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import axios from 'axios';
|
||||
import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios';
|
||||
import FormData from 'form-data';
|
||||
|
||||
import { ApiError } from './ApiError';
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
import { CancelablePromise } from './CancelablePromise';
|
||||
import type { OnCancel } from './CancelablePromise';
|
||||
import type { OpenAPIConfig } from './OpenAPI';
|
||||
|
||||
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
|
||||
return value !== undefined && value !== null;
|
||||
};
|
||||
|
||||
export const isString = (value: any): value is string => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
export const isStringWithValue = (value: any): value is string => {
|
||||
return isString(value) && value !== '';
|
||||
};
|
||||
|
||||
export const isBlob = (value: any): value is Blob => {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
typeof value.type === 'string' &&
|
||||
typeof value.stream === 'function' &&
|
||||
typeof value.arrayBuffer === 'function' &&
|
||||
typeof value.constructor === 'function' &&
|
||||
typeof value.constructor.name === 'string' &&
|
||||
/^(Blob|File)$/.test(value.constructor.name) &&
|
||||
/^(Blob|File)$/.test(value[Symbol.toStringTag])
|
||||
);
|
||||
};
|
||||
|
||||
export const isFormData = (value: any): value is FormData => {
|
||||
return value instanceof FormData;
|
||||
};
|
||||
|
||||
export const isSuccess = (status: number): boolean => {
|
||||
return status >= 200 && status < 300;
|
||||
};
|
||||
|
||||
export const base64 = (str: string): string => {
|
||||
try {
|
||||
return btoa(str);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
return Buffer.from(str).toString('base64');
|
||||
}
|
||||
};
|
||||
|
||||
export const getQueryString = (params: Record<string, any>): string => {
|
||||
const qs: string[] = [];
|
||||
|
||||
const append = (key: string, value: any) => {
|
||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
};
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isDefined(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
process(key, v);
|
||||
});
|
||||
} else if (typeof value === 'object') {
|
||||
Object.entries(value).forEach(([k, v]) => {
|
||||
process(`${key}[${k}]`, v);
|
||||
});
|
||||
} else {
|
||||
append(key, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
process(key, value);
|
||||
});
|
||||
|
||||
if (qs.length > 0) {
|
||||
return `?${qs.join('&')}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||
const encoder = config.ENCODE_PATH || encodeURI;
|
||||
|
||||
const path = options.url
|
||||
.replace('{api-version}', config.VERSION)
|
||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||
if (options.path?.hasOwnProperty(group)) {
|
||||
return encoder(String(options.path[group]));
|
||||
}
|
||||
return substring;
|
||||
});
|
||||
|
||||
const url = `${config.BASE}${path}`;
|
||||
if (options.query) {
|
||||
return `${url}${getQueryString(options.query)}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
|
||||
if (options.formData) {
|
||||
const formData = new FormData();
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isString(value) || isBlob(value)) {
|
||||
formData.append(key, value);
|
||||
} else {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(options.formData)
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => process(key, v));
|
||||
} else {
|
||||
process(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
|
||||
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
|
||||
if (typeof resolver === 'function') {
|
||||
return (resolver as Resolver<T>)(options);
|
||||
}
|
||||
return resolver;
|
||||
};
|
||||
|
||||
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => {
|
||||
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||
resolve(options, config.TOKEN),
|
||||
resolve(options, config.USERNAME),
|
||||
resolve(options, config.PASSWORD),
|
||||
resolve(options, config.HEADERS),
|
||||
]);
|
||||
|
||||
const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {}
|
||||
|
||||
const headers = Object.entries({
|
||||
Accept: 'application/json',
|
||||
...additionalHeaders,
|
||||
...options.headers,
|
||||
...formHeaders,
|
||||
})
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.reduce((headers, [key, value]) => ({
|
||||
...headers,
|
||||
[key]: String(value),
|
||||
}), {} as Record<string, string>);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = base64(`${username}:${password}`);
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (options.mediaType) {
|
||||
headers['Content-Type'] = options.mediaType;
|
||||
} else if (isBlob(options.body)) {
|
||||
headers['Content-Type'] = options.body.type || 'application/octet-stream';
|
||||
} else if (isString(options.body)) {
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
} else if (!isFormData(options.body)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
};
|
||||
|
||||
export const getRequestBody = (options: ApiRequestOptions): any => {
|
||||
if (options.body) {
|
||||
return options.body;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const sendRequest = async <T>(
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions,
|
||||
url: string,
|
||||
body: any,
|
||||
formData: FormData | undefined,
|
||||
headers: Record<string, string>,
|
||||
onCancel: OnCancel,
|
||||
axiosClient: AxiosInstance
|
||||
): Promise<AxiosResponse<T>> => {
|
||||
const source = axios.CancelToken.source();
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
url,
|
||||
headers,
|
||||
data: body ?? formData,
|
||||
method: options.method,
|
||||
withCredentials: config.WITH_CREDENTIALS,
|
||||
cancelToken: source.token,
|
||||
};
|
||||
|
||||
onCancel(() => source.cancel('The user aborted a request.'));
|
||||
|
||||
try {
|
||||
return await axiosClient.request(requestConfig);
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<T>;
|
||||
if (axiosError.response) {
|
||||
return axiosError.response;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getResponseHeader = (response: AxiosResponse<any>, responseHeader?: string): string | undefined => {
|
||||
if (responseHeader) {
|
||||
const content = response.headers[responseHeader];
|
||||
if (isString(content)) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getResponseBody = (response: AxiosResponse<any>): any => {
|
||||
if (response.status !== 204) {
|
||||
return response.data;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
|
||||
const errors: Record<number, string> = {
|
||||
400: 'Bad Request',
|
||||
401: 'Unauthorized',
|
||||
403: 'Forbidden',
|
||||
404: 'Not Found',
|
||||
500: 'Internal Server Error',
|
||||
502: 'Bad Gateway',
|
||||
503: 'Service Unavailable',
|
||||
...options.errors,
|
||||
}
|
||||
|
||||
const error = errors[result.status];
|
||||
if (error) {
|
||||
throw new ApiError(options, result, error);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
const errorStatus = result.status ?? 'unknown';
|
||||
const errorStatusText = result.statusText ?? 'unknown';
|
||||
const errorBody = (() => {
|
||||
try {
|
||||
return JSON.stringify(result.body, null, 2);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
throw new ApiError(options, result,
|
||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request method
|
||||
* @param config The OpenAPI configuration object
|
||||
* @param options The request options from the service
|
||||
* @param axiosClient The axios client instance to use
|
||||
* @returns CancelablePromise<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise<T> => {
|
||||
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||
try {
|
||||
const url = getUrl(config, options);
|
||||
const formData = getFormData(options);
|
||||
const body = getRequestBody(options);
|
||||
const headers = await getHeaders(config, options, formData);
|
||||
|
||||
if (!onCancel.isCancelled) {
|
||||
const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient);
|
||||
const responseBody = getResponseBody(response);
|
||||
const responseHeader = getResponseHeader(response, options.responseHeader);
|
||||
|
||||
const result: ApiResult = {
|
||||
url,
|
||||
ok: isSuccess(response.status),
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: responseHeader ?? responseBody,
|
||||
};
|
||||
|
||||
catchErrorCodes(options, result);
|
||||
|
||||
resolve(result.body);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
15
src/client/index.ts
Normal file
15
src/client/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export { ApiError } from './core/ApiError';
|
||||
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
||||
export { OpenAPI } from './core/OpenAPI';
|
||||
export type { OpenAPIConfig } from './core/OpenAPI';
|
||||
|
||||
export type { AuthLoginRequest } from './models/AuthLoginRequest';
|
||||
export type { AuthLoginResponse } from './models/AuthLoginResponse';
|
||||
export type { HTTPValidationError } from './models/HTTPValidationError';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
|
||||
export { AuthService } from './services/AuthService';
|
||||
12
src/client/models/AuthLoginRequest.ts
Normal file
12
src/client/models/AuthLoginRequest.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type AuthLoginRequest = {
|
||||
auth_date: number;
|
||||
first_name: string;
|
||||
hash: string;
|
||||
id: number;
|
||||
photo_url: string;
|
||||
};
|
||||
|
||||
8
src/client/models/AuthLoginResponse.ts
Normal file
8
src/client/models/AuthLoginResponse.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type AuthLoginResponse = {
|
||||
access_token: string;
|
||||
};
|
||||
|
||||
9
src/client/models/HTTPValidationError.ts
Normal file
9
src/client/models/HTTPValidationError.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ValidationError } from './ValidationError';
|
||||
export type HTTPValidationError = {
|
||||
detail?: Array<ValidationError>;
|
||||
};
|
||||
|
||||
10
src/client/models/ValidationError.ts
Normal file
10
src/client/models/ValidationError.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ValidationError = {
|
||||
loc: Array<(string | number)>;
|
||||
msg: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
31
src/client/services/AuthService.ts
Normal file
31
src/client/services/AuthService.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { AuthLoginRequest } from '../models/AuthLoginRequest';
|
||||
import type { AuthLoginResponse } from '../models/AuthLoginResponse';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class AuthService {
|
||||
/**
|
||||
* Login
|
||||
* @returns AuthLoginResponse Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static loginAuthLoginPost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: AuthLoginRequest,
|
||||
}): CancelablePromise<AuthLoginResponse> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/auth/login',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
24
src/components/Dnd/Board/Board.module.css
Normal file
24
src/components/Dnd/Board/Board.module.css
Normal file
@@ -0,0 +1,24 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/*background-color: green;*/
|
||||
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
/*border: solid var(--item-border-size) var(--mantine-color-default-border);*/
|
||||
/*border-radius: var(--item-border-radius);*/
|
||||
}
|
||||
|
||||
.items-list {
|
||||
gap: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/*background-color: red;*/
|
||||
height: 100%;
|
||||
}
|
||||
53
src/components/Dnd/Board/Board.tsx
Normal file
53
src/components/Dnd/Board/Board.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import {FC} from "react";
|
||||
import styles from './Board.module.css';
|
||||
import {Divider, Text, Title} from '@mantine/core';
|
||||
import {Draggable, Droppable} from "@hello-pangea/dnd";
|
||||
import CreateLeadButton from "../CreateLeadButton/CreateLeadButton.tsx";
|
||||
|
||||
type Props = {
|
||||
droppableId: string;
|
||||
title: string;
|
||||
withCreateButton?: boolean;
|
||||
}
|
||||
|
||||
export const Board: FC<Props> = ({droppableId, title, withCreateButton = false}) => {
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles["container"]}>
|
||||
<div className={styles["header"]}>
|
||||
<Title size={"h4"}>{title}</Title>
|
||||
<Text>12 сделок: 500р</Text>
|
||||
<Divider size={"xl"} my={10} color={"blue"}/>
|
||||
</div>
|
||||
<Droppable droppableId={droppableId}>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef} className={styles["items-list"]}>
|
||||
{withCreateButton &&
|
||||
<CreateLeadButton
|
||||
|
||||
onClick={() => {
|
||||
}}
|
||||
/>}
|
||||
<Draggable draggableId={droppableId + '1'} index={1}>
|
||||
{(provided, snapshot) => (
|
||||
<div {...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Board;
|
||||
@@ -0,0 +1,19 @@
|
||||
.container {
|
||||
/*background-color: red;*/
|
||||
min-height: 5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
background-color: light-dark(var(--mantine-color-default-hover), var(--mantine-color-gray-filled-hover));
|
||||
|
||||
}
|
||||
60
src/components/Dnd/CreateLeadButton/CreateLeadButton.tsx
Normal file
60
src/components/Dnd/CreateLeadButton/CreateLeadButton.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, {FC, useState} from "react";
|
||||
|
||||
import styles from './CreateLeadButton.module.css';
|
||||
import {Button, Center, rem, Text, TextInput, Transition} from '@mantine/core';
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
}
|
||||
const CreateLeadButton: FC<Props> = ({onClick}) => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [showButton, setShowButton] = useState(true);
|
||||
console.log(`isCreating: ${isCreating}`)
|
||||
console.log(`showButton: ${showButton}`)
|
||||
return (
|
||||
<div className={styles['container']}
|
||||
onClick={() => {
|
||||
if (isCreating) return;
|
||||
setIsCreating(prevState => !prevState)
|
||||
setShowButton(false);
|
||||
}}
|
||||
>
|
||||
|
||||
{(!isCreating && showButton) &&
|
||||
<Text>Быстрое добавление</Text>
|
||||
}
|
||||
<Transition
|
||||
mounted={isCreating}
|
||||
transition={'scale-y'}
|
||||
duration={300}
|
||||
// onExited={()=>setShowButton(true)}
|
||||
keepMounted
|
||||
>
|
||||
{(styles) => <div style={{...styles, width: '100%'}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: rem(10),
|
||||
padding: rem(10)
|
||||
}}>
|
||||
<div style={{display: "flex", flexDirection: "column", width: '100%'}}>
|
||||
<TextInput placeholder={'Название'} w={'100%'}/>
|
||||
|
||||
</div>
|
||||
|
||||
<div style={{display: "flex", flexDirection: "column", width: '100%'}}>
|
||||
<TextInput placeholder={'Компания: название'} w={'100%'}/>
|
||||
<TextInput placeholder={'Компания: адрес'} w={'100%'}/>
|
||||
</div>
|
||||
<div style={{gap:rem(10), display:"flex"}}>
|
||||
<Button>Добавить</Button>
|
||||
<Button variant={'outline'} onClick={()=>setIsCreating(false)}>Отменить</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</Transition>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default CreateLeadButton;
|
||||
19
src/components/DndList.module.css
Normal file
19
src/components/DndList.module.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--mantine-radius-md);
|
||||
border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-xl);
|
||||
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-5));
|
||||
margin-bottom: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.itemDragging {
|
||||
box-shadow: var(--mantine-shadow-sm);
|
||||
}
|
||||
|
||||
.symbol {
|
||||
font-size: rem(30px);
|
||||
font-weight: 700;
|
||||
width: rem(60px);
|
||||
}
|
||||
34
src/components/Navbar/LinksGroup/LinksGroup.module.css
Normal file
34
src/components/Navbar/LinksGroup/LinksGroup.module.css
Normal file
@@ -0,0 +1,34 @@
|
||||
.control {
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
color: var(--mantine-color-text);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
margin-left: var(--mantine-spacing-xl);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
border-left: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
73
src/components/Navbar/LinksGroup/LinksGroup.tsx
Normal file
73
src/components/Navbar/LinksGroup/LinksGroup.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import {useState} from 'react';
|
||||
import {Box, Collapse, Group, rem, ThemeIcon, UnstyledButton} from '@mantine/core';
|
||||
import {IconCalendarStats, IconChevronRight} from '@tabler/icons-react';
|
||||
import classes from './LinksGroup.module.css';
|
||||
import {Link} from "@tanstack/react-router";
|
||||
|
||||
interface LinksGroupProps {
|
||||
icon: React.FC<any>;
|
||||
label: string;
|
||||
initiallyOpened?: boolean;
|
||||
links?: { label: string; link: string }[];
|
||||
}
|
||||
|
||||
export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGroupProps) {
|
||||
const hasLinks = Array.isArray(links);
|
||||
const [opened, setOpened] = useState(initiallyOpened || false);
|
||||
const items = (hasLinks ? links : []).map((link) => (
|
||||
<Link to={link.link}
|
||||
className={classes.link}
|
||||
key={link.label}
|
||||
|
||||
>
|
||||
|
||||
{link.label}
|
||||
</Link>
|
||||
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnstyledButton onClick={() => setOpened((o) => !o)} className={classes.control}>
|
||||
<Group justify="space-between" gap={0}>
|
||||
<Box style={{display: 'flex', alignItems: 'center'}}>
|
||||
<ThemeIcon variant="light" size={30}>
|
||||
<Icon style={{width: rem(18), height: rem(18)}}/>
|
||||
</ThemeIcon>
|
||||
<Box ml="md">{label}</Box>
|
||||
</Box>
|
||||
{hasLinks && (
|
||||
<IconChevronRight
|
||||
className={classes.chevron}
|
||||
stroke={1.5}
|
||||
style={{
|
||||
width: rem(16),
|
||||
height: rem(16),
|
||||
transform: opened ? 'rotate(-90deg)' : 'none',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
{hasLinks ? <Collapse in={opened}>{items}</Collapse> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const mockdata = {
|
||||
label: 'Releases',
|
||||
icon: IconCalendarStats,
|
||||
links: [
|
||||
{label: 'Upcoming releases', link: '/'},
|
||||
{label: 'Previous releases', link: '/'},
|
||||
{label: 'Releases schedule', link: '/'},
|
||||
],
|
||||
};
|
||||
|
||||
export function NavbarLinksGroup() {
|
||||
return (
|
||||
<Box mih={220} p="md">
|
||||
<LinksGroup {...mockdata} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
37
src/components/Navbar/Logo.tsx
Normal file
37
src/components/Navbar/Logo.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
return (
|
||||
<svg {...props} version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1024.000000 1024.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
|
||||
fill="#2a75ec" stroke="none">
|
||||
<path d="M2600 7290 l0 -760 690 0 690 0 0 196 0 195 728 -4 c613 -3 743 -7
|
||||
831 -20 57 -10 106 -15 108 -13 7 7 341 -80 358 -93 8 -7 79 -34 95 -36 28 -4
|
||||
293 -151 298 -166 2 -5 8 -9 13 -9 9 0 75 -51 177 -138 44 -38 120 -123 218
|
||||
-245 73 -91 215 -390 230 -484 3 -21 8 -42 11 -46 20 -33 46 -190 58 -355 12
|
||||
-165 12 -219 0 -385 -12 -164 -38 -321 -58 -354 -3 -4 -8 -25 -11 -46 -15 -95
|
||||
-157 -394 -230 -484 -96 -119 -128 -156 -176 -203 -65 -65 -206 -180 -219
|
||||
-180 -5 0 -11 -4 -13 -9 -7 -20 -253 -151 -333 -177 -27 -9 -55 -20 -60 -25
|
||||
-6 -4 -45 -17 -88 -28 -42 -12 -111 -30 -152 -41 -42 -11 -88 -21 -103 -22
|
||||
-15 0 -67 -8 -117 -16 -72 -12 -238 -16 -827 -19 l-738 -4 0 245 0 246 -687
|
||||
-2 -688 -3 -3 -808 -2 -807 1487 0 c1611 0 1542 -2 1908 56 88 14 207 37 265
|
||||
51 108 27 369 106 405 123 11 5 52 21 90 36 168 65 524 255 588 314 11 11 27
|
||||
20 34 20 7 0 13 3 13 8 0 4 39 37 88 72 160 119 365 330 507 525 58 79 122
|
||||
173 132 195 4 8 22 39 41 68 18 29 35 61 37 70 2 10 17 41 34 70 17 29 31 57
|
||||
31 63 0 5 11 32 25 59 28 55 78 194 102 280 8 30 26 94 39 141 13 48 28 120
|
||||
34 160 5 41 17 121 27 179 26 154 25 728 -1 880 -9 58 -22 137 -27 175 -5 39
|
||||
-20 111 -33 160 -13 50 -30 113 -37 140 -7 28 -23 74 -36 104 -12 29 -23 60
|
||||
-23 67 0 8 -16 48 -35 90 -19 41 -35 79 -35 84 0 5 -14 33 -31 62 -17 29 -33
|
||||
61 -35 71 -2 11 -10 27 -18 36 -7 9 -24 36 -36 60 -12 24 -26 49 -32 55 -8 9
|
||||
-40 57 -103 155 -5 9 -15 20 -21 26 -6 5 -40 48 -76 95 -127 165 -291 325
|
||||
-473 459 -38 29 -83 62 -98 74 -16 12 -40 29 -55 37 -15 8 -67 39 -115 69 -99
|
||||
59 -339 177 -443 216 -245 93 -524 167 -774 205 -335 51 -306 50 -1877 50
|
||||
l-1473 0 0 -760z"/>
|
||||
<path d="M1730 5175 l0 -1095 1580 0 1580 0 0 1095 0 1095 -1580 0 -1580 0 0
|
||||
-1095z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
)
|
||||
}
|
||||
36
src/components/Navbar/Navbar.module.css
Normal file
36
src/components/Navbar/Navbar.module.css
Normal file
@@ -0,0 +1,36 @@
|
||||
.navbar {
|
||||
width: rem(80px);
|
||||
height: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
}
|
||||
|
||||
.navbarMain {
|
||||
flex: 1;
|
||||
margin-top: rem(50px);
|
||||
}
|
||||
|
||||
.link {
|
||||
width: rem(50px);
|
||||
height: rem(50px);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
|
||||
&:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
&,
|
||||
&:hover {
|
||||
background-color: var(--mantine-color-blue-light);
|
||||
color: var(--mantine-color-blue-light-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
89
src/components/Navbar/Navbar.tsx
Normal file
89
src/components/Navbar/Navbar.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import {Center, Image, rem, Stack, Tooltip, UnstyledButton, useMantineColorScheme} from '@mantine/core';
|
||||
import {IconCash, IconHome2, IconLogout, IconMoon, IconSun,} from '@tabler/icons-react';
|
||||
import classes from './Navbar.module.css';
|
||||
import {useAppDispatch} from "../../redux/store.ts";
|
||||
import {logout} from "../../features/authSlice.ts";
|
||||
import {useNavigate, useRouterState} from "@tanstack/react-router";
|
||||
|
||||
interface NavbarLinkProps {
|
||||
icon: typeof IconHome2;
|
||||
label?: string;
|
||||
active?: boolean;
|
||||
href: string;
|
||||
|
||||
onClick?(navlink: NavbarLinkProps): void;
|
||||
|
||||
index: number;
|
||||
}
|
||||
|
||||
function NavbarLink(props: NavbarLinkProps) {
|
||||
const {icon: Icon, label, active, onClick} = props;
|
||||
return (
|
||||
<Tooltip display={!label ? "none" : "flex"} label={label} position="right" transitionProps={{duration: 0}}>
|
||||
<UnstyledButton onClick={() => onClick && onClick(props)}
|
||||
className={classes.link}
|
||||
data-active={active || undefined}>
|
||||
<Icon style={{width: rem(20), height: rem(20)}} stroke={1.5}/>
|
||||
</UnstyledButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const mockdata = [
|
||||
{
|
||||
icon: IconHome2,
|
||||
label: 'Главная',
|
||||
href: '/'
|
||||
},
|
||||
{
|
||||
icon: IconCash,
|
||||
label: 'Сделки',
|
||||
href: '/leads'
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouterState();
|
||||
const {colorScheme, toggleColorScheme} = useMantineColorScheme({keepTransitions: false});
|
||||
const onLogoutClick = () => {
|
||||
dispatch(logout());
|
||||
navigate({to: '/login'});
|
||||
}
|
||||
const onNavlinkClick = (props: NavbarLinkProps) => {
|
||||
navigate({to: props.href});
|
||||
}
|
||||
const links = mockdata.map((link, index) => (
|
||||
<NavbarLink
|
||||
{...link}
|
||||
index={index}
|
||||
key={link.label}
|
||||
active={router.location.pathname === link.href}
|
||||
onClick={onNavlinkClick}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<nav className={classes.navbar}>
|
||||
<Center>
|
||||
<Image
|
||||
style={{filter: "drop-shadow(0 0 30px #fff)"}}
|
||||
src={colorScheme == "dark" ? "/icons/logo-light.png" : "/icons/logo.png"}
|
||||
/>
|
||||
</Center>
|
||||
|
||||
<div className={classes.navbarMain}>
|
||||
<Stack justify="center" gap={rem(10)}>
|
||||
{links}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Stack justify="center" gap={0}>
|
||||
<NavbarLink label={"Сменить тему"} onClick={toggleColorScheme} icon={colorScheme == "dark" ? IconSun : IconMoon} href={"#"} index={-1}/>
|
||||
<NavbarLink index={-1} href={"#"} onClick={onLogoutClick} icon={IconLogout} label="Выйти"/>
|
||||
</Stack>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
10
src/components/Navbar/UserButton/UserButton.module.css
Normal file
10
src/components/Navbar/UserButton/UserButton.module.css
Normal file
@@ -0,0 +1,10 @@
|
||||
.user {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
}
|
||||
}
|
||||
28
src/components/Navbar/UserButton/UserButton.tsx
Normal file
28
src/components/Navbar/UserButton/UserButton.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core';
|
||||
import { IconChevronRight } from '@tabler/icons-react';
|
||||
import classes from './UserButton.module.css';
|
||||
|
||||
export function UserButton() {
|
||||
return (
|
||||
<UnstyledButton className={classes.user}>
|
||||
<Group>
|
||||
<Avatar
|
||||
src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-8.png"
|
||||
radius="xl"
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
Harriette Spoonlicker
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
hspoonlicker@outlook.com
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<IconChevronRight style={{ width: rem(14), height: rem(14) }} stroke={1.5} />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
25
src/components/Navbar/data.ts
Normal file
25
src/components/Navbar/data.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {IconAdjustments, IconGauge} from "@tabler/icons-react";
|
||||
|
||||
export const NavbarLinks = [
|
||||
{
|
||||
label: 'Главная',
|
||||
icon: IconGauge,
|
||||
links: [
|
||||
{
|
||||
label: "123",
|
||||
link: "/login"
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Настройки',
|
||||
icon: IconAdjustments,
|
||||
links:[
|
||||
{
|
||||
label: "Профиль"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
];
|
||||
114
src/components/TelegramAuthButton/TelegramAuthButton.tsx
Normal file
114
src/components/TelegramAuthButton/TelegramAuthButton.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
|
||||
export interface TelegramUser {
|
||||
id: number
|
||||
first_name: string
|
||||
username: string
|
||||
photo_url: string
|
||||
auth_date: number
|
||||
hash: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
botName: string
|
||||
usePic?: boolean
|
||||
className?: string
|
||||
cornerRadius?: number
|
||||
requestAccess?: boolean
|
||||
dataAuthUrl?: string
|
||||
dataOnauth?: (user: TelegramUser) => void
|
||||
buttonSize?: 'large' | 'medium' | 'small'
|
||||
wrapperProps?: React.HTMLProps<HTMLDivElement>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
TelegramLoginWidget: {
|
||||
dataOnauth: (user: TelegramUser) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const TelegramLoginButton: React.FC<Props> = ({
|
||||
wrapperProps,
|
||||
dataAuthUrl,
|
||||
usePic = false,
|
||||
botName,
|
||||
className,
|
||||
buttonSize = 'large',
|
||||
dataOnauth,
|
||||
cornerRadius,
|
||||
requestAccess = true
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current === null) return
|
||||
|
||||
if (
|
||||
typeof dataOnauth === 'undefined' &&
|
||||
typeof dataAuthUrl === 'undefined'
|
||||
) {
|
||||
throw new Error(
|
||||
'One of this props should be defined: dataAuthUrl (redirect URL), dataOnauth (callback fn) should be defined.'
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof dataOnauth === 'function') {
|
||||
window.TelegramLoginWidget = {
|
||||
dataOnauth: (user: TelegramUser) => dataOnauth(user)
|
||||
}
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://telegram.org/js/telegram-widget.js?22'
|
||||
script.setAttribute('data-telegram-login', botName)
|
||||
script.setAttribute('data-size', buttonSize)
|
||||
|
||||
if (cornerRadius !== undefined) {
|
||||
script.setAttribute('data-radius', cornerRadius.toString())
|
||||
}
|
||||
|
||||
if (requestAccess) {
|
||||
script.setAttribute('data-request-access', 'write')
|
||||
}
|
||||
|
||||
script.setAttribute('data-userpic', usePic.toString())
|
||||
|
||||
if (typeof dataAuthUrl === 'string') {
|
||||
script.setAttribute('data-auth-url', dataAuthUrl)
|
||||
} else {
|
||||
script.setAttribute('data-onauth', 'TelegramLoginWidget.dataOnauth(user)')
|
||||
}
|
||||
|
||||
script.async = true
|
||||
|
||||
ref.current.appendChild(script)
|
||||
}, [
|
||||
botName,
|
||||
buttonSize,
|
||||
cornerRadius,
|
||||
dataOnauth,
|
||||
requestAccess,
|
||||
usePic,
|
||||
ref,
|
||||
dataAuthUrl
|
||||
])
|
||||
|
||||
return <div ref={ref} className={className} {...wrapperProps} />
|
||||
}
|
||||
|
||||
TelegramLoginButton.propTypes = {
|
||||
botName: PropTypes.string.isRequired,
|
||||
usePic: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
cornerRadius: PropTypes.number,
|
||||
requestAccess: PropTypes.bool,
|
||||
wrapperProps: PropTypes.object,
|
||||
dataOnauth: PropTypes.func,
|
||||
dataAuthUrl: PropTypes.string,
|
||||
buttonSize: PropTypes.oneOf(['large', 'medium', 'small'])
|
||||
}
|
||||
|
||||
export default TelegramLoginButton
|
||||
64
src/components/test.tsx
Normal file
64
src/components/test.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import cx from 'clsx';
|
||||
import {Text} from '@mantine/core';
|
||||
import {useListState} from '@mantine/hooks';
|
||||
import {DragDropContext, Droppable, Draggable} from '@hello-pangea/dnd';
|
||||
import classes from './DndList.module.css';
|
||||
|
||||
|
||||
const data = [
|
||||
{position: 6, mass: 12.011, symbol: 'C', name: 'Carbon'},
|
||||
{position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen'},
|
||||
{position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium'},
|
||||
{position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium'},
|
||||
{position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium'},
|
||||
];
|
||||
|
||||
export function DndList() {
|
||||
const [state, handlers] = useListState(data);
|
||||
|
||||
const items = (listIndex: number) => state.map((item, index) => (
|
||||
<Draggable key={item.symbol + `${listIndex}`} index={index} draggableId={item.symbol + `${listIndex}`}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={cx(classes.item, {[classes.itemDragging]: snapshot.isDragging})}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<Text className={classes.symbol}>{item.symbol}</Text>
|
||||
<div>
|
||||
<Text>{item.name}</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
Position: {item.position} • Mass: {item.mass}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
));
|
||||
|
||||
return (
|
||||
<DragDropContext
|
||||
onDragEnd={({destination, source}) =>
|
||||
handlers.reorder({from: source.index, to: destination?.index || 0})
|
||||
}
|
||||
>
|
||||
<Droppable droppableId="dnd-list" direction="vertical">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{items(1)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
<Droppable droppableId="dnd-list-2" direction="vertical">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{items(2)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
}
|
||||
34
src/features/authSlice.ts
Normal file
34
src/features/authSlice.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
|
||||
|
||||
interface AuthState {
|
||||
isAuthorized: boolean;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
const initialState = (): AuthState => {
|
||||
const localStorageState = localStorage.getItem("authState");
|
||||
if (localStorageState !== null) {
|
||||
return JSON.parse(localStorageState);
|
||||
}
|
||||
return {
|
||||
accessToken: "",
|
||||
isAuthorized: false
|
||||
}
|
||||
}
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
login: (state, action: PayloadAction<{ accessToken: string }>) => {
|
||||
state.accessToken = action.payload.accessToken;
|
||||
state.isAuthorized = true;
|
||||
},
|
||||
logout: (state) => {
|
||||
state.isAuthorized = false;
|
||||
state.accessToken = '';
|
||||
}
|
||||
}
|
||||
})
|
||||
export const {login, logout} = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
18
src/main.css
Normal file
18
src/main.css
Normal file
@@ -0,0 +1,18 @@
|
||||
body {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:root {
|
||||
--item-border-size: 0.1rem;
|
||||
--item-border-radius: 0.5rem;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
body {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
31
src/main.tsx
31
src/main.tsx
@@ -9,9 +9,11 @@ import {store} from "./redux/store.ts";
|
||||
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import './main.scss';
|
||||
import './main.css';
|
||||
import {Notifications} from "@mantine/notifications";
|
||||
import {ModalsProvider} from "@mantine/modals";
|
||||
import {OpenAPI} from "./client";
|
||||
import PageWrapper from "./pages/PageWrapper/PageWrapper.tsx";
|
||||
|
||||
// Configuring router
|
||||
const router = createRouter({routeTree})
|
||||
@@ -24,17 +26,22 @@ declare module '@tanstack/react-router' {
|
||||
// Configuring query
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
// Configuring OpenAPI
|
||||
OpenAPI.BASE = import.meta.env.VITE_API_URL
|
||||
OpenAPI.TOKEN = JSON.parse(localStorage.getItem('authState') || "{}")['accessToken'];
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MantineProvider defaultColorScheme={"dark"}>
|
||||
<ModalsProvider>
|
||||
// <React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MantineProvider defaultColorScheme={"dark"}>
|
||||
<ModalsProvider>
|
||||
|
||||
<RouterProvider router={router}/>
|
||||
<Notifications/>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
|
||||
<Notifications/>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
// </React.StrictMode>,
|
||||
)
|
||||
|
||||
1
src/pages/LeadsPage/index.ts
Normal file
1
src/pages/LeadsPage/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {LeadsPage} from './ui/LeadsPage.tsx';
|
||||
35
src/pages/LeadsPage/ui/LeadsPage.module.css
Normal file
35
src/pages/LeadsPage/ui/LeadsPage.module.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.header-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
height: 100%;
|
||||
width: 10%;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
|
||||
}
|
||||
|
||||
.boards {
|
||||
margin-top: 1rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
/*background-color: rebeccapurple;*/
|
||||
padding-right: 5%;
|
||||
padding-left: 5%;
|
||||
}
|
||||
39
src/pages/LeadsPage/ui/LeadsPage.tsx
Normal file
39
src/pages/LeadsPage/ui/LeadsPage.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import {FC} from "react";
|
||||
import styles from './LeadsPage.module.css';
|
||||
import Board from "../../../components/Dnd/Board/Board.tsx";
|
||||
import {Button, TextInput} from "@mantine/core";
|
||||
import {DragDropContext} from "@hello-pangea/dnd";
|
||||
|
||||
export const LeadsPage: FC = () => {
|
||||
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<div className={styles['header']}>
|
||||
<TextInput
|
||||
radius={0}
|
||||
placeholder={"Поиск и фильтры"}
|
||||
size={"xl"}
|
||||
className={styles['header-input']}
|
||||
/>
|
||||
<Button
|
||||
radius={0}
|
||||
color={"gray"}
|
||||
variant={"light"}
|
||||
className={styles['header-button']}
|
||||
>Поиск</Button>
|
||||
</div>
|
||||
<div className={styles['boards']}>
|
||||
<DragDropContext onDragEnd={() => {
|
||||
}}>
|
||||
<Board title={"Ожидает приемки"} withCreateButton droppableId={"AWAITING_ACCEPTANCE"}/>
|
||||
<Board title={"Упаковка"} droppableId={"PACKAGING"}/>
|
||||
<Board title={"Ожидает отгрузки"} droppableId={"AWAITING_SHIPMENT"}/>
|
||||
<Board title={"Ожидает оплаты"} droppableId={"AWAITING_PAYMENT"}/>
|
||||
<Board title={"Завершена"} droppableId={"COMPLETED"}/>
|
||||
|
||||
</DragDropContext>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
6
src/pages/LoginPage/LoginPage.module.scss
Normal file
6
src/pages/LoginPage/LoginPage.module.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.title {
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
font-weight: 900;
|
||||
}
|
||||
60
src/pages/LoginPage/LoginPage.tsx
Normal file
60
src/pages/LoginPage/LoginPage.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {Button, Container, Paper, Title,} from '@mantine/core';
|
||||
import classes from './LoginPage.module.scss';
|
||||
import {RootState, useAppDispatch} from "../../redux/store.ts";
|
||||
import {AuthService} from "../../client";
|
||||
import TelegramLoginButton, {TelegramUser} from "../../components/TelegramAuthButton/TelegramAuthButton.tsx";
|
||||
import {notifications} from "../../shared/lib/notifications.ts";
|
||||
import {login} from "../../features/authSlice.ts";
|
||||
import {useNavigate} from "@tanstack/react-router";
|
||||
import {useSelector} from "react-redux";
|
||||
|
||||
const LoginPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
const navigate = useNavigate();
|
||||
if (authState.isAuthorized) {
|
||||
navigate({to: "/"})
|
||||
return (<></>)
|
||||
}
|
||||
return (
|
||||
|
||||
<Container size={420} my={40}>
|
||||
<Title ta="center" className={classes.title}>
|
||||
Добро пожаловать на DENCO CRM
|
||||
</Title>
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<TelegramLoginButton
|
||||
botName={"DencoFulfillmentTestBot"}
|
||||
dataOnauth={() => {
|
||||
}}
|
||||
wrapperProps={{style: {display: "none"}}}
|
||||
/>
|
||||
<Button fullWidth onClick={() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
window.Telegram.Login.auth(
|
||||
{
|
||||
bot_id: import.meta.env.VITE_BOT_ID,
|
||||
request_access: true
|
||||
},
|
||||
(data: TelegramUser) => {
|
||||
AuthService.loginAuthLoginPost({requestBody: data})
|
||||
.then(({access_token}) => {
|
||||
dispatch(login({accessToken: access_token}));
|
||||
navigate({to: "/"}).then(() => {
|
||||
notifications.success({message: "Вы успешно вошли!"})
|
||||
})
|
||||
}).catch(() => {
|
||||
notifications.error({message: "Неудалось войти!"})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
}}>
|
||||
Войти через Telegram
|
||||
</Button>
|
||||
</Paper>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
export default LoginPage
|
||||
@@ -1,14 +1,11 @@
|
||||
import {FC} from "react";
|
||||
|
||||
import {Flex, Text} from "@mantine/core";
|
||||
|
||||
const MainPage: FC = () => {
|
||||
|
||||
return (
|
||||
<Flex direction={"column"} style={{flex: 1, justifyItems: "center", alignContent: "center"}}>
|
||||
<Text>авфыв</Text>
|
||||
return (<>
|
||||
|
||||
</>
|
||||
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
20
src/pages/PageWrapper/PageWrapper.tsx
Normal file
20
src/pages/PageWrapper/PageWrapper.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import {FC, ReactNode} from "react";
|
||||
import {Flex} from "@mantine/core";
|
||||
import {Navbar} from "../../components/Navbar/Navbar.tsx";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "../../redux/store.ts";
|
||||
|
||||
export type Props = {
|
||||
children: ReactNode;
|
||||
}
|
||||
const PageWrapper: FC<Props> = ({children}) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
return (<Flex style={{flex: 1}}>
|
||||
{authState.isAuthorized &&
|
||||
<Navbar/>
|
||||
}
|
||||
{children}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
export default PageWrapper;
|
||||
35
src/pages/RootPage/RootPage.tsx
Normal file
35
src/pages/RootPage/RootPage.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import {Outlet} from "@tanstack/react-router";
|
||||
import {useEffect} from "react";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "../../redux/store.ts";
|
||||
import {OpenAPI} from "../../client";
|
||||
import PageWrapper from "../PageWrapper/PageWrapper.tsx";
|
||||
|
||||
const RootPage = () => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
const rewriteLocalStorage = () => {
|
||||
const jsonData = JSON.stringify(authState);
|
||||
localStorage.setItem('authState', jsonData);
|
||||
}
|
||||
const setOpenApiToken = () => {
|
||||
OpenAPI.TOKEN = authState.accessToken;
|
||||
}
|
||||
useEffect(() => {
|
||||
rewriteLocalStorage();
|
||||
setOpenApiToken();
|
||||
}, [authState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<PageWrapper>
|
||||
<Outlet/>
|
||||
</PageWrapper>
|
||||
|
||||
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default RootPage;
|
||||
@@ -1,8 +1,11 @@
|
||||
import {configureStore} from "@reduxjs/toolkit";
|
||||
import {useDispatch} from "react-redux";
|
||||
import authReducer from '../features/authSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {}
|
||||
reducer: {
|
||||
auth: authReducer
|
||||
}
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
@@ -17,6 +17,8 @@ import { Route as rootRoute } from './routes/__root'
|
||||
// Create Virtual Routes
|
||||
|
||||
const TestLazyImport = createFileRoute('/test')()
|
||||
const LoginLazyImport = createFileRoute('/login')()
|
||||
const LeadsLazyImport = createFileRoute('/leads')()
|
||||
const IndexLazyImport = createFileRoute('/')()
|
||||
|
||||
// Create/Update Routes
|
||||
@@ -26,6 +28,16 @@ const TestLazyRoute = TestLazyImport.update({
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/test.lazy').then((d) => d.Route))
|
||||
|
||||
const LoginLazyRoute = LoginLazyImport.update({
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/login.lazy').then((d) => d.Route))
|
||||
|
||||
const LeadsLazyRoute = LeadsLazyImport.update({
|
||||
path: '/leads',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/leads.lazy').then((d) => d.Route))
|
||||
|
||||
const IndexLazyRoute = IndexLazyImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -39,6 +51,14 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/leads': {
|
||||
preLoaderRoute: typeof LeadsLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/login': {
|
||||
preLoaderRoute: typeof LoginLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/test': {
|
||||
preLoaderRoute: typeof TestLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
@@ -48,6 +68,11 @@ declare module '@tanstack/react-router' {
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export const routeTree = rootRoute.addChildren([IndexLazyRoute, TestLazyRoute])
|
||||
export const routeTree = rootRoute.addChildren([
|
||||
IndexLazyRoute,
|
||||
LeadsLazyRoute,
|
||||
LoginLazyRoute,
|
||||
TestLazyRoute,
|
||||
])
|
||||
|
||||
/* prettier-ignore-end */
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import {createRootRoute, Link, Outlet} from "@tanstack/react-router";
|
||||
import {TanStackRouterDevtools} from '@tanstack/router-devtools'
|
||||
import {createRootRoute, redirect} from "@tanstack/react-router";
|
||||
import RootPage from "../pages/RootPage/RootPage.tsx";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => (
|
||||
<>
|
||||
<div className="p-2 flex gap-2">
|
||||
<Link to="/" className="[&.active]:font-bold">
|
||||
Home
|
||||
</Link>{' '}
|
||||
<Link to="/test" className="[&.active]:font-bold">
|
||||
Test
|
||||
</Link>{' '}
|
||||
</div>
|
||||
|
||||
<Outlet/>
|
||||
<TanStackRouterDevtools/>
|
||||
</>
|
||||
),
|
||||
component: RootPage,
|
||||
beforeLoad: async ({location}) => {
|
||||
const isAuthorized = JSON.parse(localStorage.getItem('authState') || '{}')['isAuthorized'];
|
||||
if (!isAuthorized && location.pathname !== '/login') {
|
||||
throw redirect({
|
||||
to: '/login',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
6
src/routes/leads.lazy.tsx
Normal file
6
src/routes/leads.lazy.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import {createLazyFileRoute} from "@tanstack/react-router";
|
||||
import {LeadsPage} from "../pages/LeadsPage";
|
||||
|
||||
export const Route = createLazyFileRoute('/leads')({
|
||||
component: LeadsPage
|
||||
})
|
||||
6
src/routes/login.lazy.tsx
Normal file
6
src/routes/login.lazy.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import {createLazyFileRoute} from "@tanstack/react-router";
|
||||
import LoginPage from "../pages/LoginPage/LoginPage.tsx";
|
||||
|
||||
export const Route = createLazyFileRoute('/login')({
|
||||
component: LoginPage
|
||||
})
|
||||
43
src/shared/lib/notifications.ts
Normal file
43
src/shared/lib/notifications.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {notifications} from "@mantine/notifications";
|
||||
|
||||
type CustomNotifications = {
|
||||
notify: (...params: Parameters<typeof notifications.show>) => void,
|
||||
success: (...params: Parameters<typeof notifications.show>) => void,
|
||||
warn: (...params: Parameters<typeof notifications.show>) => void,
|
||||
error: (...params: Parameters<typeof notifications.show>) => void,
|
||||
guess: (ok: boolean, ...params: Parameters<typeof notifications.show>) => void,
|
||||
} & typeof notifications;
|
||||
|
||||
const customNotifications: CustomNotifications = {
|
||||
...notifications,
|
||||
notify: (params) => {
|
||||
return notifications.show({
|
||||
...params,
|
||||
color: 'blue'
|
||||
})
|
||||
},
|
||||
success: (params) => {
|
||||
return notifications.show({
|
||||
...params,
|
||||
color: 'green'
|
||||
})
|
||||
},
|
||||
warn: (params) => {
|
||||
return notifications.show({
|
||||
...params,
|
||||
color: 'yellow'
|
||||
})
|
||||
},
|
||||
error: (params) => {
|
||||
return notifications.show({
|
||||
...params,
|
||||
color: 'red'
|
||||
})
|
||||
},
|
||||
guess: (ok: boolean, params) => {
|
||||
if (ok) return customNotifications.success(params);
|
||||
else return customNotifications.error(params);
|
||||
}
|
||||
}
|
||||
|
||||
export {customNotifications as notifications};
|
||||
@@ -2,10 +2,13 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
@@ -13,13 +16,19 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,4 +8,11 @@ export default defineConfig({
|
||||
react(),
|
||||
TanStackRouterVite()
|
||||
],
|
||||
server: {
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
hmr: {
|
||||
port: 5173,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user