This commit is contained in:
2024-03-03 07:23:41 +03:00
parent d43c0a5839
commit 0db252bb27
57 changed files with 1707 additions and 105 deletions

3
.env Normal file
View File

@@ -0,0 +1,3 @@
VITE_BOT_NAME=DencoFulfillmentTestBot
VITE_BOT_ID=6467915685
VITE_API_URL=http://test.crm.denco.store/api

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
public/icons/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

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

View File

@@ -1,3 +0,0 @@
import '@mantine/core/styles.css';

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

View 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: "Профиль"
}
]
},
];

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

View File

18
src/main.css Normal file
View 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;
}

View File

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

View File

@@ -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>,
)

View File

@@ -0,0 +1 @@
export {LeadsPage} from './ui/LeadsPage.tsx';

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

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

View File

@@ -0,0 +1,6 @@
.title {
font-family:
Greycliff CF,
var(--mantine-font-family);
font-weight: 900;
}

View 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

View File

@@ -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>
)
}

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

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

View File

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

View File

@@ -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 */

View File

@@ -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',
})
}
}
})

View File

@@ -0,0 +1,6 @@
import {createLazyFileRoute} from "@tanstack/react-router";
import {LeadsPage} from "../pages/LeadsPage";
export const Route = createLazyFileRoute('/leads')({
component: LeadsPage
})

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

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

View File

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

View File

@@ -8,4 +8,11 @@ export default defineConfig({
react(),
TanStackRouterVite()
],
server: {
port: 5173,
strictPort: true,
hmr: {
port: 5173,
},
}
})