diff --git a/.env b/.env
new file mode 100644
index 0000000..3f0cc07
--- /dev/null
+++ b/.env
@@ -0,0 +1,3 @@
+VITE_BOT_NAME=DencoFulfillmentTestBot
+VITE_BOT_ID=6467915685
+VITE_API_URL=http://test.crm.denco.store/api
\ No newline at end of file
diff --git a/index.html b/index.html
index e4b78ea..38949e5 100644
--- a/index.html
+++ b/index.html
@@ -2,9 +2,9 @@
-
+
- Vite + React + TS
+ DENCO: Fulfillment
diff --git a/package.json b/package.json
index 4282144..f283e6f 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/public/icons/favicon.png b/public/icons/favicon.png
new file mode 100644
index 0000000..95f2f7f
Binary files /dev/null and b/public/icons/favicon.png differ
diff --git a/public/icons/icon.png b/public/icons/icon.png
new file mode 100644
index 0000000..406d639
Binary files /dev/null and b/public/icons/icon.png differ
diff --git a/public/icons/logo-light.png b/public/icons/logo-light.png
new file mode 100644
index 0000000..f9db7b2
Binary files /dev/null and b/public/icons/logo-light.png differ
diff --git a/public/icons/logo.png b/public/icons/logo.png
new file mode 100644
index 0000000..95c93ed
Binary files /dev/null and b/public/icons/logo.png differ
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index b9d355d..0000000
--- a/src/App.css
+++ /dev/null
@@ -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;
-}
diff --git a/src/App.tsx b/src/App.tsx
deleted file mode 100644
index 7cc8a86..0000000
--- a/src/App.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import '@mantine/core/styles.css';
-
-
diff --git a/src/client/core/ApiError.ts b/src/client/core/ApiError.ts
new file mode 100644
index 0000000..d6b8fcc
--- /dev/null
+++ b/src/client/core/ApiError.ts
@@ -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;
+ }
+}
diff --git a/src/client/core/ApiRequestOptions.ts b/src/client/core/ApiRequestOptions.ts
new file mode 100644
index 0000000..c19adcc
--- /dev/null
+++ b/src/client/core/ApiRequestOptions.ts
@@ -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;
+ readonly cookies?: Record;
+ readonly headers?: Record;
+ readonly query?: Record;
+ readonly formData?: Record;
+ readonly body?: any;
+ readonly mediaType?: string;
+ readonly responseHeader?: string;
+ readonly errors?: Record;
+};
diff --git a/src/client/core/ApiResult.ts b/src/client/core/ApiResult.ts
new file mode 100644
index 0000000..ad8fef2
--- /dev/null
+++ b/src/client/core/ApiResult.ts
@@ -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;
+};
diff --git a/src/client/core/CancelablePromise.ts b/src/client/core/CancelablePromise.ts
new file mode 100644
index 0000000..eb02246
--- /dev/null
+++ b/src/client/core/CancelablePromise.ts
@@ -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 implements Promise {
+ #isResolved: boolean;
+ #isRejected: boolean;
+ #isCancelled: boolean;
+ readonly #cancelHandlers: (() => void)[];
+ readonly #promise: Promise;
+ #resolve?: (value: T | PromiseLike) => void;
+ #reject?: (reason?: any) => void;
+
+ constructor(
+ executor: (
+ resolve: (value: T | PromiseLike) => void,
+ reject: (reason?: any) => void,
+ onCancel: OnCancel
+ ) => void
+ ) {
+ this.#isResolved = false;
+ this.#isRejected = false;
+ this.#isCancelled = false;
+ this.#cancelHandlers = [];
+ this.#promise = new Promise((resolve, reject) => {
+ this.#resolve = resolve;
+ this.#reject = reject;
+
+ const onResolve = (value: T | PromiseLike): 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(
+ onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null,
+ onRejected?: ((reason: any) => TResult2 | PromiseLike) | null
+ ): Promise {
+ return this.#promise.then(onFulfilled, onRejected);
+ }
+
+ public catch(
+ onRejected?: ((reason: any) => TResult | PromiseLike) | null
+ ): Promise {
+ return this.#promise.catch(onRejected);
+ }
+
+ public finally(onFinally?: (() => void) | null): Promise {
+ 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;
+ }
+}
diff --git a/src/client/core/OpenAPI.ts b/src/client/core/OpenAPI.ts
new file mode 100644
index 0000000..e357bb2
--- /dev/null
+++ b/src/client/core/OpenAPI.ts
@@ -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 = (options: ApiRequestOptions) => Promise;
+type Headers = Record;
+
+export type OpenAPIConfig = {
+ BASE: string;
+ VERSION: string;
+ WITH_CREDENTIALS: boolean;
+ CREDENTIALS: 'include' | 'omit' | 'same-origin';
+ TOKEN?: string | Resolver | undefined;
+ USERNAME?: string | Resolver | undefined;
+ PASSWORD?: string | Resolver | undefined;
+ HEADERS?: Headers | Resolver | 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,
+};
diff --git a/src/client/core/request.ts b/src/client/core/request.ts
new file mode 100644
index 0000000..c6a0602
--- /dev/null
+++ b/src/client/core/request.ts
@@ -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 = (value: T | null | undefined): value is Exclude => {
+ 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 => {
+ 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 = (options: ApiRequestOptions) => Promise;
+
+export const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => {
+ if (typeof resolver === 'function') {
+ return (resolver as Resolver)(options);
+ }
+ return resolver;
+};
+
+export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise> => {
+ 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);
+
+ 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 (
+ config: OpenAPIConfig,
+ options: ApiRequestOptions,
+ url: string,
+ body: any,
+ formData: FormData | undefined,
+ headers: Record,
+ onCancel: OnCancel,
+ axiosClient: AxiosInstance
+): Promise> => {
+ 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;
+ if (axiosError.response) {
+ return axiosError.response;
+ }
+ throw error;
+ }
+};
+
+export const getResponseHeader = (response: AxiosResponse, responseHeader?: string): string | undefined => {
+ if (responseHeader) {
+ const content = response.headers[responseHeader];
+ if (isString(content)) {
+ return content;
+ }
+ }
+ return undefined;
+};
+
+export const getResponseBody = (response: AxiosResponse): any => {
+ if (response.status !== 204) {
+ return response.data;
+ }
+ return undefined;
+};
+
+export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
+ const errors: Record = {
+ 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
+ * @throws ApiError
+ */
+export const request = (config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise => {
+ 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(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);
+ }
+ });
+};
diff --git a/src/client/index.ts b/src/client/index.ts
new file mode 100644
index 0000000..c1809eb
--- /dev/null
+++ b/src/client/index.ts
@@ -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';
diff --git a/src/client/models/AuthLoginRequest.ts b/src/client/models/AuthLoginRequest.ts
new file mode 100644
index 0000000..282e27f
--- /dev/null
+++ b/src/client/models/AuthLoginRequest.ts
@@ -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;
+};
+
diff --git a/src/client/models/AuthLoginResponse.ts b/src/client/models/AuthLoginResponse.ts
new file mode 100644
index 0000000..20f77de
--- /dev/null
+++ b/src/client/models/AuthLoginResponse.ts
@@ -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;
+};
+
diff --git a/src/client/models/HTTPValidationError.ts b/src/client/models/HTTPValidationError.ts
new file mode 100644
index 0000000..892e425
--- /dev/null
+++ b/src/client/models/HTTPValidationError.ts
@@ -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;
+};
+
diff --git a/src/client/models/ValidationError.ts b/src/client/models/ValidationError.ts
new file mode 100644
index 0000000..f2ff49a
--- /dev/null
+++ b/src/client/models/ValidationError.ts
@@ -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;
+};
+
diff --git a/src/client/services/AuthService.ts b/src/client/services/AuthService.ts
new file mode 100644
index 0000000..f99caba
--- /dev/null
+++ b/src/client/services/AuthService.ts
@@ -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 {
+ return __request(OpenAPI, {
+ method: 'POST',
+ url: '/auth/login',
+ body: requestBody,
+ mediaType: 'application/json',
+ errors: {
+ 422: `Validation Error`,
+ },
+ });
+ }
+}
diff --git a/src/components/Dnd/Board/Board.module.css b/src/components/Dnd/Board/Board.module.css
new file mode 100644
index 0000000..7b72253
--- /dev/null
+++ b/src/components/Dnd/Board/Board.module.css
@@ -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%;
+}
\ No newline at end of file
diff --git a/src/components/Dnd/Board/Board.tsx b/src/components/Dnd/Board/Board.tsx
new file mode 100644
index 0000000..d4927e5
--- /dev/null
+++ b/src/components/Dnd/Board/Board.tsx
@@ -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 = ({droppableId, title, withCreateButton = false}) => {
+
+
+ return (
+
+
+
{title}
+
12 сделок: 500р
+
+
+
+ {(provided, snapshot) => (
+
+ {withCreateButton &&
+
{
+ }}
+ />}
+
+ {(provided, snapshot) => (
+
+
+
+ )}
+
+
+
+ {provided.placeholder}
+
+ )}
+
+
+
+ )
+}
+
+export default Board;
\ No newline at end of file
diff --git a/src/components/Dnd/CreateLeadButton/CreateLeadButton.module.css b/src/components/Dnd/CreateLeadButton/CreateLeadButton.module.css
new file mode 100644
index 0000000..ed1b85b
--- /dev/null
+++ b/src/components/Dnd/CreateLeadButton/CreateLeadButton.module.css
@@ -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));
+
+}
\ No newline at end of file
diff --git a/src/components/Dnd/CreateLeadButton/CreateLeadButton.tsx b/src/components/Dnd/CreateLeadButton/CreateLeadButton.tsx
new file mode 100644
index 0000000..caa8465
--- /dev/null
+++ b/src/components/Dnd/CreateLeadButton/CreateLeadButton.tsx
@@ -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 = ({onClick}) => {
+ const [isCreating, setIsCreating] = useState(false);
+ const [showButton, setShowButton] = useState(true);
+ console.log(`isCreating: ${isCreating}`)
+ console.log(`showButton: ${showButton}`)
+ return (
+ {
+ if (isCreating) return;
+ setIsCreating(prevState => !prevState)
+ setShowButton(false);
+ }}
+ >
+
+ {(!isCreating && showButton) &&
+
Быстрое добавление
+ }
+
setShowButton(true)}
+ keepMounted
+ >
+ {(styles) =>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
}
+
+
+ )
+}
+export default CreateLeadButton;
\ No newline at end of file
diff --git a/src/components/DndList.module.css b/src/components/DndList.module.css
new file mode 100644
index 0000000..262ab12
--- /dev/null
+++ b/src/components/DndList.module.css
@@ -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);
+}
\ No newline at end of file
diff --git a/src/components/Navbar/LinksGroup/LinksGroup.module.css b/src/components/Navbar/LinksGroup/LinksGroup.module.css
new file mode 100644
index 0000000..8241ebc
--- /dev/null
+++ b/src/components/Navbar/LinksGroup/LinksGroup.module.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/components/Navbar/LinksGroup/LinksGroup.tsx b/src/components/Navbar/LinksGroup/LinksGroup.tsx
new file mode 100644
index 0000000..c74fdc3
--- /dev/null
+++ b/src/components/Navbar/LinksGroup/LinksGroup.tsx
@@ -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;
+ 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.label}
+
+
+ ));
+
+ return (
+ <>
+ setOpened((o) => !o)} className={classes.control}>
+
+
+
+
+
+ {label}
+
+ {hasLinks && (
+
+ )}
+
+
+ {hasLinks ? {items} : null}
+ >
+ );
+}
+
+const mockdata = {
+ label: 'Releases',
+ icon: IconCalendarStats,
+ links: [
+ {label: 'Upcoming releases', link: '/'},
+ {label: 'Previous releases', link: '/'},
+ {label: 'Releases schedule', link: '/'},
+ ],
+};
+
+export function NavbarLinksGroup() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/Navbar/Logo.tsx b/src/components/Navbar/Logo.tsx
new file mode 100644
index 0000000..e2c65bd
--- /dev/null
+++ b/src/components/Navbar/Logo.tsx
@@ -0,0 +1,37 @@
+export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
+ return (
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/Navbar/Navbar.module.css b/src/components/Navbar/Navbar.module.css
new file mode 100644
index 0000000..3e8f9c8
--- /dev/null
+++ b/src/components/Navbar/Navbar.module.css
@@ -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);
+ }
+ }
+}
+
diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx
new file mode 100644
index 0000000..992333c
--- /dev/null
+++ b/src/components/Navbar/Navbar.tsx
@@ -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 (
+
+ onClick && onClick(props)}
+ className={classes.link}
+ data-active={active || undefined}>
+
+
+
+ );
+}
+
+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) => (
+
+ ));
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/Navbar/UserButton/UserButton.module.css b/src/components/Navbar/UserButton/UserButton.module.css
new file mode 100644
index 0000000..4d3283f
--- /dev/null
+++ b/src/components/Navbar/UserButton/UserButton.module.css
@@ -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));
+ }
+}
\ No newline at end of file
diff --git a/src/components/Navbar/UserButton/UserButton.tsx b/src/components/Navbar/UserButton/UserButton.tsx
new file mode 100644
index 0000000..0970349
--- /dev/null
+++ b/src/components/Navbar/UserButton/UserButton.tsx
@@ -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 (
+
+
+
+
+
+
+ Harriette Spoonlicker
+
+
+
+ hspoonlicker@outlook.com
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/Navbar/data.ts b/src/components/Navbar/data.ts
new file mode 100644
index 0000000..57dddef
--- /dev/null
+++ b/src/components/Navbar/data.ts
@@ -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: "Профиль"
+ }
+ ]
+ },
+
+];
diff --git a/src/components/TelegramAuthButton/TelegramAuthButton.tsx b/src/components/TelegramAuthButton/TelegramAuthButton.tsx
new file mode 100644
index 0000000..3278061
--- /dev/null
+++ b/src/components/TelegramAuthButton/TelegramAuthButton.tsx
@@ -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
+}
+
+declare global {
+ interface Window {
+ TelegramLoginWidget: {
+ dataOnauth: (user: TelegramUser) => void
+ }
+ }
+}
+
+const TelegramLoginButton: React.FC = ({
+ wrapperProps,
+ dataAuthUrl,
+ usePic = false,
+ botName,
+ className,
+ buttonSize = 'large',
+ dataOnauth,
+ cornerRadius,
+ requestAccess = true
+ }) => {
+ const ref = useRef(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
+}
+
+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
\ No newline at end of file
diff --git a/src/components/test.tsx b/src/components/test.tsx
new file mode 100644
index 0000000..963ca22
--- /dev/null
+++ b/src/components/test.tsx
@@ -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) => (
+
+ {(provided, snapshot) => (
+
+
{item.symbol}
+
+ {item.name}
+
+ Position: {item.position} • Mass: {item.mass}
+
+
+
+ )}
+
+ ));
+
+ return (
+
+ handlers.reorder({from: source.index, to: destination?.index || 0})
+ }
+ >
+
+ {(provided) => (
+
+ {items(1)}
+ {provided.placeholder}
+
+ )}
+
+
+ {(provided) => (
+
+ {items(2)}
+ {provided.placeholder}
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/features/authSlice.ts b/src/features/authSlice.ts
new file mode 100644
index 0000000..87130da
--- /dev/null
+++ b/src/features/authSlice.ts
@@ -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;
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index e69de29..0000000
diff --git a/src/main.css b/src/main.css
new file mode 100644
index 0000000..2b3ad03
--- /dev/null
+++ b/src/main.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/main.scss b/src/main.scss
deleted file mode 100644
index c532a98..0000000
--- a/src/main.scss
+++ /dev/null
@@ -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;
-}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
index f761169..b4e741c 100644
--- a/src/main.tsx
+++ b/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(
-
-
-
-
-
+ //
+
+
+
+
+
-
-
-
-
-
- ,
+
+
+
+
+
+
+ // ,
)
diff --git a/src/pages/LeadsPage/index.ts b/src/pages/LeadsPage/index.ts
new file mode 100644
index 0000000..b969d79
--- /dev/null
+++ b/src/pages/LeadsPage/index.ts
@@ -0,0 +1 @@
+export {LeadsPage} from './ui/LeadsPage.tsx';
\ No newline at end of file
diff --git a/src/pages/LeadsPage/ui/LeadsPage.module.css b/src/pages/LeadsPage/ui/LeadsPage.module.css
new file mode 100644
index 0000000..f0e1deb
--- /dev/null
+++ b/src/pages/LeadsPage/ui/LeadsPage.module.css
@@ -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%;
+}
diff --git a/src/pages/LeadsPage/ui/LeadsPage.tsx b/src/pages/LeadsPage/ui/LeadsPage.tsx
new file mode 100644
index 0000000..e9895ed
--- /dev/null
+++ b/src/pages/LeadsPage/ui/LeadsPage.tsx
@@ -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 (
+
+
+
+
+
+
+ {
+ }}>
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/pages/LoginPage/LoginPage.module.scss b/src/pages/LoginPage/LoginPage.module.scss
new file mode 100644
index 0000000..7f78364
--- /dev/null
+++ b/src/pages/LoginPage/LoginPage.module.scss
@@ -0,0 +1,6 @@
+.title {
+ font-family:
+ Greycliff CF,
+ var(--mantine-font-family);
+ font-weight: 900;
+}
\ No newline at end of file
diff --git a/src/pages/LoginPage/LoginPage.tsx b/src/pages/LoginPage/LoginPage.tsx
new file mode 100644
index 0000000..69706a1
--- /dev/null
+++ b/src/pages/LoginPage/LoginPage.tsx
@@ -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 (
+
+
+
+ Добро пожаловать на DENCO CRM
+
+
+ {
+ }}
+ wrapperProps={{style: {display: "none"}}}
+ />
+
+
+
+ )
+}
+export default LoginPage
\ No newline at end of file
diff --git a/src/pages/MainPage/MainPage.tsx b/src/pages/MainPage/MainPage.tsx
index e32a8e5..30220c4 100644
--- a/src/pages/MainPage/MainPage.tsx
+++ b/src/pages/MainPage/MainPage.tsx
@@ -1,14 +1,11 @@
import {FC} from "react";
-import {Flex, Text} from "@mantine/core";
-
const MainPage: FC = () => {
- return (
-
- авфыв
+ return (<>
+
+ >
-
)
}
diff --git a/src/pages/PageWrapper/PageWrapper.tsx b/src/pages/PageWrapper/PageWrapper.tsx
new file mode 100644
index 0000000..295bec5
--- /dev/null
+++ b/src/pages/PageWrapper/PageWrapper.tsx
@@ -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 = ({children}) => {
+ const authState = useSelector((state: RootState) => state.auth);
+ return (
+ {authState.isAuthorized &&
+
+ }
+ {children}
+
+ )
+}
+export default PageWrapper;
\ No newline at end of file
diff --git a/src/pages/RootPage/RootPage.tsx b/src/pages/RootPage/RootPage.tsx
new file mode 100644
index 0000000..ca33fa3
--- /dev/null
+++ b/src/pages/RootPage/RootPage.tsx
@@ -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 (
+ <>
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export default RootPage;
\ No newline at end of file
diff --git a/src/redux/store.ts b/src/redux/store.ts
index d3f25fc..8ffe6ef 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -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;
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 40c5e4b..c2a6c12 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -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 */
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index cd05cfc..fd44bf8 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -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: () => (
- <>
-
-
- Home
- {' '}
-
- Test
- {' '}
-
-
-
-
- >
- ),
+ component: RootPage,
+ beforeLoad: async ({location}) => {
+ const isAuthorized = JSON.parse(localStorage.getItem('authState') || '{}')['isAuthorized'];
+ if (!isAuthorized && location.pathname !== '/login') {
+ throw redirect({
+ to: '/login',
+ })
+ }
+ }
})
\ No newline at end of file
diff --git a/src/routes/leads.lazy.tsx b/src/routes/leads.lazy.tsx
new file mode 100644
index 0000000..97df6fe
--- /dev/null
+++ b/src/routes/leads.lazy.tsx
@@ -0,0 +1,6 @@
+import {createLazyFileRoute} from "@tanstack/react-router";
+import {LeadsPage} from "../pages/LeadsPage";
+
+export const Route = createLazyFileRoute('/leads')({
+ component: LeadsPage
+})
\ No newline at end of file
diff --git a/src/routes/login.lazy.tsx b/src/routes/login.lazy.tsx
new file mode 100644
index 0000000..3f1cc0a
--- /dev/null
+++ b/src/routes/login.lazy.tsx
@@ -0,0 +1,6 @@
+import {createLazyFileRoute} from "@tanstack/react-router";
+import LoginPage from "../pages/LoginPage/LoginPage.tsx";
+
+export const Route = createLazyFileRoute('/login')({
+ component: LoginPage
+})
\ No newline at end of file
diff --git a/src/shared/lib/notifications.ts b/src/shared/lib/notifications.ts
new file mode 100644
index 0000000..8952991
--- /dev/null
+++ b/src/shared/lib/notifications.ts
@@ -0,0 +1,43 @@
+import {notifications} from "@mantine/notifications";
+
+type CustomNotifications = {
+ notify: (...params: Parameters) => void,
+ success: (...params: Parameters) => void,
+ warn: (...params: Parameters) => void,
+ error: (...params: Parameters) => void,
+ guess: (ok: boolean, ...params: Parameters) => 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};
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index a7fc6fb..abf8777 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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"
+ }
+ ]
}
diff --git a/vite.config.ts b/vite.config.ts
index fb14228..fad7ef1 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -8,4 +8,11 @@ export default defineConfig({
react(),
TanStackRouterVite()
],
+ server: {
+ port: 5173,
+ strictPort: true,
+ hmr: {
+ port: 5173,
+ },
+ }
})