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, + }, + } })