base structure
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,69 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "dmenu-admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.7",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"axios": "^1.11.0",
|
||||
"formik": "^2.4.6",
|
||||
"i18next": "^25.4.2",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"rc-rate": "^2.13.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-i18next": "^15.7.2",
|
||||
"react-multi-date-picker": "^4.5.2",
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-router-dom": "^7.8.2",
|
||||
"react-spinners": "^0.17.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"swiper": "^11.2.10",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"yup": "^1.7.0",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@types/react": "^19.1.10",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.39.1",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,137 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
// import 'swiper/swiper-bundle.css';
|
||||
// import 'rc-rate/assets/index.css';
|
||||
// import "react-multi-date-picker/styles/layouts/mobile.css"
|
||||
import './assets/fonts/irancell/style.css'
|
||||
import i18next from 'i18next'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ToastContainer } from 'react-toastify'
|
||||
import FaJson from './langs/fa.json'
|
||||
import { type IApiErrorRepsonse } from './types/error.types'
|
||||
import MainRouter from './router/Main'
|
||||
import AuthRouter from './router/Auth'
|
||||
import { Pages } from './config/Pages'
|
||||
import { getRefreshToken, setToken, setRefreshToken, removeToken, removeRefreshToken, getToken } from './config/func';
|
||||
import { refreshToken } from './pages/auth/service/AuthService';
|
||||
|
||||
i18next.init({
|
||||
interpolation: { escapeValue: false },
|
||||
lng: 'fa',
|
||||
resources: {
|
||||
fa: {
|
||||
global: FaJson
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
isRefreshingToken?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: async (error: IApiErrorRepsonse) => {
|
||||
if (error?.response?.status === 401) {
|
||||
try {
|
||||
// Use a flag to track if refresh is in progress
|
||||
if (window.isRefreshingToken) {
|
||||
// Wait for the refresh to complete
|
||||
await new Promise(resolve => {
|
||||
const checkComplete = setInterval(() => {
|
||||
if (!window.isRefreshingToken) {
|
||||
clearInterval(checkComplete);
|
||||
resolve(true);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the flag to indicate refresh is in progress
|
||||
window.isRefreshingToken = true;
|
||||
|
||||
try {
|
||||
const refreshTokenValue = await getRefreshToken();
|
||||
const { data } = await refreshToken({ refreshToken: refreshTokenValue || '' });
|
||||
|
||||
if (data?.accessToken?.token) {
|
||||
// Save the new token
|
||||
await setToken(data?.accessToken?.token);
|
||||
|
||||
// If refresh token also returned, update it
|
||||
if (data.refreshToken?.token) {
|
||||
await setRefreshToken(data.refreshToken?.token);
|
||||
}
|
||||
|
||||
// Retry the failed request
|
||||
queryClient.invalidateQueries();
|
||||
return;
|
||||
} else {
|
||||
throw new Error('Invalid token response');
|
||||
}
|
||||
} finally {
|
||||
window.isRefreshingToken = false;
|
||||
}
|
||||
} catch (refreshError: unknown) {
|
||||
console.error('Token refresh failed:', refreshError);
|
||||
// Clear tokens and redirect to login
|
||||
await removeToken();
|
||||
await removeRefreshToken();
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false
|
||||
// staleTime: 86400000 // 1 day in milliseconds
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const App: FC = () => {
|
||||
|
||||
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
|
||||
useEffect(() => {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
setIsLogin('isLogin')
|
||||
} else {
|
||||
setIsLogin('isNotLogin')
|
||||
if (window.location.href.split('auth').length === 1) {
|
||||
window.location.href = Pages.auth.login
|
||||
}
|
||||
}
|
||||
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nextProvider i18n={i18next}>
|
||||
{
|
||||
isLogin === 'checking' ?
|
||||
<div>loading</div>
|
||||
:
|
||||
isLogin === 'isLogin' ?
|
||||
<MainRouter />
|
||||
:
|
||||
<AuthRouter />
|
||||
}
|
||||
<ToastContainer />
|
||||
</I18nextProvider>
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,18 @@
|
||||
@font-face {
|
||||
font-family: "irancell";
|
||||
font-style: thin;
|
||||
font-weight: 200;
|
||||
src: url("./irancell-extralight.ttf");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "irancell";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url("./irancell-light.ttf");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "irancell";
|
||||
font-style: thin;
|
||||
font-weight: 600;
|
||||
src: url("./irancell-bold.ttf");
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 395 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.97508 5.44165L2.91675 10.5L7.97508 15.5583" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M17.0834 10.5H3.05835" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 868 KiB |
|
After Width: | Height: | Size: 318 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.16675 1.66675V4.16675" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.8333 1.66675V4.16675" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.41675 7.57495H17.5834" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18 7.08341V14.1667C18 16.6667 16.75 18.3334 13.8333 18.3334H7.16667C4.25 18.3334 3 16.6667 3 14.1667V7.08341C3 4.58341 4.25 2.91675 7.16667 2.91675H13.8333C16.75 2.91675 18 4.58341 18 7.08341Z" stroke="#292D32" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5788 11.4167H13.5863" stroke="#292D32" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.5788 13.9167H13.5863" stroke="#292D32" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.4963 11.4167H10.5038" stroke="#292D32" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.4963 13.9167H10.5038" stroke="#292D32" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.41185 11.4167H7.41933" stroke="#292D32" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.41185 13.9167H7.41933" stroke="#292D32" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 15L15 5" stroke="#8C90A3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15 15L5 5" stroke="#8C90A3" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.9833 9.99993C12.9833 11.6499 11.6499 12.9833 9.99993 12.9833C8.34993 12.9833 7.0166 11.6499 7.0166 9.99993C7.0166 8.34993 8.34993 7.0166 9.99993 7.0166C11.6499 7.0166 12.9833 8.34993 12.9833 9.99993Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.99987 16.8918C12.9415 16.8918 15.6832 15.1584 17.5915 12.1584C18.3415 10.9834 18.3415 9.00843 17.5915 7.83343C15.6832 4.83343 12.9415 3.1001 9.99987 3.1001C7.0582 3.1001 4.31654 4.83343 2.4082 7.83343C1.6582 9.00843 1.6582 10.9834 2.4082 12.1584C4.31654 15.1584 7.0582 16.8918 9.99987 16.8918Z" stroke="#292D32" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 794 B |
|
After Width: | Height: | Size: 209 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg width="39" height="54" viewBox="0 0 39 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.1559 34.8412C31.1559 34.8412 24.6651 41.094 19.2416 46.3804V53.5924C18.6935 54.0179 38.4905 34.8845 38.4905 34.8845L19.2416 14.6909V22.3573L31.1559 34.8412Z" fill="#1D1D1B"/>
|
||||
<path d="M19.1479 39.0818L0 18.643L0.237992 18.405L19.3931 38.8583L19.1479 39.0818Z" fill="#1D1D1B"/>
|
||||
<path d="M19.1407 37.1635L1.75977 18.6719L1.99776 18.4412L19.3787 36.9399L19.1407 37.1635Z" fill="#1D1D1B"/>
|
||||
<path d="M19.1334 35.2523L3.52661 18.708L3.7574 18.47L19.3786 35.0215L19.1334 35.2523Z" fill="#1D1D1B"/>
|
||||
<path d="M19.1262 33.3337L5.28638 18.7366L5.50995 18.5059L19.3714 33.1029L19.1262 33.3337Z" fill="#1D1D1B"/>
|
||||
<path d="M19.1191 31.4155L7.05347 18.7729L7.26982 18.5349L19.3571 31.1848L19.1191 31.4155Z" fill="#1D1D1B"/>
|
||||
<path d="M0.375071 18.7368L0.144287 18.4988L19.1407 0L19.3715 0.237996L0.375071 18.7368Z" fill="#1D1D1B"/>
|
||||
<path d="M2.15645 18.7442L1.91846 18.5134L19.1263 1.73828L19.3571 1.97628L2.15645 18.7442Z" fill="#1D1D1B"/>
|
||||
<path d="M3.93049 18.7587L3.69971 18.5279L19.1117 3.48364L19.3425 3.71443L3.93049 18.7587Z" fill="#1D1D1B"/>
|
||||
<path d="M5.71187 18.7658L5.47388 18.5422L19.0973 5.22168L19.3281 5.45968L5.71187 18.7658Z" fill="#1D1D1B"/>
|
||||
<path d="M7.49326 18.7799L7.24805 18.5564L19.0901 6.95947L19.3137 7.19747L7.49326 18.7799Z" fill="#1D1D1B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 961.6 222.5" style="enable-background:new 0 0 961.6 222.5;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#1D1D1B;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M246,180.4h-8.7v24.3h-3.6v-24.3H225v-2.9h21V180.4z"/>
|
||||
<path class="st0" d="M279.1,205c-2.7,0-5-0.9-6.7-2.7s-2.6-4.2-2.6-7.2v-0.6c0-2,0.4-3.8,1.1-5.4s1.8-2.8,3.2-3.7
|
||||
s2.9-1.3,4.5-1.3c2.6,0,4.7,0.9,6.1,2.6c1.5,1.7,2.2,4.2,2.2,7.4v1.4h-13.7c0,2,0.6,3.6,1.7,4.8s2.5,1.8,4.2,1.8
|
||||
c1.2,0,2.3-0.2,3.1-0.7s1.6-1.2,2.2-2l2.1,1.6C285,203.7,282.5,205,279.1,205z M278.7,186.9c-1.4,0-2.6,0.5-3.5,1.5
|
||||
s-1.5,2.4-1.8,4.3h10.1v-0.3c-0.1-1.8-0.6-3.1-1.4-4.1C281.3,187.4,280.1,186.9,278.7,186.9z"/>
|
||||
<path class="st0" d="M320.4,202.2c1.2,0,2.3-0.4,3.2-1.1c0.9-0.7,1.4-1.7,1.5-2.8h3.3c-0.1,1.2-0.5,2.3-1.2,3.3
|
||||
s-1.7,1.9-2.9,2.5c-1.2,0.6-2.5,0.9-3.9,0.9c-2.8,0-4.9-0.9-6.6-2.8c-1.6-1.8-2.4-4.3-2.4-7.5v-0.6c0-2,0.4-3.7,1.1-5.2
|
||||
s1.8-2.7,3.1-3.6c1.4-0.8,2.9-1.3,4.8-1.3c2.3,0,4.1,0.7,5.6,2c1.5,1.4,2.3,3.1,2.4,5.3h-3.3c-0.1-1.3-0.6-2.4-1.5-3.2
|
||||
s-2-1.3-3.3-1.3c-1.8,0-3.1,0.6-4.1,1.9s-1.4,3.1-1.4,5.5v0.7c0,2.3,0.5,4.1,1.4,5.4C317.2,201.6,318.6,202.2,320.4,202.2z"/>
|
||||
<path class="st0" d="M357,186.9c1.5-1.9,3.5-2.8,6-2.8c4.3,0,6.4,2.4,6.5,7.2v13.4H366v-13.4c0-1.5-0.3-2.5-1-3.2s-1.7-1-3.1-1
|
||||
c-1.1,0-2.1,0.3-3,0.9s-1.5,1.4-2,2.4v14.4h-3.4v-28.7h3.5V186.9z"/>
|
||||
<path class="st0" d="M399.1,184.5l0.1,2.5c1.5-1.9,3.6-2.9,6.1-2.9c4.3,0,6.4,2.4,6.5,7.2v13.4h-3.5v-13.4c0-1.5-0.3-2.5-1-3.2
|
||||
s-1.7-1-3.1-1c-1.1,0-2.1,0.3-3,0.9s-1.5,1.4-2,2.4v14.4h-3.5v-20.2h3.4V184.5z"/>
|
||||
<path class="st0" d="M437.3,194.4c0-2,0.4-3.8,1.2-5.3c0.8-1.6,1.9-2.8,3.2-3.7s3-1.3,4.8-1.3c2.8,0,5,1,6.7,2.9
|
||||
s2.5,4.4,2.5,7.6v0.2c0,2-0.4,3.7-1.1,5.3s-1.8,2.8-3.2,3.7c-1.4,0.9-3,1.3-4.8,1.3c-2.7,0-5-1-6.7-2.9s-2.5-4.4-2.5-7.6v-0.2
|
||||
H437.3z M440.8,194.8c0,2.2,0.5,4,1.6,5.4c1,1.4,2.4,2,4.2,2c1.8,0,3.1-0.7,4.2-2.1s1.6-3.3,1.6-5.8c0-2.2-0.5-4-1.6-5.4
|
||||
s-2.5-2.1-4.2-2.1s-3.1,0.7-4.1,2C441.3,190.3,440.8,192.3,440.8,194.8z"/>
|
||||
<path class="st0" d="M485.1,204.7h-3.5V176h3.5V204.7z"/>
|
||||
<path class="st0" d="M511,194.4c0-2,0.4-3.8,1.2-5.3c0.8-1.5,1.9-2.8,3.2-3.7c1.4-0.9,3-1.3,4.8-1.3c2.8,0,5,1,6.7,2.9
|
||||
s2.5,4.4,2.5,7.6v0.2c0,2-0.4,3.7-1.1,5.3s-1.8,2.8-3.2,3.7s-3,1.3-4.8,1.3c-2.7,0-5-1-6.7-2.9c-1.7-1.9-2.5-4.4-2.5-7.6v-0.2
|
||||
H511z M514.4,194.8c0,2.2,0.5,4,1.6,5.4c1,1.4,2.4,2,4.2,2s3.2-0.7,4.2-2.1c1-1.4,1.5-3.3,1.5-5.8c0-2.2-0.5-4-1.6-5.4
|
||||
s-2.5-2.1-4.2-2.1s-3.1,0.7-4.1,2C515,190.3,514.4,192.3,514.4,194.8z"/>
|
||||
<path class="st0" d="M554.1,194.4c0-3.2,0.7-5.7,2.2-7.5s3.4-2.8,5.8-2.8c2.5,0,4.4,0.9,5.8,2.6l0.2-2.2h3.2v19.7
|
||||
c0,2.6-0.8,4.7-2.3,6.2c-1.6,1.5-3.6,2.3-6.2,2.3c-1.5,0-2.9-0.3-4.3-0.9c-1.4-0.6-2.5-1.5-3.2-2.6l1.8-2.1
|
||||
c1.5,1.8,3.3,2.7,5.4,2.7c1.7,0,3-0.5,3.9-1.4s1.4-2.3,1.4-4v-1.7c-1.4,1.6-3.3,2.4-5.7,2.4s-4.3-1-5.7-2.9
|
||||
S554.1,197.7,554.1,194.4z M557.6,194.8c0,2.3,0.5,4.1,1.4,5.4c0.9,1.3,2.2,2,3.9,2c2.2,0,3.8-1,4.8-3V190
|
||||
c-1.1-1.9-2.6-2.9-4.8-2.9c-1.7,0-3,0.7-3.9,2C558.1,190.3,557.6,192.2,557.6,194.8z"/>
|
||||
<path class="st0" d="M604.3,199.6l4.7-15.1h3.7l-8.1,23.3c-1.3,3.4-3.3,5-6,5l-0.7-0.1l-1.3-0.2v-2.8l0.9,0.1
|
||||
c1.2,0,2.1-0.2,2.7-0.7c0.7-0.5,1.2-1.3,1.6-2.6l0.8-2.1l-7.2-20h3.8L604.3,199.6z"/>
|
||||
<path class="st0" d="M688.3,196c-0.3,2.9-1.4,5.1-3.2,6.7c-1.8,1.6-4.2,2.3-7.1,2.3c-3.2,0-5.8-1.2-7.7-3.5s-2.9-5.4-2.9-9.2
|
||||
v-2.6c0-2.5,0.5-4.8,1.4-6.7s2.2-3.4,3.8-4.4s3.6-1.5,5.8-1.5c2.9,0,5.2,0.8,6.9,2.4c1.7,1.6,2.8,3.8,3,6.7h-3.6
|
||||
c-0.3-2.2-1-3.7-2-4.7s-2.5-1.5-4.4-1.5c-2.3,0-4.1,0.8-5.4,2.5c-1.3,1.7-2,4.1-2,7.2v2.6c0,3,0.6,5.3,1.8,7s3,2.6,5.2,2.6
|
||||
c2,0,3.5-0.5,4.6-1.4s1.8-2.5,2.1-4.7h3.7V196z"/>
|
||||
<path class="st0" d="M713,194.4c0-2,0.4-3.8,1.2-5.3s1.9-2.8,3.2-3.7c1.4-0.9,3-1.3,4.8-1.3c2.8,0,5,1,6.7,2.9s2.5,4.4,2.5,7.6
|
||||
v0.2c0,2-0.4,3.7-1.1,5.3s-1.8,2.8-3.2,3.7s-3,1.3-4.8,1.3c-2.7,0-5-1-6.7-2.9s-2.5-4.4-2.5-7.6v-0.2H713z M716.5,194.8
|
||||
c0,2.2,0.5,4,1.6,5.4c1,1.4,2.4,2,4.2,2s3.2-0.7,4.2-2.1c1-1.4,1.5-3.3,1.5-5.8c0-2.2-0.5-4-1.6-5.4s-2.5-2.1-4.2-2.1
|
||||
s-3.1,0.7-4.1,2C717,190.3,716.5,192.3,716.5,194.8z"/>
|
||||
<path class="st0" d="M760.3,184.5l0.1,2.2c1.5-1.7,3.5-2.6,6-2.6c2.8,0,4.8,1.1,5.8,3.2c0.7-1,1.5-1.8,2.6-2.4
|
||||
c1.1-0.6,2.4-0.9,3.8-0.9c4.4,0,6.7,2.3,6.8,7v13.5h-3.5v-13.3c0-1.4-0.3-2.5-1-3.2s-1.8-1.1-3.3-1.1c-1.3,0-2.3,0.4-3.2,1.1
|
||||
c-0.8,0.8-1.3,1.8-1.5,3.1v13.4h-3.5v-13.2c0-2.9-1.4-4.4-4.3-4.4c-2.3,0-3.8,1-4.7,2.9v14.8h-3.5v-20.2h3.4V184.5z"/>
|
||||
<path class="st0" d="M828.9,194.8c0,3.1-0.7,5.6-2.1,7.4c-1.4,1.9-3.3,2.8-5.7,2.8c-2.5,0-4.4-0.8-5.8-2.3v9.7h-3.5v-28h3.2
|
||||
l0.2,2.2c1.4-1.7,3.4-2.6,5.9-2.6c2.4,0,4.3,0.9,5.8,2.7c1.4,1.8,2.1,4.4,2.1,7.6v0.5H828.9z M825.5,194.4
|
||||
c0-2.3-0.5-4.1-1.5-5.4c-1-1.3-2.3-2-4-2c-2.1,0-3.7,0.9-4.7,2.8v9.7c1,1.8,2.6,2.8,4.7,2.8c1.7,0,3-0.7,4-2
|
||||
S825.5,197,825.5,194.4z"/>
|
||||
<path class="st0" d="M867,204.7c-0.2-0.4-0.4-1.1-0.5-2.1c-1.6,1.7-3.5,2.5-5.8,2.5c-2,0-3.6-0.6-4.9-1.7s-1.9-2.6-1.9-4.3
|
||||
c0-2.1,0.8-3.7,2.4-4.9c1.6-1.2,3.9-1.7,6.8-1.7h3.4v-1.7c0-1.2-0.4-2.2-1.1-2.9c-0.7-0.7-1.8-1.1-3.2-1.1
|
||||
c-1.2,0-2.3,0.3-3.1,0.9c-0.8,0.6-1.3,1.4-1.3,2.3h-3.5c0-1,0.4-2,1.1-2.9c0.7-0.9,1.7-1.7,2.9-2.2s2.6-0.8,4-0.8
|
||||
c2.3,0,4.2,0.6,5.5,1.7c1.3,1.2,2,2.8,2.1,4.8v9.3c0,1.9,0.2,3.3,0.7,4.4v0.3H867V204.7z M861.3,202c1.1,0,2.1-0.3,3.1-0.8
|
||||
c1-0.6,1.7-1.3,2.1-2.2v-4.1h-2.7c-4.2,0-6.4,1.2-6.4,3.7c0,1.1,0.4,1.9,1.1,2.5C859.2,201.7,860.2,202,861.3,202z"/>
|
||||
<path class="st0" d="M899.9,184.5l0.1,2.5c1.5-1.9,3.6-2.9,6.1-2.9c4.3,0,6.4,2.4,6.5,7.2v13.4h-3.5v-13.4c0-1.5-0.3-2.5-1-3.2
|
||||
s-1.7-1-3.1-1c-1.1,0-2.1,0.3-3,0.9s-1.5,1.4-2,2.4v14.4h-3.5v-20.2h3.4V184.5z"/>
|
||||
<path class="st0" d="M945.6,199.6l4.7-15.1h3.7l-8.1,23.3c-1.3,3.4-3.3,5-6,5l-0.7-0.1l-1.3-0.2v-2.8l0.9,0.1
|
||||
c1.2,0,2.1-0.2,2.7-0.7c0.7-0.5,1.2-1.3,1.6-2.6l0.8-2.1l-7.2-20h3.8L945.6,199.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st0" d="M221.6,148.7V44.1h29.5c9.1,0,17.1,2,24.1,6s12.4,9.7,16.2,17.2c3.8,7.4,5.7,15.9,5.8,25.6v6.7
|
||||
c0,9.9-1.9,18.5-5.7,25.9c-3.8,7.4-9.2,13.1-16.3,17.1c-7.1,4-15.3,6-24.7,6.1L221.6,148.7L221.6,148.7z M235.4,55.4v82h14.5
|
||||
c10.6,0,18.9-3.3,24.8-9.9c5.9-6.6,8.9-16,8.9-28.3v-6.1c0-11.9-2.8-21.1-8.4-27.7s-13.5-9.9-23.7-10L235.4,55.4L235.4,55.4z"/>
|
||||
<path class="st0" d="M627.1,148.7h-13.9l-52.7-80.6v80.6h-13.9V44.1h13.9l52.8,81v-81H627v104.6H627.1z"/>
|
||||
<path class="st0" d="M900.9,100.1l-12.8,13.3v35.3h-13.8V44.1h13.8v51.7l46.5-51.7h16.7l-41.2,46.2l44.4,58.4H938L900.9,100.1z"/>
|
||||
<polygon class="st0" points="423.2,61.1 455.1,148.7 469.2,148.7 429.2,44.1 417.1,44.1 377.2,148.7 391.4,148.7 "/>
|
||||
<polygon class="st0" points="750.6,61.1 782.6,148.7 796.7,148.7 756.7,44.1 744.6,44.1 704.7,148.7 718.9,148.7 "/>
|
||||
</g>
|
||||
<path class="st0" d="M132.1,140.4c0,0-26.1,25.2-48,46.5v28.9c-2.2,1.7,77.6-75.4,77.6-75.4L84.1,59v31L132.1,140.4z"/>
|
||||
<g>
|
||||
<polygon class="st0" points="83.7,157.5 6.5,75.1 7.5,74.2 84.7,156.6 "/>
|
||||
<polygon class="st0" points="83.7,149.8 13.6,75.2 14.6,74.3 84.6,148.9 "/>
|
||||
<polygon class="st0" points="83.7,142.1 20.7,75.4 21.7,74.4 84.6,141.1 "/>
|
||||
<polygon class="st0" points="83.6,134.3 27.8,75.5 28.7,74.6 84.6,133.4 "/>
|
||||
<polygon class="st0" points="83.6,126.6 34.9,75.6 35.8,74.7 84.6,125.7 "/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st0" points="8,75.5 7.1,74.5 83.7,0 84.6,0.9 "/>
|
||||
<polygon class="st0" points="15.2,75.5 14.3,74.6 83.6,7 84.5,7.9 "/>
|
||||
<polygon class="st0" points="22.4,75.6 21.4,74.7 83.6,14 84.5,14.9 "/>
|
||||
<polygon class="st0" points="29.5,75.6 28.6,74.7 83.5,21 84.4,22 "/>
|
||||
<polygon class="st0" points="36.7,75.7 35.7,74.8 83.5,28 84.4,29 "/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,45 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 201.8 46.7" xml:space="preserve" class="logo-shape dark" style="enable-background:new 0 0 201.8 46.7;"><g><g><path d="M51,38.6h-1.9v5.1h-0.8v-5.1h-1.9V38H51L51,38.6L51,38.6z" class="st0"></path> <path d="M57.6,43.8c-0.6,0-1.1-0.2-1.4-0.6c-0.3-0.4-0.5-0.9-0.5-1.5v-0.2c0-0.6,0.2-1.1,0.5-1.5s0.8-0.6,1.3-0.6
|
||||
c0.6,0,1,0.2,1.3,0.5s0.4,0.8,0.4,1.4v0.4h-2.7l0,0c0,0.4,0.1,0.8,0.3,1c0.2,0.3,0.5,0.4,0.9,0.4c0.3,0,0.5,0,0.7-0.1
|
||||
c0.2-0.1,0.4-0.2,0.5-0.3l0.3,0.5c-0.1,0.1-0.4,0.3-0.6,0.4C58.3,43.7,58,43.8,57.6,43.8z M57.5,40c-0.3,0-0.5,0.1-0.7,0.3
|
||||
c-0.2,0.2-0.3,0.5-0.3,0.8l0,0h1.9V41c0-0.3-0.1-0.6-0.2-0.8S57.8,40,57.5,40z" class="st0"></path> <path d="M66.3,43.2c0.2,0,0.5-0.1,0.6-0.2s0.3-0.4,0.3-0.6h0.7l0,0c0,0.4-0.1,0.7-0.5,1c-0.3,0.3-0.7,0.4-1.1,0.4
|
||||
c-0.6,0-1.1-0.2-1.4-0.6c-0.3-0.4-0.5-0.9-0.5-1.5v-0.2c0-0.6,0.2-1.1,0.5-1.5s0.8-0.6,1.4-0.6c0.5,0,0.9,0.1,1.2,0.4
|
||||
c0.3,0.3,0.5,0.7,0.4,1.1l0,0h-0.7c0-0.3-0.1-0.5-0.3-0.7S66.5,40,66.3,40c-0.4,0-0.7,0.1-0.8,0.4c-0.2,0.3-0.3,0.6-0.3,1.1v0.2
|
||||
c0,0.4,0.1,0.8,0.3,1.1C65.6,43,65.9,43.2,66.3,43.2z" class="st0"></path> <path d="M74,40c0.1-0.2,0.3-0.4,0.5-0.5s0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3v2.7H76V41
|
||||
c0-0.4-0.1-0.6-0.2-0.8C75.6,40.1,75.3,40,75,40c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-6.1H74L74,40L74,40z" class="st0"></path> <path d="M83,39.4l0.1,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3
|
||||
v2.7H85V41c0-0.4-0.1-0.6-0.2-0.8c-0.1-0.2-0.4-0.2-0.7-0.2c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3L83,39.4
|
||||
L83,39.4z" class="st0"></path> <path d="M91.2,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
|
||||
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6c-0.6,0-1-0.2-1.4-0.6C91.4,42.8,91.2,42.3,91.2,41.5L91.2,41.5z M92,41.6
|
||||
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1
|
||||
c-0.2-0.3-0.5-0.5-0.9-0.5s-0.6,0.1-0.8,0.5S92,41.1,92,41.6L92,41.6z" class="st0"></path> <path d="M101.3,43.7h-0.8v-6.1h0.8V43.7z" class="st0"></path> <path d="M106.8,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
|
||||
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6s-1-0.2-1.4-0.6C107,42.8,106.8,42.3,106.8,41.5L106.8,41.5z M107.6,41.6
|
||||
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1s-0.5-0.5-0.9-0.5
|
||||
s-0.6,0.1-0.8,0.5S107.6,41.1,107.6,41.6L107.6,41.6z" class="st0"></path> <path d="M115.9,41.7c0-0.7,0.1-1.2,0.4-1.7s0.7-0.6,1.2-0.6c0.3,0,0.5,0,0.7,0.2c0.2,0.1,0.4,0.3,0.5,0.5l0.1-0.6h0.6
|
||||
v4.3c0,0.5-0.2,1-0.5,1.3s-0.8,0.4-1.4,0.4c-0.2,0-0.4,0-0.7-0.1c-0.2-0.1-0.5-0.1-0.6-0.2l0.1-0.6c0.1,0.1,0.3,0.1,0.5,0.2
|
||||
c0.2,0,0.4,0.1,0.6,0.1c0.4,0,0.6-0.1,0.8-0.3c0.2-0.2,0.3-0.4,0.3-0.8v-0.5c-0.1,0.2-0.3,0.3-0.5,0.4c-0.2,0.1-0.4,0.1-0.7,0.1
|
||||
c-0.5,0-0.9-0.2-1.2-0.6S115.9,42.4,115.9,41.7L115.9,41.7L115.9,41.7z M116.7,41.7c0,0.4,0.1,0.8,0.3,1s0.5,0.4,0.8,0.4
|
||||
c0.2,0,0.4,0,0.6-0.2c0.2-0.1,0.3-0.3,0.4-0.5v-2c-0.1-0.2-0.2-0.3-0.4-0.4c-0.2-0.1-0.4-0.2-0.6-0.2c-0.4,0-0.6,0.1-0.8,0.5
|
||||
S116.7,41.2,116.7,41.7L116.7,41.7L116.7,41.7z" class="st0"></path> <path d="M126.4,42.1l0.1,0.5h0l1-3.2h0.9l-1.8,4.9c-0.1,0.3-0.3,0.5-0.5,0.8c-0.2,0.2-0.5,0.3-0.8,0.3
|
||||
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.2,0l0.1-0.6c0,0,0,0,0.1,0c0.1,0,0.2,0,0.2,0c0.2,0,0.3-0.1,0.4-0.2c0.1-0.1,0.2-0.3,0.3-0.5
|
||||
l0.2-0.5l-1.6-4.2h0.9L126.4,42.1z" class="st0"></path> <path d="M144.4,41.9L144.4,41.9c0,0.6-0.2,1-0.6,1.4s-0.9,0.5-1.5,0.5c-0.6,0-1.2-0.2-1.6-0.7s-0.6-1-0.6-1.8v-1
|
||||
c0-0.7,0.2-1.3,0.6-1.8s0.9-0.7,1.6-0.7s1.2,0.2,1.5,0.5c0.4,0.3,0.6,0.8,0.6,1.4l0,0h-0.8c0-0.4-0.1-0.7-0.4-1
|
||||
c-0.2-0.2-0.6-0.4-1-0.4s-0.8,0.2-1,0.5c-0.3,0.4-0.4,0.8-0.4,1.3v1c0,0.5,0.1,1,0.4,1.3s0.6,0.5,1,0.5c0.4,0,0.8-0.1,1-0.4
|
||||
s0.4-0.5,0.4-1L144.4,41.9L144.4,41.9z" class="st0"></path> <path d="M149.6,41.5c0-0.6,0.2-1.1,0.5-1.6s0.8-0.6,1.4-0.6c0.6,0,1.1,0.2,1.4,0.6s0.5,0.9,0.5,1.6v0.1
|
||||
c0,0.6-0.2,1.2-0.5,1.6s-0.8,0.6-1.4,0.6s-1-0.2-1.4-0.6C149.8,42.8,149.6,42.3,149.6,41.5L149.6,41.5z M150.4,41.6
|
||||
c0,0.5,0.1,0.8,0.3,1.1s0.5,0.4,0.9,0.4c0.4,0,0.6-0.1,0.8-0.4s0.3-0.7,0.3-1.1v-0.1c0-0.5-0.1-0.8-0.3-1.1s-0.5-0.5-0.9-0.5
|
||||
s-0.6,0.1-0.8,0.5S150.4,41.1,150.4,41.6L150.4,41.6z" class="st0"></path> <path d="M159.5,39.4l0,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.8-0.2s0.5,0.1,0.7,0.2s0.4,0.3,0.5,0.6
|
||||
c0.1-0.3,0.3-0.4,0.5-0.6c0.2-0.1,0.5-0.2,0.8-0.2c0.4,0,0.8,0.1,1,0.5c0.3,0.3,0.4,0.8,0.4,1.3v2.5H164v-2.6
|
||||
c0-0.4-0.1-0.7-0.2-0.9c-0.1-0.2-0.4-0.3-0.6-0.3c-0.3,0-0.5,0.1-0.6,0.3c-0.2,0.2-0.3,0.4-0.3,0.7v0v2.7h-0.8v-2.6
|
||||
c0-0.4-0.1-0.7-0.2-0.9s-0.4-0.3-0.6-0.3c-0.2,0-0.4,0-0.6,0.1s-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3L159.5,39.4L159.5,39.4z" class="st0"></path> <path d="M174,41.7c0,0.6-0.1,1.1-0.4,1.5s-0.7,0.6-1.2,0.6c-0.3,0-0.5,0-0.7-0.1s-0.4-0.2-0.5-0.4v2.1h-0.8v-5.9h0.6
|
||||
l0.1,0.5c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.9,0.2,1.2,0.6S174,41,174,41.7L174,41.7L174,41.7z M173.2,41.7
|
||||
c0-0.5-0.1-0.9-0.3-1.2s-0.5-0.5-0.8-0.5c-0.2,0-0.4,0-0.6,0.1s-0.3,0.2-0.4,0.4v2.1c0.1,0.2,0.2,0.3,0.4,0.4s0.4,0.1,0.6,0.1
|
||||
c0.4,0,0.6-0.1,0.8-0.4C173.1,42.5,173.2,42.2,173.2,41.7L173.2,41.7L173.2,41.7z" class="st0"></path> <path d="M182.1,43.7c0-0.1,0-0.2-0.1-0.3s0-0.2,0-0.3c-0.1,0.2-0.3,0.4-0.6,0.5s-0.5,0.2-0.8,0.2
|
||||
c-0.4,0-0.8-0.1-1-0.3s-0.4-0.5-0.4-0.9c0-0.4,0.2-0.7,0.5-1c0.3-0.2,0.8-0.3,1.3-0.3h0.8v-0.4c0-0.3-0.1-0.5-0.2-0.6
|
||||
s-0.4-0.2-0.6-0.2c-0.3,0-0.5,0.1-0.6,0.2c-0.2,0.1-0.2,0.3-0.2,0.5l-0.7,0l0,0c0-0.3,0.1-0.6,0.4-0.9s0.7-0.4,1.2-0.4
|
||||
c0.5,0,0.9,0.1,1.2,0.4c0.3,0.3,0.4,0.6,0.4,1.1v2c0,0.1,0,0.3,0,0.4c0,0.1,0,0.3,0.1,0.4L182.1,43.7L182.1,43.7z M180.8,43.1
|
||||
c0.3,0,0.5-0.1,0.8-0.2c0.2-0.1,0.4-0.3,0.4-0.5v-0.7h-0.9c-0.3,0-0.6,0.1-0.8,0.2s-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.2,0.5
|
||||
S180.5,43.1,180.8,43.1z" class="st0"></path> <path d="M189.1,39.4l0.1,0.6c0.1-0.2,0.3-0.4,0.5-0.5c0.2-0.1,0.5-0.2,0.7-0.2c0.5,0,0.8,0.1,1.1,0.4s0.4,0.7,0.4,1.3
|
||||
v2.7H191V41c0-0.4-0.1-0.6-0.2-0.8c-0.1-0.2-0.4-0.2-0.7-0.2c-0.2,0-0.4,0-0.6,0.1c-0.2,0.1-0.3,0.2-0.4,0.4v3.2h-0.8v-4.3
|
||||
L189.1,39.4L189.1,39.4z" class="st0"></path> <path d="M198.7,42.1l0.1,0.5h0l1-3.2h0.9l-1.8,4.9c-0.1,0.3-0.3,0.5-0.5,0.8s-0.5,0.3-0.8,0.3c-0.1,0-0.1,0-0.2,0
|
||||
c-0.1,0-0.2,0-0.2,0l0.1-0.6c0,0,0,0,0.1,0s0.2,0,0.2,0c0.2,0,0.3-0.1,0.4-0.2s0.2-0.3,0.3-0.5l0.2-0.5l-1.6-4.2h0.9L198.7,42.1z" class="st0"></path></g></g> <g><path d="M46.4,31.9V9.9h6.2c1.9,0,3.6,0.4,5.1,1.3c1.5,0.9,2.6,2.1,3.4,3.6c0.8,1.6,1.2,3.4,1.2,5.4v1.4
|
||||
c0,2.1-0.4,3.9-1.2,5.5c-0.8,1.6-2,2.8-3.4,3.6s-3.2,1.3-5.2,1.3H46.4z M49.3,12.2v17.3h3.1c2.2,0,4-0.7,5.2-2.1
|
||||
c1.3-1.4,1.9-3.4,1.9-6v-1.3c0-2.5-0.6-4.5-1.8-5.9s-2.9-2.1-5-2.1L49.3,12.2L49.3,12.2z" class="st0"></path> <path d="M131.9,31.9H129l-11.1-17v17h-2.9V9.9h2.9L129,26.9V9.9h2.9L131.9,31.9L131.9,31.9z" class="st0"></path> <path d="M189.6,21.7l-2.7,2.8v7.5H184V9.8h2.9v10.9l9.8-10.9h3.5l-8.7,9.7l9.4,12.3h-3.5L189.6,21.7z" class="st0"></path> <polygon points="88.9,13.4 95.6,31.9 98.6,31.9 90.1,9.8 87.6,9.8 79.2,31.9 82.2,31.9 " class="st0"></polygon> <polygon points="157.9,13.4 164.7,31.9 167.6,31.9 159.2,9.8 156.7,9.8 148.2,31.9 151.2,31.9 " class="st0"></polygon></g> <path d="M27.5,30.2c0,0-5.5,5.3-10.1,9.8v6.1c-0.5,0.4,16.4-15.9,16.4-15.9L17.4,13v6.5L27.5,30.2z" class="st0"></path> <g><rect x="9.1" y="13.1" transform="matrix(0.7298 -0.6837 0.6837 0.7298 -14.5845 13.0725)" width="0.3" height="23.8" class="st0"></rect> <rect x="9.9" y="13.4" transform="matrix(0.7287 -0.6849 0.6849 0.7287 -13.8472 13.4064)" width="0.3" height="21.6" class="st0"></rect> <polygon points="17.3,30.5 4,16.4 4.2,16.2 17.5,30.3 " class="st0"></polygon> <polygon points="17.3,28.9 5.5,16.5 5.7,16.3 17.5,28.7 " class="st0"></polygon> <polygon points="17.2,27.2 7,16.5 7.2,16.3 17.5,27.1 " class="st0"></polygon></g> <g><polygon points="1.3,16.5 1.1,16.3 17.3,0.5 17.5,0.7 " class="st0"></polygon> <polygon points="2.8,16.5 2.6,16.3 17.3,2 17.4,2.2 " class="st0"></polygon> <polygon points="4.3,16.5 4.1,16.3 17.2,3.5 17.4,3.7 " class="st0"></polygon> <polygon points="5.8,16.5 5.6,16.3 17.2,5 17.4,5.2 " class="st0"></polygon> <polygon points="7.4,16.5 7.2,16.3 17.2,6.5 17.4,6.7 " class="st0"></polygon></g></svg>
|
||||
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="24" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.647 0.479492C16.8265 0.521346 17.009 0.558249 17.188 0.605502C18.888 1.05554 20.0825 2.66307 19.9932 4.37501C19.8959 6.23726 18.5754 7.73093 16.7699 7.9816C15.5651 8.14901 14.5061 7.80158 13.6261 6.95236C13.4812 6.81285 13.3928 6.82095 13.2381 6.91771C11.3048 8.12591 9.36827 9.32931 7.42844 10.5279C7.30464 10.6044 7.27594 10.6688 7.32393 10.8096C7.59332 11.5806 7.59332 12.4207 7.32393 13.1917C7.27638 13.3325 7.30554 13.3964 7.42934 13.4729C9.38113 14.6793 11.3304 15.8905 13.2771 17.1065C13.4247 17.1988 13.4933 17.1682 13.6081 17.0615C14.7084 16.035 15.9931 15.706 17.4208 16.1835C18.8561 16.6637 19.7062 17.7073 19.9524 19.201C20.2866 21.2298 18.8239 23.1874 16.7946 23.4763C16.7439 23.484 16.6888 23.4763 16.6484 23.5214H15.841C15.4431 23.4282 15.0426 23.3499 14.6662 23.1748C13.8364 22.7926 13.1743 22.1195 12.8041 21.2821C12.4339 20.4447 12.3812 19.5004 12.6558 18.6268C12.7151 18.4386 12.6935 18.3491 12.5146 18.2388C10.5795 17.0486 8.64954 15.85 6.72467 14.643C6.5901 14.5593 6.52282 14.5665 6.40665 14.6759C5.32699 15.6948 4.05804 16.0377 2.64331 15.589C1.22859 15.1403 0.365577 14.1385 0.0704313 12.6813C-0.354794 10.5806 1.19764 8.48339 3.31928 8.25837C4.52498 8.13056 5.55844 8.49689 6.42414 9.33846C6.52596 9.43747 6.5901 9.44062 6.70673 9.36771C8.65911 8.14901 10.614 6.93391 12.5715 5.72242C12.6967 5.64456 12.72 5.582 12.6742 5.43754C11.9471 3.15496 13.3596 0.890825 15.7356 0.515945C15.7713 0.506836 15.8061 0.494639 15.8397 0.479492L16.647 0.479492ZM16.2321 22.1411C16.7034 22.1421 17.1645 22.0029 17.557 21.7412C17.9496 21.4795 18.256 21.1069 18.4377 20.6706C18.6193 20.2342 18.6681 19.7537 18.5777 19.2896C18.4874 18.8255 18.262 18.3986 17.9301 18.0629C17.5981 17.7273 17.1745 17.4978 16.7126 17.4035C16.2508 17.3092 15.7714 17.3543 15.3351 17.5331C14.8988 17.7119 14.5251 18.0164 14.2611 18.4082C13.9972 18.8 13.8549 19.2614 13.8521 19.7343C13.8501 20.0493 13.9102 20.3617 14.0288 20.6533C14.1475 20.945 14.3224 21.2103 14.5436 21.434C14.7647 21.6576 15.0278 21.8352 15.3175 21.9566C15.6073 22.0779 15.9181 22.1406 16.2321 22.1411ZM16.2299 1.8593C15.7587 1.86064 15.2984 2.002 14.9072 2.26554C14.516 2.52908 14.2113 2.90299 14.0317 3.34008C13.8521 3.77716 13.8056 4.25784 13.898 4.72144C13.9904 5.18504 14.2176 5.61078 14.551 5.94493C14.8844 6.27908 15.3089 6.50666 15.7711 6.59895C16.2332 6.69124 16.7123 6.6441 17.1477 6.46348C17.5832 6.28287 17.9556 5.97687 18.2179 5.58411C18.4802 5.19135 18.6207 4.72943 18.6216 4.25665C18.6232 3.94118 18.5624 3.62853 18.4427 3.3368C18.3231 3.04506 18.1469 2.78004 17.9245 2.55708C17.7021 2.33411 17.4377 2.15763 17.1469 2.03786C16.856 1.91808 16.5443 1.85739 16.2299 1.8593ZM3.76783 9.60938C3.2971 9.60992 2.83707 9.75031 2.44578 10.0129C2.05448 10.2754 1.74944 10.6483 1.56914 11.0846C1.38884 11.5209 1.34136 12.0009 1.43267 12.4643C1.52399 12.9276 1.75002 13.3534 2.08225 13.688C2.41448 14.0226 2.83803 14.2509 3.29947 14.3443C3.76091 14.4376 4.23957 14.3918 4.67508 14.2125C5.11058 14.0333 5.48342 13.7286 5.74656 13.337C6.0097 12.9454 6.15136 12.4844 6.15366 12.0121C6.15598 11.6967 6.09591 11.384 5.97695 11.092C5.85798 10.8 5.68247 10.5346 5.46058 10.3111C5.23869 10.0877 4.97483 9.91059 4.68427 9.79013C4.3937 9.66968 4.08221 9.60824 3.76783 9.60938Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,32 @@
|
||||
import { type ButtonHTMLAttributes, type FC, memo, type ReactNode } from 'react'
|
||||
import MoonLoader from "react-spinners/MoonLoader"
|
||||
import { type XOR } from '../helpers/types'
|
||||
import { clx } from '../helpers/utils'
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
isLoading?: boolean,
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
XOR<{ children: ReactNode }, { label: string }>;
|
||||
|
||||
const Button: FC<Props> = memo((props: Props) => {
|
||||
|
||||
const buttonClass = clx(
|
||||
'flex rounded-xl items-center justify-center text-center h-10 text-sm bg-primary text-white w-full',
|
||||
props.disabled && 'cursor-not-allowed opacity-60',
|
||||
props.className
|
||||
);
|
||||
|
||||
return (
|
||||
<button disabled={props.isLoading} {...props} className={`${buttonClass} ${props.className}`} >
|
||||
{
|
||||
props.isLoading ?
|
||||
<MoonLoader color="white" size={16} />
|
||||
:
|
||||
props.label || props.children
|
||||
}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
||||
export default Button
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useState, useEffect, type FC } from 'react';
|
||||
import DatePicker from 'react-multi-date-picker';
|
||||
import persian from 'react-date-object/calendars/persian';
|
||||
import persian_fa from 'react-date-object/locales/persian_fa';
|
||||
import DateObject from 'react-date-object';
|
||||
import CalenderIcon from '../assets/images/calendar.svg'
|
||||
import { clx } from '../helpers/utils';
|
||||
|
||||
type Props = {
|
||||
onChange: (date: string) => void;
|
||||
defaulValue?: string;
|
||||
error_text?: string;
|
||||
placeholder: string;
|
||||
reset?: boolean;
|
||||
isDateTime?: boolean;
|
||||
className?: string;
|
||||
label?: string
|
||||
};
|
||||
|
||||
const DatePickerComponent: FC<Props> = (props: Props) => {
|
||||
const [value, setValue] = useState<DateObject | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.reset) {
|
||||
setValue(null);
|
||||
}
|
||||
}, [props.reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const formattedDate = `${value.year}/${value.month.number}/${value.day}`;
|
||||
props.onChange(formattedDate);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.defaulValue && !value) {
|
||||
const defaultDate = new DateObject({
|
||||
date: props.defaulValue,
|
||||
calendar: persian,
|
||||
locale: persian_fa,
|
||||
});
|
||||
setValue(defaultDate);
|
||||
}
|
||||
}, [props.defaulValue, value]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{props.label &&
|
||||
<div className='text-sm'>
|
||||
{props.label}
|
||||
</div>}
|
||||
<div className={clx(
|
||||
'relative ',
|
||||
props.label && 'mt-1.5'
|
||||
)}>
|
||||
<DatePicker
|
||||
placeholder={props.placeholder}
|
||||
value={value}
|
||||
onChange={(date) => setValue(date as DateObject)}
|
||||
calendar={persian}
|
||||
locale={persian_fa}
|
||||
calendarPosition="bottom-right"
|
||||
className={`rmdp-mobile ${props.className}`}
|
||||
/>
|
||||
{props.error_text && props.error_text !== '' && (
|
||||
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
|
||||
{props.error_text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img src={CalenderIcon} className='absolute top-0 bottom-0 my-auto left-2' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePickerComponent;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { type FC, Fragment, type ReactNode, useEffect } from 'react'
|
||||
import HeaderModal from './HeaderModal'
|
||||
|
||||
interface Props {
|
||||
open: boolean,
|
||||
close: () => void,
|
||||
children: ReactNode,
|
||||
isHeader?: boolean,
|
||||
title_header?: string,
|
||||
width?: number
|
||||
}
|
||||
|
||||
const DefaulModal: FC<Props> = (props: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (props.open) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = 'auto'
|
||||
}
|
||||
}, [props.open])
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
props.open && (
|
||||
<Fragment>
|
||||
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-x-hidden overflow-y-auto fixed inset-0 z-[60] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
||||
<div className='relative xl:h-full h-[80%] bottom-0 left-0 flex xl:items-center sm:h-auto w-full xl:my-6 xl:p-2'>
|
||||
<div className='border-0 h-auto p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full modalGlass2 outline-none focus:outline-none'>
|
||||
|
||||
|
||||
{
|
||||
props.isHeader && props.title_header &&
|
||||
<div className='pb-6 border-b border-white/20'>
|
||||
<div className='h-[5px] w-[200px] mx-auto bg-[#D1D3D7] rounded-full mb-4 xl:hidden'></div>
|
||||
<HeaderModal close={props.close} label={props.title_header} />
|
||||
</div>
|
||||
}
|
||||
|
||||
{props.children}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div onClick={props.close} className='fixed size-full top-0 bottom-0 right-0 modalGlass inset-0 z-50 '></div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaulModal
|
||||
@@ -0,0 +1,15 @@
|
||||
import { type FC } from 'react'
|
||||
|
||||
type Props = {
|
||||
errorText: string
|
||||
}
|
||||
|
||||
const Error: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='mt-1.5 font-normal text-red-500 text-[11px]'>
|
||||
{props.errorText}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Error
|
||||
@@ -0,0 +1,21 @@
|
||||
import { type FC } from 'react'
|
||||
import XIcon from '../assets/images/close-circle.svg'
|
||||
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
close: () => void,
|
||||
}
|
||||
|
||||
const HeaderModal: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='text-sm'>{props.label}</div>
|
||||
<div className='size-7 rounded-full bg-white bg-opacity-35 flex justify-center items-center'>
|
||||
<img src={XIcon} alt='close' className='w-4 h-4' onClick={props.close} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HeaderModal
|
||||
@@ -0,0 +1,115 @@
|
||||
import { type FC, type InputHTMLAttributes, useEffect, useState } from 'react'
|
||||
import { clx } from '../helpers/utils';
|
||||
import EyeIcon from '../assets/images/eye.svg'
|
||||
import { SearchNormal } from 'iconsax-react';
|
||||
import Error from './Error';
|
||||
|
||||
type Variant = "floating_outlined" | "primary" | "search";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
className?: string;
|
||||
variant?: Variant;
|
||||
error_text?: string;
|
||||
onEnter?: () => void;
|
||||
unit?: string;
|
||||
seprator?: boolean;
|
||||
isNotRequired?: boolean;
|
||||
onChangeSearchFinal?: (value: string) => void;
|
||||
} & InputHTMLAttributes<HTMLInputElement>
|
||||
|
||||
const formatNumber = (value: string | number): string => {
|
||||
if (!value) return '';
|
||||
const inputValue = String(value).replace(/,/g, '');
|
||||
return inputValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
};
|
||||
|
||||
const Input: FC<Props> = (props: Props) => {
|
||||
|
||||
const [formattedValue, setFormattedValue] = useState<string>(
|
||||
props.value ? formatNumber(props.value as string) : ''
|
||||
);
|
||||
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||
const [search, setSearch] = useState<string>('')
|
||||
|
||||
const inputClass = clx(
|
||||
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
||||
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
|
||||
props.className
|
||||
);
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (props.seprator) {
|
||||
const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها
|
||||
const formatted = formatNumber(inputValue);
|
||||
|
||||
// بهروزرسانی مقدار قالببندیشده
|
||||
setFormattedValue(formatted);
|
||||
|
||||
// ارسال مقدار خام به `onChange` والد
|
||||
props.onChange?.({
|
||||
...event,
|
||||
target: {
|
||||
...event.target,
|
||||
value: inputValue,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
props.onChange?.(event);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value) {
|
||||
setFormattedValue(formatNumber(props.value as string));
|
||||
} else {
|
||||
setFormattedValue('');
|
||||
}
|
||||
}, [props.value])
|
||||
|
||||
useEffect(() => {
|
||||
if (props.variant === 'search' && props.onChangeSearchFinal) {
|
||||
const timeout = setTimeout(() => {
|
||||
props.onChangeSearchFinal?.(search)
|
||||
}, 1000)
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
}, [search, props])
|
||||
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<label className='text-sm'>
|
||||
{props.label}
|
||||
</label>
|
||||
|
||||
<div className='w-full relative mt-1'>
|
||||
<input {...props} onChange={(e) => {
|
||||
setSearch(e.target.value)
|
||||
handleInputChange(e)
|
||||
}} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
|
||||
|
||||
{
|
||||
props.type === 'password' &&
|
||||
<img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
|
||||
}
|
||||
|
||||
{
|
||||
props.variant === 'search' &&
|
||||
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
|
||||
}
|
||||
|
||||
{
|
||||
props.error_text &&
|
||||
<Error
|
||||
errorText={props.error_text}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input
|
||||
@@ -0,0 +1,67 @@
|
||||
import { type FC, useState } from 'react'
|
||||
import DefaulModal from './DefaulModal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from './Button'
|
||||
import Textarea from './Textarea'
|
||||
|
||||
type Props = {
|
||||
isLoading?: boolean,
|
||||
close: () => void,
|
||||
isOpen: boolean,
|
||||
onConfrim: (text?: string) => void,
|
||||
label?: string,
|
||||
isHasDescription?: boolean
|
||||
}
|
||||
|
||||
const ModalConfrim: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [description, setDescription] = useState<string>('')
|
||||
|
||||
return (
|
||||
<DefaulModal
|
||||
open={props.isOpen}
|
||||
close={props.close}
|
||||
title_header={t('confrim.subject')}
|
||||
isHeader
|
||||
>
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm text-center'>
|
||||
{
|
||||
props.label ?
|
||||
props.label
|
||||
:
|
||||
t('confrim.content')
|
||||
}
|
||||
|
||||
{
|
||||
props.isHasDescription &&
|
||||
<div className='mt-4'>
|
||||
<Textarea
|
||||
placeholder={t('description')}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
value={description}
|
||||
className='bg-transparent border border-gray-500 rounded-xl w-[300px] p-2'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='flex gap-4 justify-center mt-10'>
|
||||
<Button
|
||||
label={t('confrim.yes')}
|
||||
onClick={() => props.onConfrim(props.isHasDescription ? description : undefined)}
|
||||
isLoading={props.isLoading}
|
||||
/>
|
||||
<Button
|
||||
label={t('confrim.cancel')}
|
||||
className='bg-transparent text-black border border-primary'
|
||||
onClick={props.close}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DefaulModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalConfrim
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type FC } from 'react'
|
||||
import Logo from '../assets/images/logo_orig.svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const PageLoading: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
return (
|
||||
<div className='flex bg-transparent w-fit mx-auto flex-col gap-4 items-center'>
|
||||
<img src={Logo} alt='logo' className='w-28' />
|
||||
<div className='text-xs text-gray-700'>
|
||||
{t('loading')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageLoading
|
||||
@@ -0,0 +1,99 @@
|
||||
import React from "react";
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
onPageChange: (page: number) => void;
|
||||
}
|
||||
|
||||
const Pagination: React.FC<PaginationProps> = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPageChange,
|
||||
}) => {
|
||||
const getPageNumbers = () => {
|
||||
const pageNumbers: (number | string)[] = [];
|
||||
const maxVisiblePages = 5; // تعداد حداکثری صفحات قابل مشاهده
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
} else {
|
||||
if (currentPage > 3) {
|
||||
pageNumbers.push(1);
|
||||
if (currentPage > 4) {
|
||||
pageNumbers.push("...");
|
||||
}
|
||||
}
|
||||
|
||||
const start = Math.max(2, currentPage - 1);
|
||||
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - 2) {
|
||||
if (currentPage < totalPages - 3) {
|
||||
pageNumbers.push("...");
|
||||
}
|
||||
pageNumbers.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pageNumbers;
|
||||
};
|
||||
|
||||
const pageNumbers = getPageNumbers();
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 text-xs justify-center items-center space-x-2 mt-4">
|
||||
{/* دکمه قبلی */}
|
||||
{/* <button
|
||||
className={`px-3 py-1 rounded-md ${currentPage === 1
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-blue-600 hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={() => currentPage > 1 && onPageChange(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
قبلی
|
||||
</button> */}
|
||||
|
||||
{/* شماره صفحات */}
|
||||
{pageNumbers.map((page, index) =>
|
||||
typeof page === "number" ? (
|
||||
<button
|
||||
key={index}
|
||||
className={`size-8 rounded-md ${currentPage === page
|
||||
? "bg-primary text-white"
|
||||
: "text-primary bg-[#EAECF5] hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={() => onPageChange(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
) : (
|
||||
<span key={index} className="px-3 py-1 text-gray-500">
|
||||
{page}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* دکمه بعدی */}
|
||||
{/* <button
|
||||
className={`px-3 py-1 rounded-md ${currentPage === totalPages
|
||||
? "text-gray-400 cursor-not-allowed"
|
||||
: "text-blue-600 hover:bg-gray-100"
|
||||
}`}
|
||||
onClick={() => currentPage < totalPages && onPageChange(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
بعدی
|
||||
</button> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type FC } from 'react'
|
||||
|
||||
type Props = {
|
||||
isActive: boolean,
|
||||
value: string | boolean,
|
||||
onChange: (value: string | boolean) => void
|
||||
}
|
||||
|
||||
const Radio: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div onClick={() => props.onChange(props.value)} className='size-4 cursor-pointer rounded-full bg-[#EAEDF5] flex justify-center items-center'>
|
||||
{
|
||||
props.isActive &&
|
||||
<div className='size-2 bg-black rounded-full'></div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Radio
|
||||
@@ -0,0 +1,31 @@
|
||||
import { type FC } from 'react'
|
||||
import Radio from './Radio'
|
||||
|
||||
|
||||
type Props = {
|
||||
items: {
|
||||
label: string
|
||||
value: string | boolean
|
||||
}[]
|
||||
selected: string | boolean
|
||||
onChange: (value: string | boolean) => void
|
||||
}
|
||||
|
||||
const RadioGroup: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='flex gap-5 items-center text-xs'>
|
||||
{
|
||||
props.items.map((item, index) => (
|
||||
<div key={index} className='flex gap-2 items-center'>
|
||||
<Radio value={item.value} onChange={props.onChange} isActive={item.value === props.selected} />
|
||||
<div className='mt-0.5'>
|
||||
{item.label}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RadioGroup
|
||||
@@ -0,0 +1,62 @@
|
||||
import { type FC, type SelectHTMLAttributes } from 'react'
|
||||
import { clx } from '../helpers/utils'
|
||||
import { ArrowDown2 } from 'iconsax-react'
|
||||
import Error from './Error'
|
||||
|
||||
export type ItemsSelectType = {
|
||||
value: string,
|
||||
label: string,
|
||||
}
|
||||
type Props = {
|
||||
className?: string,
|
||||
items: ItemsSelectType[],
|
||||
error_text?: string,
|
||||
placeholder?: string,
|
||||
label?: string,
|
||||
} & SelectHTMLAttributes<HTMLSelectElement>
|
||||
|
||||
const Select: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='w-full relative'>
|
||||
{
|
||||
props.label &&
|
||||
<label className='text-sm'>
|
||||
{props.label}
|
||||
</label>
|
||||
}
|
||||
<select {...props} className={clx(
|
||||
'w-full text-black block border appearance-none border-border px-2.5 h-10 text-sm rounded-2.5 bg-gray',
|
||||
props.className,
|
||||
props.label && 'mt-1'
|
||||
)}>
|
||||
{
|
||||
props.placeholder &&
|
||||
<option value="" disabled selected>{props.placeholder}</option>
|
||||
}
|
||||
{
|
||||
props.items?.map((item) => {
|
||||
return (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
<ArrowDown2 size={16} color='black' className={clx(
|
||||
'absolute z-0 top-3 left-2',
|
||||
props.label && 'top-10'
|
||||
)} />
|
||||
{
|
||||
props.error_text && props.error_text !== '' ?
|
||||
<Error
|
||||
errorText={props.error_text}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Select
|
||||
@@ -0,0 +1,19 @@
|
||||
import { type FC } from 'react'
|
||||
|
||||
const ServiceSection: FC = () => {
|
||||
return (
|
||||
<div className='flex gap-4'>
|
||||
<div className='size-10 rounded-xl bg-green-300'></div>
|
||||
<div>
|
||||
<div className='text-sm'>
|
||||
دی منو
|
||||
</div>
|
||||
<div className='text-xs text-description'>
|
||||
منو رستوران
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServiceSection
|
||||
@@ -0,0 +1,15 @@
|
||||
import { type FC } from 'react'
|
||||
|
||||
type Props = {
|
||||
color: string
|
||||
}
|
||||
|
||||
const StatusCircle: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div style={{ background: props.color }} className='size-1.5 rounded-full'>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusCircle
|
||||
@@ -0,0 +1,22 @@
|
||||
import { type FC } from 'react'
|
||||
import { clx } from '../helpers/utils'
|
||||
|
||||
type Props = {
|
||||
variant: 'success' | 'error' | 'warning',
|
||||
text: string,
|
||||
}
|
||||
|
||||
const StatusWithText: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className={clx(
|
||||
'w-fit py-1 px-2 text-xs h-fit rounded-full',
|
||||
props.variant === 'success' && 'bg-green-100 text-success',
|
||||
props.variant === 'error' && 'bg-red-100 text-red-400',
|
||||
props.variant === 'warning' && 'bg-yellow-100 text-yellow-400',
|
||||
)}>
|
||||
{props.text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusWithText
|
||||
@@ -0,0 +1,50 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { clx } from '../helpers/utils'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||
|
||||
type Item = {
|
||||
icon: ReactNode,
|
||||
label: string,
|
||||
value: string,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
items: Item[],
|
||||
onChange: (value: string) => void,
|
||||
active: string,
|
||||
}
|
||||
|
||||
const Tabs: FC<Props> = (props: Props) => {
|
||||
|
||||
const SWIPER_SLIDE_STYLE = {
|
||||
overflow: 'visible !important',
|
||||
display: 'flex',
|
||||
}
|
||||
|
||||
return (
|
||||
<Swiper
|
||||
slidesPerView='auto'
|
||||
spaceBetween={30}
|
||||
className='px-10 max-w-full w-fit items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-10 flex h-[80px] rounded-[32px] bg-white bg-opacity-45'>
|
||||
{
|
||||
props.items.map((item: Item, index: number) => {
|
||||
return (
|
||||
<SwiperSlide style={SWIPER_SLIDE_STYLE} onClick={() => props.onChange(item.value)} key={item.value} className={clx(
|
||||
'flex flex-col max-w-fit mt-[15px] items-center gap-2 cursor-pointer',
|
||||
index === 0 && 'pr-[30px]',
|
||||
index === props.items.length - 1 && 'pl-[30px]',
|
||||
props.active === item.value && 'text-black'
|
||||
)}>
|
||||
{item.icon}
|
||||
<div className='text-xs'>
|
||||
{item.label}
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Swiper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tabs
|
||||
@@ -0,0 +1,22 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
text: string,
|
||||
children?: ReactNode,
|
||||
dir?: string,
|
||||
}
|
||||
|
||||
const Td: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<td className='td' style={{ direction: props.dir === "ltr" ? "ltr" : "rtl" }}>
|
||||
{
|
||||
props.text ?
|
||||
props.text
|
||||
:
|
||||
props.children
|
||||
}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
export default Td
|
||||
@@ -0,0 +1,39 @@
|
||||
import { type FC, type InputHTMLAttributes } from 'react'
|
||||
import Error from './Error'
|
||||
import { clx } from '../helpers/utils'
|
||||
|
||||
type Props = {
|
||||
label?: string,
|
||||
error_text?: string
|
||||
} & InputHTMLAttributes<HTMLTextAreaElement>
|
||||
|
||||
const Textarea: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='w-full relative'>
|
||||
{
|
||||
props.label &&
|
||||
<div className='text-sm' >
|
||||
{props.label}
|
||||
</div>
|
||||
}
|
||||
|
||||
<textarea
|
||||
{...props}
|
||||
className={clx(
|
||||
'border border-border leading-7 w-full rounded-xl px-4 py-3 min-h-[100px] mt-1 text-xs',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
|
||||
</textarea>
|
||||
|
||||
{
|
||||
props.error_text &&
|
||||
<Error errorText={props.error_text} />
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Textarea
|
||||
@@ -0,0 +1,18 @@
|
||||
import { type FC } from 'react'
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
}
|
||||
|
||||
const TitleLine: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<div className='w-full text-sm flex items-center gap-4'>
|
||||
<div className='whitespace-nowrap'>{props.title}</div>
|
||||
<svg style={{ width: '100%', height: 1 }} xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
|
||||
<line x1="0" y1="0" x2="100%" y2="0" stroke="#aaa" strokeWidth="1" strokeDasharray="5 3" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TitleLine
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Trash } from 'iconsax-react'
|
||||
import { type FC, useState } from 'react'
|
||||
import ModalConfrim from './ModalConfrim'
|
||||
|
||||
interface Props {
|
||||
onDelete: () => void,
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const TrashWithConfrim: FC<Props> = ({ onDelete, isLoading = false }) => {
|
||||
const [isConfirm, setIsConfirm] = useState(false)
|
||||
return (
|
||||
<div>
|
||||
<Trash
|
||||
onClick={() => setIsConfirm(true)}
|
||||
className='size-5'
|
||||
color='#888'
|
||||
/>
|
||||
|
||||
<ModalConfrim
|
||||
isOpen={isConfirm}
|
||||
close={() => setIsConfirm(false)}
|
||||
onConfrim={() => onDelete()}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TrashWithConfrim
|
||||
@@ -0,0 +1,91 @@
|
||||
import { type FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { CloseCircle } from 'iconsax-react'
|
||||
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
isMultiple?: boolean,
|
||||
onChange?: (file: File[]) => void;
|
||||
isReset?: boolean;
|
||||
}
|
||||
|
||||
const UploadBox: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
setFiles(array)
|
||||
if (props.onChange) {
|
||||
props.onChange(array);
|
||||
}
|
||||
} else {
|
||||
setFiles([acceptedFiles[0]])
|
||||
if (props.onChange) {
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files])
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
const array = [...files]
|
||||
array.splice(index, 1)
|
||||
setFiles(array)
|
||||
if (props.onChange) {
|
||||
props.onChange(array)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (props.isReset) {
|
||||
setFiles([])
|
||||
}
|
||||
|
||||
}, [props.isReset])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='text-sm'>{props.label}</div>
|
||||
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
||||
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
||||
<input {...getInputProps()} />
|
||||
|
||||
{t('select_file')}
|
||||
</button>
|
||||
|
||||
<div className='lg:flex gap-2 hidden'>
|
||||
{
|
||||
files?.map((item, index) => (
|
||||
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
|
||||
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
|
||||
<div className='text-xs'>{item.name}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='lg:hidden gap-2 flex flex-wrap mt-4'>
|
||||
{
|
||||
files?.map((item, index) => (
|
||||
<div key={index} className='flex bg-gray-200 py-1.5 px-2 rounded-lg items-center gap-1'>
|
||||
<CloseCircle onClick={() => handleRemove(index)} size={16} color='red' />
|
||||
<div className='text-xs'>{item.name}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UploadBox
|
||||
@@ -0,0 +1,154 @@
|
||||
import { CloseCircle, DocumentUpload, Gallery } from 'iconsax-react'
|
||||
import { type FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { clx } from '../helpers/utils';
|
||||
|
||||
type Props = {
|
||||
label: string;
|
||||
onChange: (file: File[]) => void;
|
||||
isMultiple?: boolean;
|
||||
isFile?: boolean,
|
||||
preview?: string[],
|
||||
onChangePreview?: (preview: string[]) => void,
|
||||
getCover?: (url: string) => void,
|
||||
coverUrl?: string
|
||||
}
|
||||
|
||||
const UploadBoxDraggble: FC<Props> = (props: Props) => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [perviews, setPerviews] = useState<string[]>(props.preview ? props.preview : [])
|
||||
const [isFill, setIsFill] = useState<boolean>(false)
|
||||
const [cover, setCover] = useState<string>(props.coverUrl ? props.coverUrl : '')
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
if (props.isMultiple) {
|
||||
const array = [...files]
|
||||
array.push(acceptedFiles[0])
|
||||
setFiles(array)
|
||||
props.onChange(array)
|
||||
} else {
|
||||
setFiles([acceptedFiles[0]])
|
||||
props.onChange([acceptedFiles[0]])
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [files])
|
||||
const { getRootProps, getInputProps } = useDropzone({ onDrop })
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
const array = [...files]
|
||||
array.splice(index, 1)
|
||||
setFiles(array)
|
||||
props.onChange(array)
|
||||
}
|
||||
|
||||
const handleDeletePreview = (index: number) => {
|
||||
const array = [...perviews];
|
||||
array.splice(index, 1);
|
||||
setPerviews(array);
|
||||
if (props.onChangePreview) {
|
||||
props.onChangePreview(array);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (props.preview && props.preview.length > 0 && !isFill) {
|
||||
setPerviews(props.preview)
|
||||
console.log('preview', props.preview);
|
||||
|
||||
setIsFill(true)
|
||||
}
|
||||
|
||||
}, [props.preview, isFill])
|
||||
|
||||
useEffect(() => {
|
||||
setCover(props.coverUrl ? props.coverUrl : '')
|
||||
}, [props.coverUrl])
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div {...getRootProps()} className='w-full py-8 border border-dashed border-description flex flex-col justify-center items-center gap-4 rounded-2xl'>
|
||||
<input {...getInputProps()} />
|
||||
<Gallery
|
||||
className='size-8'
|
||||
color='#8C90A3'
|
||||
/>
|
||||
<div className='text-description text-xs'>
|
||||
{props.label}
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<DocumentUpload
|
||||
className='size-4'
|
||||
color='black'
|
||||
/>
|
||||
<div className='text-xs'>{t('upload')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
files.length > 0 || perviews ? (
|
||||
<div className='mt-4 flex gap-4 items-center'>
|
||||
{
|
||||
perviews && perviews.map((item, index) => {
|
||||
return (
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
<div key={index} className='flex relative items-center gap-2'>
|
||||
<img onClick={() => {
|
||||
if (props.getCover) {
|
||||
setCover(item)
|
||||
props.getCover(item)
|
||||
}
|
||||
}} src={item}
|
||||
className={clx(
|
||||
'size-10 rounded-full object-cover',
|
||||
cover === item ? 'border-2 border-red-400' : ''
|
||||
)} />
|
||||
<div onClick={() => handleDeletePreview(index)} className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
|
||||
<CloseCircle className='size-4 ' color='red' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
{
|
||||
files.map((file, index) => {
|
||||
if (props.isFile) {
|
||||
return (
|
||||
<div key={index} className='flex border p-2 rounded-lg items-center gap-2'>
|
||||
<div className='flex relative items-center gap-2'>
|
||||
<div className='text-xs'>{file.name}</div>
|
||||
<div className='absolute -left-4 -top-4 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
|
||||
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else
|
||||
return (
|
||||
<div key={index} className='flex items-center gap-2'>
|
||||
<div key={index} className='flex relative items-center gap-2'>
|
||||
<img src={URL.createObjectURL(file)} alt={file.name} className='size-10 rounded-full object-cover' />
|
||||
<div className='absolute -left-2 -top-2 shadow-md bg-white size-5 rounded-full flex justify-center items-center'>
|
||||
<CloseCircle onClick={() => handleDelete(index)} className='size-4 ' color='red' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UploadBoxDraggble
|
||||
@@ -0,0 +1 @@
|
||||
export const SIDEBAR_SIZE_LEFT = 300;
|
||||
@@ -0,0 +1,119 @@
|
||||
export const Pages = {
|
||||
auth: {
|
||||
login: "/auth/login",
|
||||
register: "/auth/register",
|
||||
forgotPassword: "/auth/forgot",
|
||||
},
|
||||
dashboard: "/dashboard",
|
||||
services: {
|
||||
mine: "/services",
|
||||
other: "/other-service",
|
||||
detail: "/services/detail/",
|
||||
update: "/services/update/",
|
||||
add: "/services/add",
|
||||
list: "/services/list",
|
||||
category: "/services/category",
|
||||
plan: "/services/plan/",
|
||||
review: "/services/reviews",
|
||||
guide: "/services/guide/",
|
||||
},
|
||||
sliders: {
|
||||
list: "/sliders/list",
|
||||
create: "/sliders/create",
|
||||
detail: "/sliders/detail/",
|
||||
},
|
||||
transactions: "/transactions",
|
||||
receipts: {
|
||||
index: "/receipts/list",
|
||||
detail: "/receipts/detail/",
|
||||
create: "/receipts/create",
|
||||
},
|
||||
ticket: {
|
||||
list: "/tickets/list",
|
||||
create: "/tickets/create",
|
||||
detail: "/tickets/messages/",
|
||||
category: "/tickets/category",
|
||||
},
|
||||
announcement: {
|
||||
list: "/announcement",
|
||||
detail: "/announcement/detail/",
|
||||
create: "/announcement/create",
|
||||
},
|
||||
criticisms: {
|
||||
list: "/criticisms/list",
|
||||
detail: "/ccriticisms/detail/",
|
||||
},
|
||||
feedback: {
|
||||
list: "/feedback/list",
|
||||
},
|
||||
learning: {
|
||||
list: "/learning/list",
|
||||
detail: "/learning/detail/",
|
||||
create: "/learning/create",
|
||||
category: "/learning/category",
|
||||
},
|
||||
setting: "/setting",
|
||||
wallet: "/wallet",
|
||||
profile: "/profile",
|
||||
customers: {
|
||||
list: "/customers/list",
|
||||
},
|
||||
representative: {
|
||||
list: "/representatives/list",
|
||||
},
|
||||
developers: {
|
||||
list: "/developers/list",
|
||||
},
|
||||
discount: {
|
||||
list: "/discounts/list",
|
||||
create: "/discounts/create",
|
||||
detail: "/discounts/detail/",
|
||||
},
|
||||
referralCode: {
|
||||
list: "/referral-code/list",
|
||||
},
|
||||
users: {
|
||||
list: "/users/list",
|
||||
create: "/users/create",
|
||||
detail: "/users/detail/",
|
||||
roleList: "/users/roles-all",
|
||||
roleCreate: "/users/roles-add",
|
||||
groupAdd: "/users/group-add",
|
||||
groupList: "/users/groups",
|
||||
},
|
||||
ads: {
|
||||
list: "/ads/list",
|
||||
create: "/ads/create",
|
||||
update: "/ads/update/",
|
||||
},
|
||||
blog: {
|
||||
list: "/blog/list",
|
||||
create: "/blog/create",
|
||||
category: "/blog/category",
|
||||
detail: "/blog/detail/",
|
||||
comments: "/blog/comments",
|
||||
},
|
||||
messages: {
|
||||
list: "/messages/list",
|
||||
detail: "/messages/detail/",
|
||||
},
|
||||
customer: {
|
||||
list: "/customers/list",
|
||||
create: "/customers/create",
|
||||
update: "/customers/update/",
|
||||
},
|
||||
cardBank: {
|
||||
list: "/card-bank/list",
|
||||
create: "/card-bank/create",
|
||||
detail: "/card-bank/detail/",
|
||||
},
|
||||
payment: {
|
||||
list: "/payments/list",
|
||||
},
|
||||
support: {
|
||||
create: "/support/create",
|
||||
list: "/support/list",
|
||||
detail: "/support/detail/",
|
||||
planUsers: "/support/plan-users/",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export const SideBarItemHasSubMenu = [
|
||||
"services",
|
||||
"customers",
|
||||
"receipts",
|
||||
"users",
|
||||
"tickets",
|
||||
"learning",
|
||||
"blog",
|
||||
"support",
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
import axios from "axios";
|
||||
import { getToken } from "./func";
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_BASE_URL,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axiosInstance.interceptors.request.use(async function (config) {
|
||||
const token = getToken();
|
||||
|
||||
config.headers.Authorization = "Bearer " + token;
|
||||
return config;
|
||||
});
|
||||
|
||||
export default axiosInstance;
|
||||
@@ -0,0 +1,54 @@
|
||||
export function isEmail(input: string): boolean {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return emailRegex.test(input);
|
||||
}
|
||||
|
||||
export function NumberFormat(number: number): string {
|
||||
return Intl.NumberFormat("fa-IR").format(number);
|
||||
}
|
||||
|
||||
export const getToken = () => {
|
||||
return localStorage.getItem(import.meta.env.VITE_TOKEN_NAME);
|
||||
};
|
||||
|
||||
export const getRefreshToken = () => {
|
||||
return localStorage.getItem(import.meta.env.VITE_REFRESH_TOKEN_NAME);
|
||||
};
|
||||
|
||||
export const setToken = (token: string) => {
|
||||
localStorage.setItem(import.meta.env.VITE_TOKEN_NAME, token);
|
||||
};
|
||||
|
||||
export const setRefreshToken = (refreshToken: string) => {
|
||||
localStorage.setItem(import.meta.env.VITE_REFRESH_TOKEN_NAME, refreshToken);
|
||||
};
|
||||
|
||||
export const removeToken = () => {
|
||||
localStorage.removeItem(import.meta.env.VITE_TOKEN_NAME);
|
||||
};
|
||||
|
||||
export const removeRefreshToken = () => {
|
||||
localStorage.removeItem(import.meta.env.VITE_REFRESH_TOKEN_NAME);
|
||||
};
|
||||
|
||||
export const timeAgo = (date: string | Date): string => {
|
||||
const now = new Date();
|
||||
const past = new Date(date);
|
||||
const diff = Math.floor((now.getTime() - past.getTime()) / 1000); // اختلاف بر حسب ثانیه
|
||||
|
||||
const units = [
|
||||
{ name: "سال", value: 60 * 60 * 24 * 365 },
|
||||
{ name: "ماه", value: 60 * 60 * 24 * 30 },
|
||||
{ name: "روز", value: 60 * 60 * 24 },
|
||||
{ name: "ساعت", value: 60 * 60 },
|
||||
{ name: "دقیقه", value: 60 },
|
||||
{ name: "ثانیه", value: 1 },
|
||||
];
|
||||
|
||||
for (const unit of units) {
|
||||
const amount = Math.floor(diff / unit.value);
|
||||
if (amount >= 1) return `${amount} ${unit.name} پیش`;
|
||||
}
|
||||
|
||||
return "همین الان";
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
export const formatDate = (dateString: string): string => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("fa-IR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(date);
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatDateShort = (dateString: string): string => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return new Intl.DateTimeFormat("fa-IR", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(date);
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
|
||||
export type XOR<T, U> = T | U extends object
|
||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||
: T | U;
|
||||
|
||||
export type ItemsSelectType = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type ErrorType = Error & {
|
||||
response?: {
|
||||
data?: {
|
||||
error: {
|
||||
message: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
/**
|
||||
* Concatenates truthy classes into a space-separated string.
|
||||
*
|
||||
* @param classes - The classes to concatenate.
|
||||
* @returns The concatenated classes.
|
||||
*/
|
||||
export const clx = (...classes: (string | boolean | undefined)[]): string => {
|
||||
return twMerge(classes.filter(Boolean).join(" "));
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface ICountDown {
|
||||
m: number;
|
||||
s: number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param minutes how many minutes.
|
||||
* @param autoplay by default true. if you want play coundDown after an action, you can set it to false and manually play it.
|
||||
* @param onEnd you can pass a callback function that will trigger at the END.
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export function useCountDown(minutes: number, autoplay = true, onEnd = () => {}) {
|
||||
|
||||
const [countDown, setCountDown] = useState<ICountDown>({m: minutes, s: 0});
|
||||
const [stop, setStop] = useState<boolean>(!autoplay);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
if (countDown.m !== 0 || countDown.s !== 0) {
|
||||
setCountDown(calculateCountDown(countDown));
|
||||
} else {
|
||||
onEnd();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
}, [countDown]);
|
||||
|
||||
const reset = () => {
|
||||
setCountDown({m: minutes, s: 0});
|
||||
};
|
||||
const pause = () => {
|
||||
setStop(true);
|
||||
};
|
||||
const play = () => {
|
||||
setStop(false);
|
||||
setCountDown({...countDown});
|
||||
};
|
||||
|
||||
const calculateCountDown = (st: ICountDown): ICountDown => {
|
||||
if (stop) {
|
||||
return countDown;
|
||||
}
|
||||
const timeLeft = { m: 0, s: 0 };
|
||||
const remain = st.m * 60 + st.s - 1;
|
||||
if (remain > 0) {
|
||||
timeLeft.m = Math.floor((remain) / 60);
|
||||
timeLeft.s = remain - timeLeft.m * 60;
|
||||
return timeLeft;
|
||||
} else {
|
||||
if (remain === 0 && countDown.s === 1) {
|
||||
return timeLeft;
|
||||
} else {
|
||||
return countDown;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
value: `${countDown.m > 9 ? countDown.m : '0' + countDown.m}:${countDown.s > 9 ? countDown.s : '0' + countDown.s}`,
|
||||
reset,
|
||||
pause,
|
||||
play
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
export const useOutsideClick = (callback: () => void) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mouseup', handleClickOutside);
|
||||
document.addEventListener('touchend', handleClickOutside);
|
||||
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', handleClickOutside);
|
||||
document.removeEventListener('touchend', handleClickOutside);
|
||||
};
|
||||
}, [callback]);
|
||||
|
||||
return ref;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
html {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: irancell;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
direction: rtl;
|
||||
overflow-x: hidden !important;
|
||||
background: #eceef6;
|
||||
font-weight: 300;
|
||||
overflow: hidden;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100% !important;
|
||||
height: 100%;
|
||||
}
|
||||
* {
|
||||
outline: none;
|
||||
}
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-primary: black;
|
||||
--color-secondary: #f4f5f9;
|
||||
--color-border: #d0d0d0;
|
||||
--color-description: #888888;
|
||||
--color-header: #c3c7dd;
|
||||
--color-success: #00ba4b;
|
||||
--width-sidebar: 300px;
|
||||
--border-radius-2.5: 10px;
|
||||
}
|
||||
@@ -0,0 +1,832 @@
|
||||
{
|
||||
"auth": {
|
||||
"welcome": "خوش آمدید",
|
||||
"enter_information": "برای ثبت نام اطلاعات خود را وارد کنید.",
|
||||
"user_info": "اطلاعات کاربر",
|
||||
"name": "نام",
|
||||
"enter_name": "نام خود را وارد کنید",
|
||||
"family": "نام خانوادگی",
|
||||
"enter_family": "نام خانوادگی خود را وارد کنید",
|
||||
"select_date": "انتخاب تاریخ",
|
||||
"dateofbirth": "تاریخ تولد",
|
||||
"national_code": "کد ملی",
|
||||
"national_code_enter": "کد ملی خود را وارد کنید",
|
||||
"email": "ایمیل",
|
||||
"enter_email": "ایمیل خود را وارد کنید",
|
||||
"phonen_number": "شماره تماس",
|
||||
"enter_phone_number": "شماره تماس خود را وارد کنید",
|
||||
"password": "رمز عبور",
|
||||
"enter_password": "رمز عبور انتخابی خود را وارد کنید",
|
||||
"next": "ادامه",
|
||||
"before_registered": "آیا قبلا ثبت نام کردید؟",
|
||||
"login": "ورود",
|
||||
"enter_info_login": "لطفا اطلاعات خود را وارد کنید.",
|
||||
"mobile_or_email": " شماره موبایل یا ایمیل",
|
||||
"enter_mobile_or_email": " شماره موبایل یا ایمیل خود را وارد کنید",
|
||||
"have_account": "آیا حساب کاربری ندارید؟",
|
||||
"register": "ثبت نام",
|
||||
"enter_otp": "کد تایید را وارد کنید",
|
||||
"verfify_code_text_1": "کد تایید برای ",
|
||||
"verfify_code_text_2": "ارسال شد.",
|
||||
"change_number": "تغییر شماره",
|
||||
"verify_code": "کد تایید ",
|
||||
"counter_otp": "مانده تا دریافت مجدد کد",
|
||||
"login_with_password": "ورود با رمز عبور",
|
||||
"enter_password_step3": "رمز عبور را وارد کنید",
|
||||
"enter_your_password": "رمز عبور خود را وارد کنید.",
|
||||
"forgot_password": "فراموشی رمز عبور",
|
||||
"login_with_otp": "ورود با رمز عبور یکبار مصرف"
|
||||
},
|
||||
"errors": {
|
||||
"required": "این فیلد اجباری می باشد",
|
||||
"upload_image": "تصاویر مربوطه باید انتخاب شوند"
|
||||
},
|
||||
"description": "توضیحات",
|
||||
"sidebar": {
|
||||
"menu": "منو",
|
||||
"services": "سرویس ها",
|
||||
"customers": "مشتریان",
|
||||
"representatives": "نمایندگان",
|
||||
"developers": "توسعه دهندگان",
|
||||
"discounts": "تخفیف ها",
|
||||
"code_referral": "کد ارجاع",
|
||||
"users": "کاربران",
|
||||
"mnage_content": "مدیریت محتوا",
|
||||
"ads": "تبلیغات",
|
||||
"blog": "بلاگ",
|
||||
"messages": "پیام ها",
|
||||
|
||||
"mainpage": "صفحه اصلی",
|
||||
"myservice": "سرویس های من",
|
||||
"other_service": "سایر سرویس ها",
|
||||
"receipt_list": "لیست صورتحساب",
|
||||
"transactions": "تراکنش ها",
|
||||
"other": "سایر",
|
||||
"ticket": "تیکت ها",
|
||||
"announcement": "اطلاعیه ها",
|
||||
"criticisms": "انتقادات",
|
||||
"learning": "آموزش",
|
||||
"setting": "تنظیمات",
|
||||
"logout": "خروج",
|
||||
"card": "حساب های بانکی",
|
||||
"payments": "پرداخت ها",
|
||||
"sliders": "اسلایدر",
|
||||
"support": "پلن پشتیبانی",
|
||||
"feedback": "گزارش ها"
|
||||
},
|
||||
"slider": {
|
||||
"new_slider": "اسلایدر جدید",
|
||||
"submit": "ثبت اسلایدر",
|
||||
"title": "عنوان",
|
||||
"link": "لینک",
|
||||
"description": "توضیحات",
|
||||
"image": "تصویر",
|
||||
"order": "ترتیب",
|
||||
"isActive": "فعال / غیرفعال",
|
||||
"image_required": "تصویر الزامی می باشد",
|
||||
"list_slider": "لیست اسلایدر ها",
|
||||
"submit_slider": "اسلایدر جدید",
|
||||
"active": "فعال",
|
||||
"inactive": "غیرفعال",
|
||||
"slider_deleted": "اسلایدر با موفقیت حذف شد"
|
||||
},
|
||||
"submenu": {
|
||||
"services_list": "لیست سرویس ها",
|
||||
"add_service": "افزودن سرویس",
|
||||
"service_category": "دسته بندی سرویس ها",
|
||||
"customer_list": "لیست مشتریان",
|
||||
"create_customer": "افزودن مشتری",
|
||||
"ticket_list": "لیست تیکت ها",
|
||||
"send_ticket": "ارسال تیکت",
|
||||
"category": "دسته بندی",
|
||||
"user_list": "لیست کاربران",
|
||||
"add_user": "افزودن کاربر",
|
||||
"role_list": "لیست نقش ها",
|
||||
"role_create": "افزودن نقش",
|
||||
"reviews": "نظرات",
|
||||
"create_plan": "ایجاد پلن",
|
||||
"plan_users": "کاربران پلن",
|
||||
"plan_list": "لیست پلن ها",
|
||||
"guide": "راهنمای سرویس"
|
||||
},
|
||||
"guide": {
|
||||
"list_guide": "لیست راهنمای سرویس",
|
||||
"submit_guide": "افزودن راهنمای سرویس",
|
||||
"order": "ترتیب",
|
||||
"content": "محتوا"
|
||||
},
|
||||
"review": {
|
||||
"title": "عنوان",
|
||||
"comment": "نظر",
|
||||
"user": "کاربر",
|
||||
"service": "سرویس",
|
||||
"date": "تاریخ",
|
||||
"status": "وضعیت",
|
||||
"approved": "تایید شده",
|
||||
"rejected": "رد شده",
|
||||
"approve": "تایید",
|
||||
"reject": "رد"
|
||||
},
|
||||
"header": {
|
||||
"search": "جستجو"
|
||||
},
|
||||
"home": {
|
||||
"new": "جدید",
|
||||
"study": "مطالعه",
|
||||
"myservice": "سرویس های من",
|
||||
"active_service": "سرویس های فعال",
|
||||
"ticket": "تیکت ها",
|
||||
"unread_messages": "پیام خوانده نشده دارید ",
|
||||
"announcement": "اطلاعیه ها",
|
||||
"unread_announcement": "اطلاعیه خوانده نشده دارید",
|
||||
"learnings": "آموزش ها",
|
||||
"learning": "آموزش",
|
||||
"access": "دسترسی سریع",
|
||||
"add_access": "دسترسی سریع تازه ایجاد کنید.",
|
||||
"danak_learning": "آموزش داناک",
|
||||
"see_all": "دیدن همه",
|
||||
"create_new_access": "ایجاد دسترسی سریع",
|
||||
"services": "سرویس ها",
|
||||
"service": "سرویس",
|
||||
"customers": "مشتریان",
|
||||
"customer": "مشتری",
|
||||
"invoices": "صورت حساب ها",
|
||||
"new_invoice": "صورت حساب جدید",
|
||||
"ads": "تبلیغات",
|
||||
"active_ads": "تبلیغ فعال"
|
||||
},
|
||||
"all": "همه",
|
||||
"service": {
|
||||
"my_service": "سرویس های من",
|
||||
"search": "جستجو در سرویس ها",
|
||||
"active_menu": "منو فعال",
|
||||
"selected_danak": "انتخاب داناک",
|
||||
"buy": "خرید",
|
||||
"other_services": "سایر سرویس ها",
|
||||
"year": "سالانه",
|
||||
"month3": "۳ ماهه",
|
||||
"month": "ماهیانه",
|
||||
"company_develop": "شرکت توسعه دهنده",
|
||||
"company_danak": "شرکت داناک",
|
||||
"user_count": "تعداد کاربران",
|
||||
"year_make": "سال ساخت",
|
||||
"lang_app": "زبان نرم افزار",
|
||||
"persian": "فارسی",
|
||||
"score": "امتیاز",
|
||||
"compelete_description": "توضیحات تکمیلی",
|
||||
"submit_comment": "ثبت نظر",
|
||||
"hint_comment": "شما پس از خرید این سرویس میتوانید نظر خود را به ثبت برسانید.",
|
||||
"fullName": "نام و نام خانوادگی",
|
||||
"enter_your_name": "نام خود را واردکنید",
|
||||
"title": "عنوان",
|
||||
"enter_title_comment": "عنوان نظر خود را وارد کنید",
|
||||
"your_comment": "نظر شما",
|
||||
"write_your_comment": "نظر خود را راجع به این سرویس بنویسید",
|
||||
"users_comments": "نظرات کاربران",
|
||||
"add_service": "افزودن سرویس",
|
||||
"service_name": "نام سرویس",
|
||||
"description_short": "توضیح کوتاه سرویس",
|
||||
"company_developed": "شرکت توسعه دهنده",
|
||||
"service_status": "وضعیت سرویس",
|
||||
"special_danak": "این سرویس پیشنهاد ویژه داناک هست",
|
||||
"service_link": "لینک دریافت سرویس",
|
||||
"category": "دسته بندی",
|
||||
"service_icon": "آیکون سرویس",
|
||||
"upload_icon_service": "آیکون سرویس مورد نظر را آپلود کنید",
|
||||
"service_images": "تصاویر سرویس",
|
||||
"upload_images_service": "تصاویر سرویس مورد نظر را آپلود کنید",
|
||||
"submit_service": "ثبت سرویس",
|
||||
"new_service": "سرویس جدید",
|
||||
"list_service": "لیست سرویس ها",
|
||||
"status": "وضعیت",
|
||||
"publish_date": "تاریخ انتشار",
|
||||
"cateory_services": "دسته بندی سرویس ها",
|
||||
"parent_category": "دسته مادر",
|
||||
"count_sub_category": "تعداد زیر دسته",
|
||||
"count_service": "تعداد سرویس",
|
||||
"add_category": "اضافه کردن دسته بندی",
|
||||
"status_category": "وضعیت دسته بندی",
|
||||
"title_category": "عنوان دسته بندی",
|
||||
"category_parent_2": "دسته بندی مادر",
|
||||
"icon_serice_category": "آیکون سرویس مورد نظر را آپلود کنید",
|
||||
"active": "فعال",
|
||||
"deactive": "غیرفعال",
|
||||
"software_langauge": "زبان نرم افزار",
|
||||
"link": "لینک",
|
||||
"danak_sujescet": "پیشنهاد داناک",
|
||||
"plans": "پلن ها",
|
||||
"add_plan": "افزودن پلن",
|
||||
"active_plan": "پلن فعال",
|
||||
"slider": "نمایش در اسلایدر",
|
||||
"slug": "اسلاگ",
|
||||
"page_title": "عنوان صفحه",
|
||||
"page_description": "توضیحات صفحه"
|
||||
},
|
||||
"plan": {
|
||||
"plans_list": "لیست پلن های ",
|
||||
"new_plan": "پلن جدید",
|
||||
"add_plan": "افزودن پلن",
|
||||
"time": "مدت زمان",
|
||||
"price": "قیمت",
|
||||
"original_price": "قیمت اصلی",
|
||||
"cancel": "لغو",
|
||||
"submit_plan": "ثبت پلن",
|
||||
"error_submit": "حداقل یک پلن باید اضافه شود",
|
||||
"success_submit": "پلن با موفقیت ثبت شد",
|
||||
"edit_plan": "ویرایش پلن"
|
||||
},
|
||||
"save": "ذخیره",
|
||||
"search": "جستجو",
|
||||
"upload": "آپلود",
|
||||
"select": "انتخاب",
|
||||
"footer": {
|
||||
"home": "صفحه اصلی",
|
||||
"other_services": "سایر سرویس ها",
|
||||
"my_services": "سرویس های من",
|
||||
"announcements": "اطلاعیه ها",
|
||||
"tickets": "تیکت ها",
|
||||
"customers": "مشتریان",
|
||||
"receipts": "صورت حساب ها"
|
||||
},
|
||||
"ticket": {
|
||||
"tickets": "تیکت ها",
|
||||
"new_ticket": "تیکت جدید",
|
||||
"answered": "پاسخ داده شده",
|
||||
"checking": "در حال بررسی",
|
||||
"wait_answered": "در انتظار پاسخ",
|
||||
"closed": "بسته شده",
|
||||
"number": "شماره",
|
||||
"title": "عنوان",
|
||||
"team": "تیم",
|
||||
"date": "تاریخ",
|
||||
"status": "وضعیت",
|
||||
"priority": "الویت",
|
||||
"detail": "جزییات",
|
||||
"submit_your_message": "ثبت پیام شما",
|
||||
"name": "نام",
|
||||
"email": "ایمیل",
|
||||
"subject": "موضوع",
|
||||
"enter_your_subject": "موضوع درخواست خودرا وارد کنید",
|
||||
"select_your_service": "سرویس مربوطه",
|
||||
"not_once": "هیچکدام",
|
||||
"your_description": "توضیحات شما",
|
||||
"attachment": "فایل های ضمیمه",
|
||||
"send": "ارسال",
|
||||
"title_hint": "جهت پاسخگویی بهتر به مسائل زیر توجه بفرمایید :",
|
||||
"ticket_number": "تیکت شماره",
|
||||
"user_name": "نام کاربر",
|
||||
"expert": "کارشناس: ",
|
||||
"answer": "پاسخ",
|
||||
"send_answer": "ارسال پاسخ",
|
||||
"close_ticket": "بستن تیکت",
|
||||
"info_ticket": "اطلاعات تیکت",
|
||||
"update_sms": "به روزرسانی این تیکت از طریق پیامک اطلاع داده میشود",
|
||||
"category_ticket": "دسته بندی تیکت",
|
||||
"users_count": "تعداد کاربر",
|
||||
"add_category": "اضافه کردن دسته بندی",
|
||||
"status_category": "وضعیت دسته بندی",
|
||||
"title_category": "عنوان دسته بندی",
|
||||
"users": "کاربران",
|
||||
"MEDIUM": "متوسط",
|
||||
"LOW": "پایین",
|
||||
"HIGH": "بالا",
|
||||
"PENDING": "در انتظار",
|
||||
"ANSWERED": "پاسخ داده شده",
|
||||
"CLOSED": "بسته شده",
|
||||
"asignto": "کارشناس پاسخگویی",
|
||||
"category": "دسته بندی",
|
||||
"low": "پایین",
|
||||
"medium": "متوسط",
|
||||
"high": "بالا",
|
||||
"service": "سرویس",
|
||||
"refer_ticket": "ارجاع تیکت",
|
||||
"user": "کاربر",
|
||||
"assign_to": "کارشناس پاسخگویی",
|
||||
"user_plan": "پلن کاربر",
|
||||
"refer_ticket_success": "تیکت با موفقیت ارجاع شد",
|
||||
"refer_ticket_error": "تیکت ارجاع نشد"
|
||||
},
|
||||
"active": "فعال",
|
||||
"inactive": "غیرفعال",
|
||||
"attach": "پیوست",
|
||||
"date": "تاریخ",
|
||||
"status": "وضعیت",
|
||||
"title": "عنوان",
|
||||
"ads": {
|
||||
"ads": "تبلیغات",
|
||||
"upload": "آپلود",
|
||||
"deactive": "غیرفعال",
|
||||
"submit": "ثبت تبلیغ",
|
||||
"upload_picture": "تصویر نمایش تبلیغ",
|
||||
"to_upload_message": "تصویر مورد نظر را آپلود کنید",
|
||||
"new_ads": "افزودن تبلیغ",
|
||||
"location": "جایگاه",
|
||||
"startDate": "تاریخ شروع نمایش",
|
||||
"endDate": "تاریخ پایان نمایش",
|
||||
"search": "جستجو",
|
||||
"number": "شماره",
|
||||
"title": "عنوان",
|
||||
"date": "تاریخ",
|
||||
"status": "وضعیت",
|
||||
"enter_your_title": "عنوان تبلیغ را وارد کنید",
|
||||
"link": "لینک",
|
||||
"enter_your_link": "لینک تبلیغ را وارد کنید",
|
||||
"HOMEPAGE_TOP": "بالای صفحه اصلی",
|
||||
"SINGLE_SERVICE_PAGE": "صفحه سرویس تکی",
|
||||
"MY_SERVICES_PAGE": "صفحه سرویس های من",
|
||||
"OTHER_SERVICES_TOP": "بالای سایر سرویس ها",
|
||||
"OTHER_SERVICES_LEFT": "سمت چپ سایر سرویس ها",
|
||||
"SERVICE": "سرویس",
|
||||
"file_error": "فایل مورد نظر باید انتخاب شود",
|
||||
"BLOG_PAGE_TOP_RIGHT": "بالای صفحه بلاگ راست",
|
||||
"BLOG_PAGE_BOTTOM_RIGHT": "پایین صفحه بلاگ راست",
|
||||
"BLOG_PAGE_TOP_LEFT": "بالای صفحه بلاگ چپ"
|
||||
},
|
||||
"discount": {
|
||||
"list": "لیست تخفیفات",
|
||||
"type": "نوع تخفیف",
|
||||
"price": "مبلغ تخفیف",
|
||||
"title": "عنوان کد",
|
||||
"code": "کد تخفیف",
|
||||
"CODE": "کد تخفیف",
|
||||
"enter_discount_description": "عنوان کد تخفیف را وارد کنید",
|
||||
"add_new_discount": "افزودن تخفیف",
|
||||
"auto_generate_code": "ساخت کد",
|
||||
"price_or_percent": "درصد / مبلغ تخفیف",
|
||||
"enter_price_or_percent": "درصد / مبلغ تخفیف را وارد کنید",
|
||||
"service": "سرویس",
|
||||
"status": "وضعیت",
|
||||
"number": "شماره",
|
||||
"startDate": "تاریخ شروع",
|
||||
"endDate": "تاریخ پایان",
|
||||
"service_status": "وضعیت سرویس",
|
||||
"availabe_service": "سرویس هایی که شامل تخفیف میشوند",
|
||||
"new_discount": "کد تخفیف جدید",
|
||||
"submit": "ثبت تخفیف",
|
||||
"discount_code": "کد تخفیف",
|
||||
"direct_discount": "تخفیف مستقیم",
|
||||
"fixed_amount": "مبلغ ثابت",
|
||||
"percent": "درصد",
|
||||
"errors_plan": "سرویس و پلن را انتخاب کنید",
|
||||
"DIRECT": "مستقیم",
|
||||
"FIXED": "ثابت",
|
||||
"plan": "پلن",
|
||||
"user": "کاربر",
|
||||
"0": "درصد",
|
||||
"1": "مبلغ",
|
||||
"update_discount": "ویرایش تخفیف",
|
||||
"customer": "مشتری"
|
||||
},
|
||||
"blog": {
|
||||
"blog": "بلاگ",
|
||||
"category": "دسته بندی",
|
||||
"new_blog": "افزودن بلاگ",
|
||||
"blog_title": "عنوان بلاگ",
|
||||
"blog_summary": "خلاصه بلاگ",
|
||||
"content": "محتوا",
|
||||
"blog_status": "وضعیت بلاگ",
|
||||
"featured_image": "تصویر شاخص",
|
||||
"tags": "برچسب ها",
|
||||
"tag_hint": "جدا کردن با کاما یا کلید Enter ",
|
||||
"shareDate": "تاریخ انتشار",
|
||||
"enter_your_title": "عنوان بلاگ را وارد کنید",
|
||||
"add_blog": "افزودن بلاگ",
|
||||
"blog_category": "دسته بندی بلاگ",
|
||||
"add_category": "اضافه کردن دسته بندی",
|
||||
"category_status": "وضعیت دسته بندی",
|
||||
"category_title": "عنوان دسته بندی",
|
||||
"parent_category": "دسته بندی مادر",
|
||||
"price": "مبلغ تخفیف",
|
||||
"title": "عنوان کد",
|
||||
"code": "کد تخفیف",
|
||||
"enter_discount_description": "عنوان کد تخفیف را وارد کنید",
|
||||
"add_new_discount": "افزودن تخفیف",
|
||||
"auto_generate_code": "ساخت کد",
|
||||
"price_or_percent": "درصد / مبلغ تخفیف",
|
||||
"enter_price_or_percent": "درصد / مبلغ تخفیف را وارد کنید",
|
||||
"service": "سرویس",
|
||||
"status": "وضعیت",
|
||||
"number": "شماره",
|
||||
"startDate": "تاریخ شروع",
|
||||
"endDate": "تاریخ پایان",
|
||||
"service_status": "وضعیت سرویس",
|
||||
"availabe_service": "سرویس هایی که شامل تخفیف میشوند",
|
||||
"new_discount": "کد تخفیف جدید",
|
||||
"submit": "ثبت بلاگ",
|
||||
"discount_code": "کد تخفیف",
|
||||
"direct_discount": "تخفیف مستقیم",
|
||||
"fixed_amount": "مبلغ ثابت",
|
||||
"percent": "درصد",
|
||||
"enter_your_summary": "خلاصه بلاگ را وارد کنید",
|
||||
"enter_your_content": "محتوای بلاگ را وارد کنید",
|
||||
"blog_content": "محتوای بلاگ",
|
||||
"enter_tags": "برچسب ها را وارد کنید",
|
||||
"enter_your_slug": "اسلاگ بلاگ را وارد کنید",
|
||||
"slug": "اسلاگ",
|
||||
"meta_title": "عنوان متا",
|
||||
"enter_your_meta_title": "عنوان متا بلاگ را وارد کنید",
|
||||
"meta_description": "توضیحات متا",
|
||||
"enter_your_meta_description": "توضیحات متا بلاگ را وارد کنید",
|
||||
"comments": "نظرات",
|
||||
"user": "کاربر",
|
||||
"comment": "نظر",
|
||||
"picture": "تصویر"
|
||||
},
|
||||
"receip": {
|
||||
"receips": "صورتحساب ها",
|
||||
"receip_list": "لیست صورتحساب ها",
|
||||
"add_receip": "افزودن صورتحساب",
|
||||
"dont_pay": "پرداخت نشده",
|
||||
"dont_confrim": "تایید نشده",
|
||||
"paid": "پرداخت شده",
|
||||
"canceled": "لغو شده",
|
||||
"returned": "پس داده شده",
|
||||
"its_time": "سررسیده شده",
|
||||
"archive": "بایگانی شده",
|
||||
"detail_account": "جزییات صورتحساب ",
|
||||
"personal_information": "اطلاعات شخصی",
|
||||
"type_person": "نوع شخص : ",
|
||||
"company": "حقوقی",
|
||||
"representativeـname": "نام نماینده :",
|
||||
"company_name": "نام شرکت/ سازمان :",
|
||||
"registration_number": "شماره ثبت :",
|
||||
"tel_company": "تلفن شرکت :",
|
||||
"national_code": "کد ملی نماینده :",
|
||||
"postal_code": "کد پستی :",
|
||||
"state": "استان :",
|
||||
"city": "شهر :",
|
||||
"address": "آدرس :",
|
||||
"detail_receip": "مشخصات صورتحساب",
|
||||
"not_paid": "پرداخت نشده",
|
||||
"description": "توضیحات",
|
||||
"price": "مبلغ",
|
||||
"receip_information": "اطلاعات صورتحساب",
|
||||
"date_factor": "تاریخ صدور فاکتور",
|
||||
"due_date": "تاریخ سررسید فاکتور",
|
||||
"factor_number": "شماره فاکتور",
|
||||
"status_factor": "وضعیت فاکتور",
|
||||
"submit_receip": "ثبت صورتحساب",
|
||||
"customer_information": "اطلاعات مشتری",
|
||||
"customer": "مشتری",
|
||||
"receipt_information": "اطلاعات صورتحساب",
|
||||
"number": "شماره",
|
||||
"product_name": "نام کالا",
|
||||
"count": "تعداد",
|
||||
"unit_amount": "مبلغ واحد",
|
||||
"discount": "تخفیف(٪)",
|
||||
"total_amount": "مبلغ کل(تومان)",
|
||||
"tax": "مالیات بر ارزش افزوده (۱۰ درصد)",
|
||||
"total": "جمع",
|
||||
"error_empty": "همه فیلد ها را پر کنید",
|
||||
"date_receipt": "تاریخ صورتحساب",
|
||||
"last_date_receipt": "آخرین مهلت پرداخت",
|
||||
"service": "سرویس",
|
||||
"status": "وضعیت تایید",
|
||||
"status_paid": "وضعیت پرداخت",
|
||||
"PENDING": "در انتظار",
|
||||
"PAID": "پرداخت شده",
|
||||
"CANCELED": "لغو شده",
|
||||
"EXPIRED": "منقضی شده",
|
||||
"approved": "تایید شده",
|
||||
"APPROVED": "تایید شده",
|
||||
"ARCHIVED": "بایگانی شده",
|
||||
"pending": "در انتظار",
|
||||
"since": "از تاریخ",
|
||||
"to": "تا تاریخ",
|
||||
"recurring": "دوره ای",
|
||||
"select_type": "انتخاب نوع دوره",
|
||||
"1": "هفتگی",
|
||||
"2": "ماهانه",
|
||||
"3": "سهماهه",
|
||||
"4": "ششماهه",
|
||||
"5": "سالانه",
|
||||
"maxRecurringCycles": "حداکثر تعداد تکرار دوره",
|
||||
"WAIT_PAYMENT": "در انتظار پرداخت",
|
||||
"overdue": "سررسید شده",
|
||||
"OVERDUE": "سررسید شده",
|
||||
"lateFee": "مبلغ جریمه",
|
||||
"recurringPeriod": "دوره تکرار",
|
||||
"WEEKLY": "هفتگی",
|
||||
"MONTHLY": "ماهانه",
|
||||
"QUARTERLY": "سهماهه",
|
||||
"SEMIANNUALLY": "ششماهه",
|
||||
"ANNUALLY": "سالانه"
|
||||
},
|
||||
"edit": "ویرایش",
|
||||
"transaction": {
|
||||
"transaction": "تراکنش ها",
|
||||
"totao_deposit": "مجموع واریز ها",
|
||||
"total_withdrawal": "مجموع برداشت ها",
|
||||
"balance": "موجودی کیف پول",
|
||||
"transaction2": "تراکنش",
|
||||
"type_deposit": "نوع واریز",
|
||||
"status": "وضعیت",
|
||||
"success_paid": "پرداخت موفق",
|
||||
"print": "پرینت",
|
||||
"download": "دانلود",
|
||||
"type_getway": "نوع درگاه",
|
||||
"doer": "انجام دهنده",
|
||||
"tracking_number": "شماره پیگیری",
|
||||
"description_transaction": "شرح تراکنش",
|
||||
"amount": "مبلغ",
|
||||
"date": "تاریخ تراکنش",
|
||||
"order_number": "شماره سفارش",
|
||||
"description": "توضیحات تراکنش",
|
||||
"user_search": "جستجو کاربر"
|
||||
},
|
||||
"announcement": {
|
||||
"announcements": "اطلاعیه ها",
|
||||
"search": "جستجو در اطلاعیه ها",
|
||||
"new": "جدید",
|
||||
"important": "مهم",
|
||||
"text_announcement": "متن اطلاعیه",
|
||||
"add_announcement": "افزودن اطلاعیه",
|
||||
"submit_announcement": "ثبت اطلاعیه",
|
||||
"title": "عنوان اطلاعیه",
|
||||
"text": "متن اطلاعیه",
|
||||
"publish_date": "زمان انتشار",
|
||||
"info_publishdata": "اگر مایلید اطلاعیه در زمان دیگری منتشر شود تاریخ انتشار مورد نظر را انتخاب کنید",
|
||||
"label_importance": "لیبل “مهم” در اطلاعیه درج شود",
|
||||
"select_service": "انتخاب سرویس",
|
||||
"select_customer": "انتخاب مشتری",
|
||||
"contact_announcement": "مخاطب این اطلاعیه",
|
||||
"date_created": "تاریخ ثبت",
|
||||
"summary": "خلاصه اطلاعیه",
|
||||
"user": "کاربر مخاطب",
|
||||
"created_date": "زمان ایجاد",
|
||||
"service": "سرویس"
|
||||
},
|
||||
"back": "بازگشت",
|
||||
"criticisms": {
|
||||
"criticisms": "انتقادات",
|
||||
"submit_your_criticisms": "ثبت انتقاد شما",
|
||||
"description_criticisms": "با انتقاد خود راهگشای ما باشید.",
|
||||
"subject": "موضوع",
|
||||
"enter_your_subject": "موضوع انتقاد خودرا وارد کنید",
|
||||
"description": "توضیحات",
|
||||
"enter_description": "توضیحات شما",
|
||||
"criticisms_list": "لیست انتقادات",
|
||||
"number": "شماره",
|
||||
"title_criticisms": "عنوان انتقاد",
|
||||
"summary_criticisms": "خلاصه انتقاد",
|
||||
"date_created": "تاریخ ثبت",
|
||||
"criticism": "انتقاد",
|
||||
"documents": "مستندات"
|
||||
},
|
||||
"detail": "جزییات",
|
||||
"select_file": "انتخاب فایل",
|
||||
"no_select_file": "هیچ فایلی انتخاب نشده است",
|
||||
"attachment": "فایل های ضمیمه",
|
||||
"learning": {
|
||||
"learning": "آموزش",
|
||||
"learnings": "آموزش ها",
|
||||
"new_learning": "افزودن آموزش",
|
||||
"category_learning": "دسته بندی آموزش",
|
||||
"submit_learning": "ثبت آموزش",
|
||||
"title": "عنوان آموزش",
|
||||
"category": "دسته بندی",
|
||||
"description": "توضیحات",
|
||||
"video_duration": "مدت زمان ویدیو",
|
||||
"learning_cover": "کاور آموزش",
|
||||
"upload_cover": "آپلود کاور",
|
||||
"video": "آپلود ویدیو",
|
||||
"upload_video": "آپلود ویدیو",
|
||||
"error_video_cover_required": "آپلود ویدیو و کاور اجباری است",
|
||||
"list_learning": "لیست آموزش ها"
|
||||
},
|
||||
"setting": {
|
||||
"setting": "تنظیمات",
|
||||
"notification": "پیام و ایمیل",
|
||||
"password": "رمز عبور",
|
||||
"setting_email_message": "تنظیمات ارسال پیام و ایمیل",
|
||||
"notification_announcement": "اعلانات و اطلاعیه ها",
|
||||
"email": "ایمیل",
|
||||
"sms": "پیامک",
|
||||
"change_password": "تغییر رمز عبور",
|
||||
"current_password": "رمز عبور فعلی",
|
||||
"new_password": "رمز عبور جدید",
|
||||
"reapeat_password": "تکرار رمز عبور",
|
||||
"save_change": "ذخیره تغییرات",
|
||||
"password_is": "رمز عبور میبایست:",
|
||||
"min-8": "حداقل 8 کاراکتر باشد",
|
||||
"mix_charachter_number": "ترکیبی از حروف کوچک و بزرگ باشد",
|
||||
"exsist_number": "شامل اعداد باشد",
|
||||
"exsist_special_character": "شامل کاراکتر های خاص (نماد ها) باشد"
|
||||
},
|
||||
"wallet": {
|
||||
"increese_wallet": "افزایش موجودی حساب اعتباری",
|
||||
"online_pay": "پرداخت آنلاین",
|
||||
"card_to_card": "کارت به کارت",
|
||||
"select_price": "انتخاب مبلغ",
|
||||
"optional_price": "مبلغ دلخواه (به تومان وارد کنید)",
|
||||
"enter_your_price": "مبلغ دلخواه خودرا وارد کنید",
|
||||
"select_payment": "انتخاب درگاه پرداخت",
|
||||
"pay": "پرداخت",
|
||||
"amount": "مبلغ",
|
||||
"enter_amount_card": "مبلغ کارت به کارت شده را وارد کنید",
|
||||
"upload_deposit_slip": "آپلود فیش واریزی",
|
||||
"submit_slip": "ثبت فیش",
|
||||
"sheba": "شبا",
|
||||
"pay_sheba": "پرداخت شبا",
|
||||
"enter_sheba_deposit": "مبلغ شبا شده شده را وارد کنید"
|
||||
},
|
||||
"notif": {
|
||||
"natification": "اعلان ها",
|
||||
"all_read": "همه خوانده شده"
|
||||
},
|
||||
"user": {
|
||||
"users_list": "لیست کاربران",
|
||||
"add_user": "افزودن کاربر",
|
||||
"name": "نام",
|
||||
"title_job": "عنوان شغلی",
|
||||
"email": "ایمیل",
|
||||
"role": "نقش",
|
||||
"submit_user": "ثبت کاربر",
|
||||
"info_user": "اطلاعات کاربر",
|
||||
"family": "نام خانوادگی",
|
||||
"phone_number": "شماره تماس",
|
||||
"password": "رمز عبور",
|
||||
"reapeat_password": "تکرار رمز عبور",
|
||||
"image": "تصویر حساب کاربری",
|
||||
"roles_list": "لیست نقش ها",
|
||||
"add_role": "افزودن نقش",
|
||||
"role_search": "جستجو در نقش ها",
|
||||
"title": "عنوان",
|
||||
"user": "کاربر",
|
||||
"submit_role": "ثبت نقش",
|
||||
"title_role": "عنوان نقش",
|
||||
"permissions": "دسترسی ها",
|
||||
"status_role": "وضعیت نقش",
|
||||
"error_count_permission": "حداقل به دسترسی رو باید انتخاب کنید",
|
||||
"is_admin": "آیا ادمین است؟",
|
||||
"user_group": "گروه کاربری",
|
||||
"add_user_group": "ساخت گروه کاربری",
|
||||
"users": "کاربران",
|
||||
"group_add": "ساخت گروه",
|
||||
"submit_group": "ثبت گروه",
|
||||
"error_count_users": "حداقل یک کاربر باید انتخاب شود",
|
||||
"list_user_group": "لیست گروه های کاربری",
|
||||
"groups": "گروه ها",
|
||||
"group_name": "نام گروه",
|
||||
"update_user": "ویرایش کاربر"
|
||||
},
|
||||
"submit": "ثبت",
|
||||
"yes": "بله",
|
||||
"no": "خیر",
|
||||
"profile": {
|
||||
"account_user": "حساب کاربری",
|
||||
"image_profile": "تصویر حساب کاربری",
|
||||
"image_profile_desc": "تصویر خود را در حساب کاربری قرار دهید.",
|
||||
"upload_image": "آپلود تصویر",
|
||||
"formats": "فرمت های",
|
||||
"max_size": "حداکثر ۱مگابایت",
|
||||
"format_image": " jpg, png,jpeg.",
|
||||
"info_account": "اطلاعات حساب",
|
||||
"auth_after_change_info": "در صورت تغییر اطلاعات حساب باید مجددا احراز هویت انجام شود.",
|
||||
"username": "نام کاربری",
|
||||
"phone_call": "شماره تماس",
|
||||
"personal_information": "اطلاعات شخصی",
|
||||
"enter_carefully_your_information": "اطلاعت شخصی خود را با دقت وارد کنید.",
|
||||
"date_of_birth": "تاریخ تولد",
|
||||
"national_code": "کد ملی",
|
||||
"address": "آدرس",
|
||||
"address_live": "آدرس محل سکونت خود را با دقت وارد کنید",
|
||||
"province": "استان",
|
||||
"city": "شهر",
|
||||
"postal_code": "کد پستی",
|
||||
"username_available": "نام کاربری موجود",
|
||||
"username_not_available": "نام کاربری موجود نیست",
|
||||
"username_save_success": "نام کاربری با موفقیت ذخیره شد",
|
||||
"image_uploaded_successfully": "تصویر با موفقیت آپلود شد",
|
||||
"confrim_email": "تایید ایمیل",
|
||||
"confrim": "تایید صورتحساب ",
|
||||
"link_email": "یک لینک برای تایید ایمیل برای شما ارسال شد.",
|
||||
"email_not_verified": "ایمیل تایید نشده"
|
||||
},
|
||||
"email": "ایمیل",
|
||||
"customer": {
|
||||
"customer_list": "لیست مشتریان",
|
||||
"add_cutomer": "افزودن مشتری",
|
||||
"name": "نام",
|
||||
"company": "سازمان / شرکت",
|
||||
"email": "ایمیل",
|
||||
"active_services": "سرویس های فعال",
|
||||
"factor": "صورت حساب ها",
|
||||
"tickets": "تیکت ها",
|
||||
"detail": "جزییات",
|
||||
"submit_customer": "ثبت مشتری",
|
||||
"family": "نام خانوادگی",
|
||||
"dateofbirth": "تاریخ تولد",
|
||||
"national_code": "کد ملی",
|
||||
"phone_number": "شماره تماس",
|
||||
"information_legal": "اطلاعات حقوقی",
|
||||
"economic_code": "کد اقتصادی",
|
||||
"id_national": "شناسه ملی",
|
||||
"enter_nation_code": "کد ملی خود را وارد کنید",
|
||||
"enter_email": "ایمیل خود را وارد کنید",
|
||||
"enter_phone_number": "شماره تماس خود را وارد کنید",
|
||||
"enter_economic_code": "کد اقتصادی را وارد کنید",
|
||||
"enter_id_nation": "شناسه ملی خود را وارد کنید",
|
||||
"registeration_id": "شناسه ثبت",
|
||||
"enter_registeration_id": "شناسه ثبت خود را وارد کنید",
|
||||
"your_phone_fixed": "شماره تماس ثابت",
|
||||
"enter_phone_fixed": "شماره تماس خود را وارد کنید",
|
||||
"state": "استان",
|
||||
"city": "شهر",
|
||||
"password": "رمز عبور",
|
||||
"reapeat_password": "تکرار رمز عبور",
|
||||
"copany_name": "نام شرکت",
|
||||
"enter_company_name": "نام ثبتی شرکت را وارد کنید",
|
||||
"postal_code": "کد پستی",
|
||||
"enter_postal_code": "کد پستی را وارد کنید",
|
||||
"address": "آدرس",
|
||||
"image_profile": "تصویر پروفایل",
|
||||
"upload_image": "آپلود تصویر",
|
||||
"new_password": "رمز عبور جدید"
|
||||
},
|
||||
"priority": "الویت",
|
||||
"messages": {
|
||||
"messages_list": "لیست پیام ها",
|
||||
"submit_date": "تاریخ ثبت",
|
||||
"number": "شماره",
|
||||
"fullName": "نام و نام خانوادگی",
|
||||
"email": "ایمیل",
|
||||
"subject": "موضوع",
|
||||
"date": "تاریخ",
|
||||
"message": "پیام"
|
||||
},
|
||||
"loading": "در حال بارگذاری...",
|
||||
"success": "عملیات با موفقیت انجام شد.",
|
||||
"confrim": {
|
||||
"subject": "حذف",
|
||||
"content": "برای حذف این آیتم مطمئن هستید؟",
|
||||
"yes": "بله",
|
||||
"cancel": "لغو"
|
||||
},
|
||||
"cardBank": {
|
||||
"new_account": "حساب جدید",
|
||||
"submit_account": "ثبت حساب",
|
||||
"account_information": "اطلاعات حساب",
|
||||
"status_account": "وضعیت حساب",
|
||||
"account_owner_name": "نام صاحب حساب",
|
||||
"bank_name": "نام بانک",
|
||||
"card_number": "شماره کارت",
|
||||
"sheba": "شماره شبا",
|
||||
"manage_banks": "مدیریت حساب های بانکی",
|
||||
"add_account": "افزودن حساب"
|
||||
},
|
||||
"payment": {
|
||||
"payments": "پرداخت ها",
|
||||
"status_payment": "وضعیت پرداخت",
|
||||
"user": "کاربر",
|
||||
"card_number": "شماره کارت",
|
||||
"sheba_number": "شماره شبا",
|
||||
"amount": "مبلغ",
|
||||
"date": "تاریخ",
|
||||
"payment_type": "نوع پرداخت",
|
||||
"PENDING": "در انتظار",
|
||||
"FAILD": "ناموفق",
|
||||
"COMPELETED": "تکمیل شده",
|
||||
"CANCELED": "لغو شده",
|
||||
"REJECTED": "رد شده",
|
||||
"APPROVED": "تایید شده",
|
||||
"aceept": "تایید",
|
||||
"reject": "رد",
|
||||
"accept_confrim": "آیا از تایید این پرداخت مطمئن هستید؟",
|
||||
"reject_comment_error": "لطفا دلیل رد پرداخت را وارد کنید",
|
||||
"receip_image": "تصویر فیش واریزی"
|
||||
},
|
||||
"support": {
|
||||
"support_plan": "پلن پشتیبانی",
|
||||
"support_plan_list": "لیست پلن پشتیبانی",
|
||||
"support_plan_create": "ایجاد پلن پشتیبانی",
|
||||
"support_plan_edit": "ویرایش پلن پشتیبانی",
|
||||
"1-learning_docs": "راهنما و مستندات آموزشی",
|
||||
"2-ticket_support": "ارسال تیکت",
|
||||
"3-ticket_limit": "ظرفیت ارسال تیکت",
|
||||
"4-response_time": "زمان پاسخگویی به تیکت",
|
||||
"5-technical_issue_resolution": "رفع ایراد فنی",
|
||||
"6-phone_support": "پاسخگویی تلفنی",
|
||||
"7-technical_expert_access": "دسترسی به متخصصین فنی",
|
||||
"8-backup_version": "نسخه بکاپ",
|
||||
"9-service_functionality_test": "تست عملکرد سرویس",
|
||||
"10-on_site_support": "پشتیبانی در محل",
|
||||
"11-on_site_training": "آموزش در محل",
|
||||
"plans": "پلن ها",
|
||||
"plan_name": "نام پلن",
|
||||
"price": "قیمت",
|
||||
"duration": "مدت",
|
||||
"features": "ویژگی ها",
|
||||
"actions": "عملیات",
|
||||
"new_plan": "پلن جدید",
|
||||
"plan_users": "کاربران ",
|
||||
"ACTIVE": "فعال",
|
||||
"INACTIVE": "غیرفعال",
|
||||
"EXPIRED": "منقضی شده",
|
||||
"start_date": "تاریخ شروع",
|
||||
"end_date": "تاریخ پایان",
|
||||
"status": "وضعیت",
|
||||
"full_name": "نام و نام خانوادگی",
|
||||
"users": "کاربران"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
import { type FC } from 'react'
|
||||
import LogoImage from '../../assets/images/logo.svg'
|
||||
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||
import { useAuthStore } from './store/AuthStore'
|
||||
import LoginStep1 from './components/LoginStep1'
|
||||
import LoginStep2 from './components/LoginStep2'
|
||||
import LoginStep3 from './components/LoginStep3'
|
||||
|
||||
const Login: FC = () => {
|
||||
|
||||
const { stepLogin, phone, email } = useAuthStore()
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||
<div className='flex-1 min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||
<div className='flex-1 h-full flex flex-col'>
|
||||
<img src={LogoSmallImage} className='w-8' />
|
||||
|
||||
<div className='flex flex-1 flex-col h-full justify-center'>
|
||||
{
|
||||
stepLogin === 1 ?
|
||||
<LoginStep1 />
|
||||
:
|
||||
stepLogin === 2 && !!phone ?
|
||||
<LoginStep2 />
|
||||
: stepLogin === 2 && !!email ?
|
||||
<LoginStep3 />
|
||||
: stepLogin === 3 ?
|
||||
<LoginStep3 />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
|
||||
<img src={LogoImage} className='h-20' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
@@ -0,0 +1,60 @@
|
||||
import { type FC } from 'react'
|
||||
import LogoImage from '../../assets/images/logo.svg'
|
||||
import LogoSmallImage from '../../assets/images/logo-small.svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import RegisterStep1 from './components/RegisterStep1'
|
||||
import { useAuthStore } from './store/AuthStore'
|
||||
import RegisterStep2 from './components/RegisterStep2'
|
||||
|
||||
|
||||
const Register: FC = () => {
|
||||
|
||||
const { stepLogin } = useAuthStore()
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
|
||||
return (
|
||||
<div className='w-full h-full flex justify-center lg:py-[75px] py-4 lg:items-center lg:px-10 px-4'>
|
||||
<div className='flex w-full max-h-[812px] max-w-[1200px] bg-secondary h-full rounded-3xl overflow-hidden'>
|
||||
<div className='flex-1 flex flex-col justify-center min-w-[50%] overflow-y-auto bg-white lg:px-9 px-4 py-7'>
|
||||
<div className=''>
|
||||
<img src={LogoSmallImage} className='w-8' />
|
||||
<div className='mt-5'>
|
||||
<h2 className='text-2xl font-bold'>
|
||||
{t('auth.welcome')}
|
||||
</h2>
|
||||
<p className='text-description text-sm mt-2'>
|
||||
{t('auth.enter_information')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{
|
||||
stepLogin === 1 ?
|
||||
<RegisterStep1 />
|
||||
:
|
||||
<RegisterStep2 />
|
||||
}
|
||||
|
||||
<div className='mt-6 flex justify-center gap-2 items-center text-sm'>
|
||||
<p className='text-description'>
|
||||
{t('auth.before_registered')}
|
||||
</p>
|
||||
<Link to={Pages.auth.login}>
|
||||
<div>{t('auth.login')}</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 min-w-[50%] lg:flex hidden justify-center items-center'>
|
||||
<img src={LogoImage} className='h-20' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Register
|
||||
@@ -0,0 +1,99 @@
|
||||
import { FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoginType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Error from '../../../components/Error'
|
||||
import Button from '../../../components/Button'
|
||||
import { isEmail } from '../../../config/func'
|
||||
import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
const LoginStep1: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { setPhone, phone, setStepLogin, setEmail } = useAuthStore()
|
||||
const loginWithOtp = useLoginWithOtp()
|
||||
const checkHasAccount = useCheckHasAccount()
|
||||
|
||||
const formik = useFormik<LoginType>({
|
||||
initialValues: {
|
||||
phone_email: phone,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
phone_email: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
if (isEmail(values.phone_email)) {
|
||||
setEmail(values.phone_email)
|
||||
checkHasAccount.mutate({ email: values.phone_email }, {
|
||||
onSuccess() {
|
||||
setStepLogin(2)
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
},
|
||||
})
|
||||
} else {
|
||||
setPhone(values.phone_email)
|
||||
loginWithOtp.mutate({ phone: values.phone_email }, {
|
||||
onSuccess() {
|
||||
setStepLogin(2)
|
||||
toast.success(t('auth.otp_sent'))
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mt-5'>
|
||||
<h2 className='text-2xl font-bold'>
|
||||
{t('auth.welcome')}
|
||||
</h2>
|
||||
<p className='text-description text-sm mt-2'>
|
||||
{t('auth.enter_info_login')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-16'>
|
||||
<Input
|
||||
label={t('auth.mobile_or_email')}
|
||||
placeholder={t('auth.enter_mobile_or_email')}
|
||||
type='text'
|
||||
className='text-right'
|
||||
name='phone_email'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.phone_email}
|
||||
/>
|
||||
|
||||
{
|
||||
formik.touched.phone_email && formik.errors.phone_email &&
|
||||
<Error
|
||||
errorText={formik.errors.phone_email}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
label={t('auth.next')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginStep1
|
||||
@@ -0,0 +1,118 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { OtpVerifyType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Button from '../../../components/Button'
|
||||
import OTPInput from 'react-otp-input'
|
||||
import { useCountDown } from '../../../hooks/useCountDown'
|
||||
// import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
import { useOtpVerify } from '../hooks/useAuthData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { setToken, setRefreshToken } from '../../../config/func'
|
||||
|
||||
const LoginStep2: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { phone, setStepLogin } = useAuthStore()
|
||||
const { value } = useCountDown(2, true)
|
||||
const otpVerify = useOtpVerify()
|
||||
|
||||
const formik = useFormik<OtpVerifyType>({
|
||||
initialValues: {
|
||||
phone: phone,
|
||||
code: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
code: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
const params: OtpVerifyType = {
|
||||
phone: phone,
|
||||
code: values.code
|
||||
}
|
||||
otpVerify.mutate(params, {
|
||||
onSuccess(data) {
|
||||
setToken(data?.data?.accessToken?.token)
|
||||
setRefreshToken(data?.data?.refreshToken?.token)
|
||||
window.location.href = Pages.dashboard
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mt-5'>
|
||||
<h2 className='lg:text-2xl font-bold'>
|
||||
{t('auth.enter_otp')}
|
||||
</h2>
|
||||
<p className='text-description flex lg:flex-row flex-col lg:gap-1 gap-2 text-sm lg:mt-2 mt-3'>
|
||||
<div className='flex gap-1'>
|
||||
<div>{t('auth.verfify_code_text_1')}</div>
|
||||
<div>{phone}</div>
|
||||
<div>
|
||||
{t('auth.verfify_code_text_2')}
|
||||
</div>
|
||||
</div>
|
||||
<div onClick={() => setStepLogin(1)} className='text-black cursor-pointer'>
|
||||
{t('auth.change_number')}
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-16'>
|
||||
<div className='text-sm'>
|
||||
{t('auth.verify_code')}
|
||||
</div>
|
||||
<div className='mt-2 w-full flex justify-center dltr otp'>
|
||||
<OTPInput
|
||||
shouldAutoFocus
|
||||
value={formik.values.code}
|
||||
numInputs={5}
|
||||
inputType='tel'
|
||||
onChange={(otp: string) => formik.setFieldValue('code', otp)}
|
||||
renderInput={(props) => <input {...props} className='w-full h-[50px] flex-1 mx-2 bg-white border rounded-2.5' />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
formik.touched.code && formik.errors.code &&
|
||||
<div className='text-xs mt-2 text-red-500'>{formik.errors.code}</div>
|
||||
}
|
||||
|
||||
<div className='flex mt-4 justify-end'>
|
||||
<div className='flex gap-1 text-description text-xs'>
|
||||
<div>{value}</div>
|
||||
<div>{t('auth.counter_otp')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
label={t('auth.login')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={otpVerify.isPending}
|
||||
/>
|
||||
|
||||
{/* <div onClick={() => setStepLogin(3)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
|
||||
<p className='text-description'>
|
||||
{t('auth.login_with_password')}
|
||||
</p>
|
||||
<img src={ArrowLeftIcon} className='w-5' />
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginStep2
|
||||
@@ -0,0 +1,101 @@
|
||||
import { FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoginWithPasswordType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Error from '../../../components/Error'
|
||||
import Button from '../../../components/Button'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
|
||||
import { useLoginWithPassword } from '../hooks/useAuthData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { setRefreshToken } from '../../../config/func'
|
||||
import { setToken } from '../../../config/func'
|
||||
|
||||
const LoginStep3: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { setStepLogin, email } = useAuthStore()
|
||||
const loginWithPassword = useLoginWithPassword()
|
||||
|
||||
const formik = useFormik<LoginWithPasswordType>({
|
||||
initialValues: {
|
||||
password: '',
|
||||
email: email
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
password: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
loginWithPassword.mutate(values, {
|
||||
onSuccess(data) {
|
||||
setToken(data?.data?.accessToken?.token)
|
||||
setRefreshToken(data?.data?.refreshToken?.token)
|
||||
window.location.href = Pages.dashboard
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
},
|
||||
})
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mt-5'>
|
||||
<h2 className='text-2xl font-bold'>
|
||||
{t('auth.enter_password_step3')}
|
||||
</h2>
|
||||
<p className='text-description text-sm mt-2'>
|
||||
{t('auth.enter_your_password')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='mt-16'>
|
||||
<Input
|
||||
label={t('auth.password')}
|
||||
type='password'
|
||||
className='text-right'
|
||||
name='password'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.password}
|
||||
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
||||
/>
|
||||
|
||||
{
|
||||
formik.touched.password && formik.errors.password &&
|
||||
<Error
|
||||
errorText={formik.errors.password}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
<Link to={Pages.auth.forgotPassword}>
|
||||
<div className='mt-4 flex justify-end text-sm'>
|
||||
{t('auth.forgot_password')}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
label={t('auth.login')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
/>
|
||||
|
||||
<div onClick={() => setStepLogin(2)} className='mt-6 cursor-pointer flex gap-2 items-center text-sm'>
|
||||
<p className='text-description'>
|
||||
{t('auth.login_with_otp')}
|
||||
</p>
|
||||
<img src={ArrowLeftIcon} className='w-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginStep3
|
||||
@@ -0,0 +1,77 @@
|
||||
import { FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CheckHasAccountPhoneType } from '../types/AuthTypes'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import Error from '../../../components/Error'
|
||||
import Button from '../../../components/Button'
|
||||
import { useCheckHasAccountRegister, useLoginWithOtp } from '../hooks/useAuthData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
|
||||
const RegisterStep1: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { setPhone, setStepLogin } = useAuthStore()
|
||||
const loginWithOtp = useLoginWithOtp()
|
||||
const checkHasAccount = useCheckHasAccountRegister()
|
||||
|
||||
const formik = useFormik<CheckHasAccountPhoneType>({
|
||||
initialValues: {
|
||||
phone: '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
phone: Yup.string()
|
||||
.required(t('errors.required'))
|
||||
}),
|
||||
onSubmit(values) {
|
||||
checkHasAccount.mutate(values, {
|
||||
onSuccess() {
|
||||
setPhone(values.phone)
|
||||
setStepLogin(2)
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex-1'>
|
||||
|
||||
|
||||
<div className='mt-16'>
|
||||
<Input
|
||||
label={t('auth.mobile_number')}
|
||||
placeholder={t('auth.enter_mobile_number')}
|
||||
type='tel'
|
||||
className='text-right'
|
||||
name='phone'
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.phone}
|
||||
/>
|
||||
|
||||
{
|
||||
formik.touched.phone && formik.errors.phone &&
|
||||
<Error
|
||||
errorText={formik.errors.phone}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
label={t('auth.next')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={loginWithOtp.isPending || checkHasAccount.isPending}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterStep1
|
||||
@@ -0,0 +1,136 @@
|
||||
import { FC, Fragment } from 'react'
|
||||
import { useFormik } from 'formik'
|
||||
import { RegisterType } from '../types/AuthTypes'
|
||||
import * as Yup from 'yup'
|
||||
import { useAuthStore } from '../store/AuthStore'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '../../../components/Input'
|
||||
import DatePickerComponent from '../../../components/DatePicker'
|
||||
import Button from '../../../components/Button'
|
||||
import { useRegister } from '../hooks/useAuthData'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { Pages } from '../../../config/Pages'
|
||||
|
||||
const RegisterStep2: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { phone } = useAuthStore()
|
||||
const register = useRegister()
|
||||
|
||||
const formik = useFormik<RegisterType>({
|
||||
initialValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
birthDate: '',
|
||||
code: 0,
|
||||
nationalCode: '',
|
||||
password: '',
|
||||
phone: phone,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
firstName: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
lastName: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
birthDate: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
code: Yup.number()
|
||||
.required(t('errors.required')),
|
||||
nationalCode: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
password: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
phone: Yup.string()
|
||||
.required(t('errors.required')),
|
||||
}),
|
||||
onSubmit(values) {
|
||||
register.mutate(values, {
|
||||
onSuccess(data) {
|
||||
localStorage.setItem(import.meta.env.VITE_TOKEN_NAME, data?.data?.accessToken?.token)
|
||||
localStorage.setItem(import.meta.env.VITE_REFRESH_TOKEN_NAME, data?.data?.refreshToken?.token)
|
||||
window.location.href = Pages.dashboard
|
||||
},
|
||||
onError(error: ErrorType) {
|
||||
toast.error(error?.response?.data?.error?.message[0])
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className='mt-5 rowTwoInput'>
|
||||
<Input
|
||||
label={t('auth.name')}
|
||||
placeholder={t('auth.enter_name')}
|
||||
name='firstName'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.firstName && formik.errors.firstName ? formik.errors.firstName : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('auth.family')}
|
||||
placeholder={t('auth.enter_family')}
|
||||
name='lastName'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.lastName && formik.errors.lastName ? formik.errors.lastName : ''}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className='mt-4 min-w-full rowTwoInput'>
|
||||
<DatePickerComponent
|
||||
label={t('auth.dateofbirth')}
|
||||
onChange={(date: string) => formik.setFieldValue('birthDate', date)}
|
||||
placeholder={t('auth.select_date')}
|
||||
error_text={formik.touched.birthDate && formik.errors.birthDate ? formik.errors.birthDate : ''}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('auth.national_code')}
|
||||
placeholder={t('auth.national_code_enter')}
|
||||
name='nationalCode'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.nationalCode && formik.errors.nationalCode ? formik.errors.nationalCode : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 rowTwoInput'>
|
||||
<Input
|
||||
label={t('auth.password')}
|
||||
placeholder={t('auth.enter_password')}
|
||||
type='password'
|
||||
name='password'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.password && formik.errors.password ? formik.errors.password : ''}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth.phonen_number')}
|
||||
placeholder={t('auth.enter_phone_number')}
|
||||
readOnly
|
||||
value={phone}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 rowTwoInput'>
|
||||
<Input
|
||||
label={t('auth.verify_code')}
|
||||
placeholder={t('auth.enter_verify_code')}
|
||||
type='tel'
|
||||
name='code'
|
||||
onChange={formik.handleChange}
|
||||
error_text={formik.touched.code && formik.errors.code ? formik.errors.code : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
label={t('auth.next')}
|
||||
className='mt-8'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={register.isPending}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterStep2
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import * as api from '../service/AuthService'
|
||||
import { CheckHasAccountPhoneType, CheckHasAccountType, LoginWithOtpType, LoginWithPasswordType, OtpVerifyType, RegisterType } from '../types/AuthTypes';
|
||||
|
||||
export const useLoginWithPassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: LoginWithPasswordType) => api.loginWithPassword(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useLoginWithOtp = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: LoginWithOtpType) => api.loginWithOtp(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useOtpVerify = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: OtpVerifyType) => api.otpVerify(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCheckHasAccount = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CheckHasAccountType) => api.checkHasAccount(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCheckHasAccountRegister = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: CheckHasAccountPhoneType) => api.checkHasAccountRegister(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useRegister = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: RegisterType) => api.register(variables),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import axios from "../../../config/axios";
|
||||
import {
|
||||
type CheckHasAccountPhoneType,
|
||||
type CheckHasAccountType,
|
||||
type LoginWithOtpType,
|
||||
type LoginWithPasswordType,
|
||||
type OtpVerifyType,
|
||||
type RefreshTokenType,
|
||||
type RegisterType,
|
||||
} from "../types/AuthTypes";
|
||||
|
||||
export const loginWithPassword = async (params: LoginWithPasswordType) => {
|
||||
const { data } = await axios.post(`/auth/login/password/admin`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const loginWithOtp = async (params: LoginWithOtpType) => {
|
||||
const { data } = await axios.post(`/auth/otp/send`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const otpVerify = async (params: OtpVerifyType) => {
|
||||
const { data } = await axios.post(`/auth/otp/verify/admin`, params);
|
||||
return data;
|
||||
};
|
||||
export const checkHasAccount = async (params: CheckHasAccountType) => {
|
||||
const { data } = await axios.post(`/auth/check`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const checkHasAccountRegister = async (
|
||||
params: CheckHasAccountPhoneType
|
||||
) => {
|
||||
const { data } = await axios.post(`/auth/register/initiate`, params);
|
||||
return data;
|
||||
};
|
||||
export const register = async (params: RegisterType) => {
|
||||
const { data } = await axios.post(`/auth/register/complete`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const refreshToken = async (params: RefreshTokenType) => {
|
||||
const { data } = await axios.post(`/auth/refresh`, params);
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { create } from "zustand";
|
||||
import { AuthStoreType } from "../../auth/types/AuthTypes";
|
||||
|
||||
export const useAuthStore = create<AuthStoreType>((set) => ({
|
||||
phone: "",
|
||||
setPhone(value) {
|
||||
set({ phone: value });
|
||||
},
|
||||
email: "",
|
||||
setEmail(value) {
|
||||
set({ email: value });
|
||||
},
|
||||
stepLogin: 1,
|
||||
setStepLogin(value) {
|
||||
set({ stepLogin: value });
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,54 @@
|
||||
import { type XOR } from "../../../helpers/types";
|
||||
|
||||
export type LoginType = {
|
||||
phone_email: string;
|
||||
};
|
||||
export type LoginPasswordType = {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type AuthStoreType = {
|
||||
phone: string;
|
||||
setPhone: (value: string) => void;
|
||||
email: string;
|
||||
setEmail: (value: string) => void;
|
||||
stepLogin: number;
|
||||
setStepLogin: (value: number) => void;
|
||||
};
|
||||
|
||||
export type LoginWithPasswordType = XOR<
|
||||
{ email: string },
|
||||
{ phone: number }
|
||||
> & {
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type LoginWithOtpType = {
|
||||
phone: string;
|
||||
};
|
||||
|
||||
export type OtpVerifyType = {
|
||||
phone: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type CheckHasAccountType = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type CheckHasAccountPhoneType = {
|
||||
phone: string;
|
||||
};
|
||||
|
||||
export type RegisterType = {
|
||||
phone: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthDate: string;
|
||||
nationalCode: string;
|
||||
password: string;
|
||||
code: number;
|
||||
};
|
||||
export type RefreshTokenType = {
|
||||
refreshToken: string;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
const Auth = () => {
|
||||
return (
|
||||
<div>Auth</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Auth
|
||||
@@ -0,0 +1,36 @@
|
||||
import { type FC } from 'react'
|
||||
import SideBar from '../shared/SideBar'
|
||||
import Header from '../shared/Header'
|
||||
import { Routes } from 'react-router-dom'
|
||||
import Footer from '../shared/Footer'
|
||||
import { clx } from '../helpers/utils'
|
||||
import { useSharedStore } from '../shared/store/sharedStore'
|
||||
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
const { hasSubMenu } = useSharedStore()
|
||||
|
||||
return (
|
||||
<div className='p-4 overflow-hidden'>
|
||||
<SideBar />
|
||||
<Header />
|
||||
<div className={clx(
|
||||
'flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]',
|
||||
hasSubMenu && 'xl:ms-[305px]',
|
||||
)}>
|
||||
<div className={clx(
|
||||
`overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`,
|
||||
)}>
|
||||
<div className='pb-20'>
|
||||
<Routes>
|
||||
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainRouter
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Home2, Messages3, NotificationStatus, People, Receipt21 } from 'iconsax-react'
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pages } from '../config/Pages'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const Footer: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
return (
|
||||
<div className='xl:hidden'>
|
||||
<div className='h-[60px] '></div>
|
||||
<div className='fixed bottom-2 right-3 left-3 bg-white h-[60px] rounded-2xl flex justify-between items-center px-3'>
|
||||
<Link to={Pages.dashboard}>
|
||||
<div className='text-description w-[70px] flex flex-col items-center gap-1.5'>
|
||||
<Home2
|
||||
className='size-5'
|
||||
color='#8C90A3'
|
||||
/>
|
||||
<div className='text-[10px]'>
|
||||
{t('footer.home')}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link to={Pages.customer.list}>
|
||||
<div className='text-description w-[70px] flex flex-col items-center gap-1.5'>
|
||||
<People
|
||||
className='size-5'
|
||||
color='#8C90A3'
|
||||
/>
|
||||
<div className='text-[10px]'>
|
||||
{t('footer.customers')}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link to={Pages.receipts.index}>
|
||||
<div className='text-description w-[70px] flex flex-col items-center gap-1.5'>
|
||||
<div className='bg-white p-1 rounded-full -mt-7'>
|
||||
<div className='bg-black flex justify-center items-center size-10 rounded-full'>
|
||||
<Receipt21
|
||||
className='size-5'
|
||||
color='white'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-[10px] '>
|
||||
{t('footer.receipts')}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<Link to={Pages.announcement.list}>
|
||||
<div className='text-description w-[70px] flex flex-col items-center gap-1.5'>
|
||||
<NotificationStatus
|
||||
className='size-5'
|
||||
color='#8C90A3'
|
||||
/>
|
||||
<div className='text-[10px]'>
|
||||
{t('footer.announcements')}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link to={Pages.ticket.list}>
|
||||
<div className='text-description w-[70px] flex flex-col items-center gap-1.5'>
|
||||
<Messages3
|
||||
className='size-5'
|
||||
color='#8C90A3'
|
||||
/>
|
||||
<div className='text-[10px]'>
|
||||
{t('footer.tickets')}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
@@ -0,0 +1,99 @@
|
||||
import { type FC } from 'react'
|
||||
import Input from '../components/Input'
|
||||
// import { ArrowDown2, CloseCircle, HambergerMenu, Logout, Wallet } from 'iconsax-react'
|
||||
// import AvatarImage from '../assets/images/avatar_image.png'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Pages } from '../config/Pages'
|
||||
import { useSharedStore } from './store/sharedStore'
|
||||
import { HambergerMenu, Wallet } from 'iconsax-react'
|
||||
|
||||
const Header: FC = () => {
|
||||
|
||||
// const location = useLocation();
|
||||
// const [popoverKey, setPopoverKey] = useState(0);
|
||||
const { t } = useTranslation('global')
|
||||
const { setOpenSidebar, openSidebar } = useSharedStore()
|
||||
|
||||
// useEffect(() => {
|
||||
// setPopoverKey((prevKey) => prevKey + 1);
|
||||
// }, [location.pathname]);
|
||||
|
||||
// const handleLogout = () => {
|
||||
// removeToken()
|
||||
// removeRefreshToken()
|
||||
// window.location.href = Pages.auth.login
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className='fixed z-10 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]'>
|
||||
|
||||
<div className='min-w-[270px] hidden xl:block'>
|
||||
<Input
|
||||
variant='search'
|
||||
placeholder={t('header.search')}
|
||||
/>
|
||||
</div>
|
||||
<div onClick={() => setOpenSidebar(!openSidebar)} className='xl:hidden block'>
|
||||
<HambergerMenu size={24} color='black' />
|
||||
</div>
|
||||
{/* <img src={LogoImage} className='h-6 xl:hidden block absolute right-0 left-0 mx-auto' /> */}
|
||||
<div className='flex xl:gap-6 gap-4 items-center'>
|
||||
<Link className='xl:hidden' to={Pages.wallet}>
|
||||
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
|
||||
</Link>
|
||||
{/* <Notifications /> */}
|
||||
{/* {
|
||||
data && (
|
||||
<Popover className="relative" key={popoverKey}>
|
||||
<PopoverButton >
|
||||
<div className='flex gap-2 items-center mt-2.5'>
|
||||
<div className='size-6 rounded-full bg-description overflow-hidden'>
|
||||
<img src={data?.data?.user?.profilePic ? data?.data?.user?.profilePic : AvatarImage} className='size-full object-cover' />
|
||||
</div>
|
||||
<div className='xl:flex hidden gap-1 items-center'>
|
||||
<div className='text-xs'>
|
||||
{data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName}
|
||||
</div>
|
||||
<ArrowDown2 size={14} color='#8C90A3' />
|
||||
</div>
|
||||
</div>
|
||||
</PopoverButton>
|
||||
|
||||
<PopoverPanel style={{ minHeight: window.innerWidth < 1140 ? window.innerHeight : undefined }} anchor="bottom" className="flex xl:ml-6 overflow-auto flex-col gap-3 bg-white boxShadow xl:mt-7 z-30 py-4 text-xs rounded-2.5 xl:w-[300px] -mt-[50px] w-full fixed xl:h-fit h-full shadow-md">
|
||||
<div className='absolute xl:hidden top-6 left-6'>
|
||||
<CloseCircle onClick={() => setPopoverKey((prevKey) => prevKey + 1)} size={20} color='black' />
|
||||
</div>
|
||||
<Link to={Pages.profile} className='flex flex-col gap-2 items-center justify-center border-b border-[#EAEDF5] pb-4'>
|
||||
<div className='size-14 rounded-full overflow-hidden'>
|
||||
<img src={data?.data?.user?.profilePic ? data?.data?.user?.profilePic : AvatarImage} className='size-full object-cover' />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{data?.data?.user?.firstName + ' ' + data?.data?.user?.lastName}
|
||||
</div>
|
||||
|
||||
<div className='text-description'>
|
||||
{data?.data?.user?.email}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className='px-6 mt-2'>
|
||||
<div onClick={() => handleLogout()} className='flex gap-2 items-center cursor-pointer'>
|
||||
<Logout size={20} color='black' />
|
||||
<div>
|
||||
{t('sidebar.logout')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
)
|
||||
} */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
@@ -0,0 +1,346 @@
|
||||
import { type FC, useEffect } from 'react'
|
||||
import LogoImage from '../assets/images/logo.svg'
|
||||
import LogoSmall from '../assets/images/logo-small.svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, Code, CodeCircle, DocumentLike, DocumentText, Element3, Gallery, Headphone, Home2, Logout, Message, Messages3, Money3, NotificationStatus, People, Profile, Receipt21, Setting2, SmsTracking, Teacher, TicketDiscount, UserSquare } from 'iconsax-react'
|
||||
import SideBarItem from './SideBarItem'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../config/Pages'
|
||||
import { useSharedStore } from './store/sharedStore'
|
||||
import { clx } from '../helpers/utils'
|
||||
import ServicesSubMenu from './components/ServicesSubMenu'
|
||||
import { SideBarItemHasSubMenu } from '../config/SideBarSubMenu'
|
||||
import ReceiptSubMenu from './components/ReceiptSubMenu'
|
||||
import CustomerSubMenu from './components/CustomerSubMenu'
|
||||
import TicketSubMenu from './components/TicketSubMenu'
|
||||
import UsersSubMenu from './components/UsersSubMenu'
|
||||
import LearningSubMenu from './components/LearningSubMenu'
|
||||
import BlogSubMenu from './components/BlogSubMenu'
|
||||
// import { useGetProfile } from '../pages/profile/hooks/useProfileData'
|
||||
import SupportSubMenu from './components/SupportSubMenu'
|
||||
const SideBar: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const { openSidebar, setOpenSidebar, hasSubMenu, setSubMenuName, setSubtMenu, subMenuName } = useSharedStore()
|
||||
const location = useLocation()
|
||||
// const { data: profile } = useGetProfile()
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const split = location.pathname.split('/')
|
||||
|
||||
|
||||
if (SideBarItemHasSubMenu.includes(split[1])) {
|
||||
setSubMenuName(split[1])
|
||||
setSubtMenu(true)
|
||||
} else {
|
||||
setSubMenuName('')
|
||||
setSubtMenu(false)
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname])
|
||||
|
||||
|
||||
const iconSizeSideBar = 20
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
openSidebar && <div className='fixed top-0 left-0 right-0 bottom-0 bg-black bg-opacity-50 z-10' onClick={() => setOpenSidebar(false)} />
|
||||
}
|
||||
<div
|
||||
className={clx(
|
||||
'fixed xl:flex translate-x-[400px] xl:translate-x-0 transition-all ease-in-out opacity-0 invisible xl:visible xl:opacity-100 xl:right-4 right-0 xl:top-4 top-0 xl:bottom-4 bottom-0 xl:rounded-[32px] w-[250px] bg-white flex-col py-12',
|
||||
openSidebar && 'opacity-100 visible -translate-x-[0px] block z-40',
|
||||
hasSubMenu && 'w-24 !rounded-tl-none !rounded-bl-none'
|
||||
)}
|
||||
>
|
||||
<div className='flex justify-center'>
|
||||
{
|
||||
!hasSubMenu ?
|
||||
<img src={LogoImage} className='w-[140px]' />
|
||||
:
|
||||
<img src={LogoSmall} className='w-7' />
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='flex-1 flex flex-col h-full overflow-y-auto no-scrollbar'>
|
||||
<div className={clx(
|
||||
'mt-10 px-12 text-header text-sm font-normal',
|
||||
hasSubMenu && 'px-2 text-center'
|
||||
)}>
|
||||
{t('sidebar.menu')}
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Home2 variant={isActive('dashboard') ? 'Bold' : 'Outline'} color={isActive('dashboard') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.mainpage')}
|
||||
isActive={isActive('dashboard')}
|
||||
link={Pages.dashboard}
|
||||
activeName=''
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Element3 variant={isActive('services') ? 'Bold' : 'Outline'} color={isActive('services') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.services')}
|
||||
isActive={isActive('services')}
|
||||
link={Pages.services.list}
|
||||
name='services'
|
||||
activeName='services'
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<Headphone variant={isActive('support') ? 'Bold' : 'Outline'} color={isActive('support') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.support')}
|
||||
isActive={isActive('support')}
|
||||
link={Pages.support.list}
|
||||
activeName='support_plan'
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<People variant={isActive('customers') ? 'Bold' : 'Outline'} color={isActive('customers') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.customers')}
|
||||
isActive={isActive('customers')}
|
||||
link={Pages.customers.list}
|
||||
activeName='customers'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<UserSquare variant={isActive('representatives') ? 'Bold' : 'Outline'} color={isActive('representatives') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.representatives')}
|
||||
isActive={isActive('representatives')}
|
||||
link={Pages.representative.list}
|
||||
activeName='agents'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<CodeCircle variant={isActive('developers') ? 'Bold' : 'Outline'} color={isActive('developers') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.developers')}
|
||||
isActive={isActive('developers')}
|
||||
link={Pages.developers.list}
|
||||
activeName='developers'
|
||||
/>
|
||||
|
||||
|
||||
<SideBarItem
|
||||
icon={<Receipt21 variant={isActive('receipts') ? 'Bold' : 'Outline'} color={isActive('receipts') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.receipt_list')}
|
||||
isActive={isActive('receipts')}
|
||||
link={Pages.receipts.index}
|
||||
activeName='invoices'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Card variant={isActive('transactions') ? 'Bold' : 'Outline'} color={isActive('transactions') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.transactions')}
|
||||
isActive={isActive('transactions')}
|
||||
link={Pages.transactions}
|
||||
activeName='transactions'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Money3 variant={isActive('payments') ? 'Bold' : 'Outline'} color={isActive('payments') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.payments')}
|
||||
isActive={isActive('payments')}
|
||||
link={Pages.payment.list}
|
||||
activeName='payments'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<TicketDiscount variant={isActive('discounts') ? 'Bold' : 'Outline'} color={isActive('discounts') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.discounts')}
|
||||
isActive={isActive('discounts')}
|
||||
link={Pages.discount.list}
|
||||
activeName='discounts'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Code variant={isActive('referral-code') ? 'Bold' : 'Outline'} color={isActive('referral-code') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.code_referral')}
|
||||
isActive={isActive('referral-code')}
|
||||
link={Pages.referralCode.list}
|
||||
activeName='referralCode'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Profile variant={isActive('users') ? 'Bold' : 'Outline'} color={isActive('users') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.users')}
|
||||
isActive={isActive('users')}
|
||||
link={Pages.users.list}
|
||||
activeName='admins'
|
||||
/>
|
||||
<SideBarItem
|
||||
icon={<Gallery variant={isActive('sliders') ? 'Bold' : 'Outline'} color={isActive('sliders') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.sliders')}
|
||||
isActive={isActive('sliders')}
|
||||
link={Pages.sliders.list}
|
||||
activeName='blogs'
|
||||
/>
|
||||
|
||||
<SideBarItem
|
||||
icon={<Message variant={isActive('feedback') ? 'Bold' : 'Outline'} color={isActive('feedback') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.feedback')}
|
||||
isActive={isActive('feedback')}
|
||||
link={Pages.feedback.list}
|
||||
activeName='services'
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{/* {
|
||||
profile?.data?.user?.roles.some((role: { name: string }) => role.name === 'super_admin') &&
|
||||
<div className={clx(
|
||||
'mt-10 px-12 text-header text-sm font-normal',
|
||||
hasSubMenu && 'px-2 text-center'
|
||||
)}>
|
||||
{t('sidebar.other')}
|
||||
</div>
|
||||
} */}
|
||||
|
||||
|
||||
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Messages3 variant={isActive('tickets') ? 'Bold' : 'Outline'} color={isActive('tickets') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.ticket')}
|
||||
isActive={isActive('tickets')}
|
||||
link={Pages.ticket.list}
|
||||
activeName='tickets'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<DocumentText variant={isActive('criticisms') ? 'Bold' : 'Outline'} color={isActive('criticisms') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.criticisms')}
|
||||
isActive={isActive('criticisms')}
|
||||
link={Pages.criticisms.list}
|
||||
activeName='criticisms'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* {
|
||||
profile?.data?.user?.roles?.some((role: { name: string }) => role.name === 'super_admin') &&
|
||||
<div className={clx(
|
||||
'mt-10 px-12 text-header text-sm font-normal',
|
||||
hasSubMenu && 'px-2 text-center text-xs'
|
||||
)}>
|
||||
{t('sidebar.mnage_content')}
|
||||
</div>
|
||||
} */}
|
||||
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<DocumentLike variant={isActive('ads') ? 'Bold' : 'Outline'} color={isActive('ads') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.ads')}
|
||||
isActive={isActive('ads')}
|
||||
link={Pages.ads.list}
|
||||
activeName='advertisements'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<NotificationStatus variant={isActive('announcement') ? 'Bold' : 'Outline'} color={isActive('announcement') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.announcement')}
|
||||
isActive={isActive('announcement')}
|
||||
link={Pages.announcement.list}
|
||||
activeName='announcements'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<DocumentText variant={isActive('blog') ? 'Bold' : 'Outline'} color={isActive('blog') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.blog')}
|
||||
isActive={isActive('blog')}
|
||||
link={Pages.blog.list}
|
||||
activeName='blogs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Teacher variant={isActive('learning') ? 'Bold' : 'Outline'} color={isActive('learning') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.learning')}
|
||||
isActive={isActive('learning')}
|
||||
link={Pages.learning.list}
|
||||
activeName='learnings'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<SmsTracking variant={isActive('messages') ? 'Bold' : 'Outline'} color={isActive('messages') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.messages')}
|
||||
isActive={isActive('messages')}
|
||||
link={Pages.messages.list}
|
||||
activeName='contacts_us'
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className='flex-1 flex flex-col justify-end mt-14'>
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Card variant={isActive('card') ? 'Bold' : 'Outline'} color={isActive('card') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.card')}
|
||||
isActive={isActive('card')}
|
||||
link={Pages.cardBank.list}
|
||||
activeName='bank_accounts'
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Setting2 variant={isActive('setting') ? 'Bold' : 'Outline'} color={isActive('setting') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.setting')}
|
||||
isActive={isActive('setting')}
|
||||
link={Pages.setting}
|
||||
activeName='settings'
|
||||
/>
|
||||
</div>
|
||||
<div className='text-xs text-[#8C90A3]'>
|
||||
<SideBarItem
|
||||
icon={<Logout variant={isActive('logout') ? 'Bold' : 'Outline'} color={isActive('logout') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||
title={t('sidebar.logout')}
|
||||
isActive={isActive('logout')}
|
||||
link={`#`}
|
||||
isLogout
|
||||
activeName=''
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
hasSubMenu && (openSidebar || window.innerWidth > 1139) &&
|
||||
<div className='fixed xl:right-[112px] right-[96px] bg-white z-20 xl:top-4 xl:bottom-4 top-0 bottom-0 rounded-tl-[32px] rounded-bl-[32px] w-[190px] border-r'>
|
||||
{
|
||||
subMenuName === 'services' ?
|
||||
<ServicesSubMenu />
|
||||
: subMenuName === 'receipts' ?
|
||||
<ReceiptSubMenu />
|
||||
: subMenuName === 'customers' ?
|
||||
<CustomerSubMenu />
|
||||
: subMenuName === 'tickets' ?
|
||||
<TicketSubMenu />
|
||||
: subMenuName === 'users' ?
|
||||
<UsersSubMenu />
|
||||
: subMenuName === 'learning' ?
|
||||
<LearningSubMenu />
|
||||
: subMenuName === 'blog' ?
|
||||
<BlogSubMenu />
|
||||
: subMenuName === 'support' ?
|
||||
<SupportSubMenu />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SideBar
|
||||
@@ -0,0 +1,63 @@
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { clx } from '../helpers/utils'
|
||||
import { Pages } from '../config/Pages'
|
||||
import { useSharedStore } from './store/sharedStore'
|
||||
import { removeToken, removeRefreshToken } from '../config/func'
|
||||
// import { useGetAdminPermissions } from '../pages/users/hooks/useUserData'
|
||||
|
||||
type Props = {
|
||||
icon: ReactNode,
|
||||
title: string,
|
||||
isActive: boolean,
|
||||
link: string,
|
||||
isLogout?: boolean,
|
||||
name?: string,
|
||||
activeName?: string
|
||||
}
|
||||
|
||||
const SideBarItem: FC<Props> = (props: Props) => {
|
||||
|
||||
const { hasSubMenu, setSubMenuName, setSubtMenu } = useSharedStore()
|
||||
// const { data: adminPermissions } = useGetAdminPermissions()
|
||||
const handleLogout = () => {
|
||||
removeToken()
|
||||
removeRefreshToken()
|
||||
window.location.href = Pages.auth.login
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (props.name) {
|
||||
setSubMenuName(props.name)
|
||||
setSubtMenu(true)
|
||||
}
|
||||
}
|
||||
// if (props.activeName === '' || props.activeName && adminPermissions?.data?.permissions?.some((permission: { name: string }) => permission.name === props.activeName)) {
|
||||
if (true) {
|
||||
return (
|
||||
<Link onClick={props.isLogout ? handleLogout : handleClick} to={props.link} className={clx(
|
||||
'flex text-xs gap-9 mt-4',
|
||||
hasSubMenu && 'flex-col',
|
||||
)}>
|
||||
<div className={clx(
|
||||
'w-1 bg-black h-6',
|
||||
!props.isActive && 'invisible',
|
||||
hasSubMenu && 'hidden',
|
||||
)}></div>
|
||||
<div className={clx(
|
||||
'flex gap-3 items-center',
|
||||
hasSubMenu && 'flex-col text-[10px]',
|
||||
)}>
|
||||
{props.icon}
|
||||
<div className={props.isActive ? 'text-black' : ''}>
|
||||
{props.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SideBarItem
|
||||
@@ -0,0 +1,50 @@
|
||||
import { DocumentText } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SubMenuItem from './SubMenuItem'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const BlogSubMenu: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b pb-10 px-6'>
|
||||
<DocumentText
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
{t('sidebar.blog')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={'لیست'}
|
||||
isActive={isActive('list')}
|
||||
link={Pages.blog.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={'دسته بندی'}
|
||||
isActive={isActive('create')}
|
||||
link={Pages.blog.category}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={'نظرات'}
|
||||
isActive={isActive('comments')}
|
||||
link={Pages.blog.comments}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogSubMenu
|
||||
@@ -0,0 +1,45 @@
|
||||
import { People } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SubMenuItem from './SubMenuItem'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const CustomerSubMenu: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b pb-10 px-6'>
|
||||
<People
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
{t('sidebar.customers')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={t('submenu.customer_list')}
|
||||
isActive={isActive('list')}
|
||||
link={Pages.customer.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.create_customer')}
|
||||
isActive={isActive('create')}
|
||||
link={Pages.customer.create}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerSubMenu
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Teacher } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SubMenuItem from './SubMenuItem'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const LearningSubMenu: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
const location = useLocation()
|
||||
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b pb-10 px-6'>
|
||||
<Teacher
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
{t('sidebar.learning')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={t('learning.learnings')}
|
||||
isActive={isActive('list')}
|
||||
link={Pages.learning.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('learning.new_learning')}
|
||||
isActive={isActive('create')}
|
||||
link={Pages.learning.create}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('learning.category_learning')}
|
||||
isActive={isActive('category')}
|
||||
link={Pages.learning.category}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LearningSubMenu
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Receipt21 } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SubMenuItem from './SubMenuItem'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const ReceiptSubMenu: FC = () => {
|
||||
|
||||
const location = useLocation()
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b pb-10 px-6'>
|
||||
<Receipt21
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
{t('receip.receips')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={t('receip.receip_list')}
|
||||
isActive={isActive('list')}
|
||||
link={Pages.receipts.index}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('receip.add_receip')}
|
||||
isActive={isActive('create')}
|
||||
link={Pages.receipts.create}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReceiptSubMenu
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Element3 } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SubMenuItem from './SubMenuItem'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const ServicesSubMenu: FC = () => {
|
||||
|
||||
const location = useLocation()
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b pb-10 px-6'>
|
||||
<Element3
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
{t('sidebar.services')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={t('submenu.services_list')}
|
||||
isActive={isActive('list')}
|
||||
link={Pages.services.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.add_service')}
|
||||
isActive={isActive('add')}
|
||||
link={Pages.services.add}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.service_category')}
|
||||
isActive={isActive('category')}
|
||||
link={Pages.services.category}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.reviews')}
|
||||
isActive={isActive('reviews')}
|
||||
link={Pages.services.review}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServicesSubMenu
|
||||
@@ -0,0 +1,33 @@
|
||||
import { FC } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { clx } from '../../helpers/utils'
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
isActive: boolean,
|
||||
link: string,
|
||||
isLogout?: boolean,
|
||||
}
|
||||
|
||||
const SubMenuItem: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<Link to={props.link} className={clx(
|
||||
'flex text-xs gap-6 mt-1',
|
||||
)}>
|
||||
<div className={clx(
|
||||
'w-1 bg-black h-6',
|
||||
!props.isActive && 'invisible',
|
||||
)}></div>
|
||||
<div className={clx(
|
||||
'flex gap-3 items-center mt-1',
|
||||
)}>
|
||||
<div className={props.isActive ? 'text-black' : 'text-description'}>
|
||||
{props.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default SubMenuItem
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Headphone } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
import SubMenuItem from './SubMenuItem'
|
||||
|
||||
const SupportSubMenu: FC = () => {
|
||||
|
||||
const location = useLocation()
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
|
||||
return <div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b pb-10 px-6'>
|
||||
<Headphone
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
{t('sidebar.support')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={t('submenu.plan_list')}
|
||||
isActive={isActive('list')}
|
||||
link={Pages.support.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.create_plan')}
|
||||
isActive={isActive('create')}
|
||||
link={Pages.support.create}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
export default SupportSubMenu
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Messages3 } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SubMenuItem from './SubMenuItem'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const TicketSubMenu: FC = () => {
|
||||
|
||||
const location = useLocation()
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b pb-10 px-6'>
|
||||
<Messages3
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
{t('sidebar.ticket')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={t('submenu.ticket_list')}
|
||||
isActive={isActive('list')}
|
||||
link={Pages.ticket.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.send_ticket')}
|
||||
isActive={isActive('create')}
|
||||
link={Pages.ticket.create}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.category')}
|
||||
isActive={isActive('category')}
|
||||
link={Pages.ticket.category}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TicketSubMenu
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Card } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SubMenuItem from './SubMenuItem'
|
||||
|
||||
const TransactionsSubMenu: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b pb-10 px-6'>
|
||||
<Card
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
{t('sidebar.transactions')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={t('sidebar.services')}
|
||||
isActive={isActive('services')}
|
||||
link='/services'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TransactionsSubMenu
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Profile } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SubMenuItem from './SubMenuItem'
|
||||
import { Pages } from '../../config/Pages'
|
||||
|
||||
const UsersSubMenu: FC = () => {
|
||||
|
||||
const { t } = useTranslation('global')
|
||||
|
||||
const isActive = (name: string) => {
|
||||
return location.pathname.includes(name)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-12'>
|
||||
<div className='flex gap-3 items-center border-b pb-10 px-6'>
|
||||
<Profile
|
||||
size={24}
|
||||
color='#000'
|
||||
variant='Bold'
|
||||
/>
|
||||
<div className='text-xs'>
|
||||
{t('sidebar.users')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col mt-10 gap-4'>
|
||||
<SubMenuItem
|
||||
title={t('submenu.user_list')}
|
||||
isActive={isActive('list')}
|
||||
link={Pages.users.list}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.add_user')}
|
||||
isActive={isActive('create')}
|
||||
link={Pages.users.create}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.role_list')}
|
||||
isActive={isActive('all')}
|
||||
link={Pages.users.roleList}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('submenu.role_create')}
|
||||
isActive={isActive('roles-add')}
|
||||
link={Pages.users.roleCreate}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('user.add_user_group')}
|
||||
isActive={isActive('group-add')}
|
||||
link={Pages.users.groupAdd}
|
||||
/>
|
||||
<SubMenuItem
|
||||
title={t('user.list_user_group')}
|
||||
isActive={isActive('groups')}
|
||||
link={Pages.users.groupList}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UsersSubMenu
|
||||
@@ -0,0 +1,11 @@
|
||||
import { create } from "zustand";
|
||||
import { type SharedStoreType } from "../types/SharedTypes";
|
||||
|
||||
export const useSharedStore = create<SharedStoreType>((set) => ({
|
||||
openSidebar: false,
|
||||
setOpenSidebar: (value) => set({ openSidebar: value }),
|
||||
hasSubMenu: false,
|
||||
setSubtMenu: (value: boolean) => set({ hasSubMenu: value }),
|
||||
subMenuName: "",
|
||||
setSubMenuName: (value: string) => set({ subMenuName: value }),
|
||||
}));
|
||||
@@ -0,0 +1,8 @@
|
||||
export type SharedStoreType = {
|
||||
openSidebar: boolean;
|
||||
setOpenSidebar: (value: boolean) => void;
|
||||
hasSubMenu: boolean;
|
||||
setSubtMenu: (value: boolean) => void;
|
||||
subMenuName: string;
|
||||
setSubMenuName: (value: string) => void;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type DefaultError } from "@tanstack/react-query";
|
||||
import { type IBaseResponse } from "./response.types";
|
||||
|
||||
interface IError {
|
||||
message: string[];
|
||||
}
|
||||
|
||||
export interface IErrorResponse extends IBaseResponse {
|
||||
error: IError;
|
||||
}
|
||||
|
||||
export interface IApiErrorRepsonse extends DefaultError {
|
||||
response?: IErrorResponse;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface IBaseResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
}
|
||||
export interface IResponse<T> extends IBaseResponse {
|
||||
data: T;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||