landing + single company + auth

This commit is contained in:
hamid zarghami
2025-05-19 12:06:59 +03:30
parent c4c0c93599
commit 2fbdf05a25
29 changed files with 771 additions and 548 deletions
+4 -2
View File
@@ -1,4 +1,6 @@
VITE_TOKEN_NAME = 'dzone_token'
VITE_REFRESH_TOKEN_NAME = 'dzone_refresh_token'
VITE_BASE_URL = 'https://dzone-api.danakcorp.com'
# VITE_BASE_URL = 'http://192.168.1.109:4000'
# VITE_BASE_URL = 'https://dzone-api.danakcorp.com'
VITE_BASE_URL = 'http://192.168.1.113:4001'
VITE_SLUG_ID = 'slug_id'
+61
View File
@@ -16,12 +16,14 @@
"i18next": "^24.2.1",
"iconsax-react": "^0.0.8",
"js-cookie": "^3.0.5",
"moment-jalaali": "^0.10.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropzone": "^14.3.5",
"react-i18next": "^15.4.0",
"react-loading": "^2.0.3",
"react-multi-date-picker": "^4.5.2",
"react-otp-input": "^3.1.1",
"react-router-dom": "^7.1.3",
"react-toastify": "^11.0.3",
"swiper": "^11.2.1",
@@ -32,6 +34,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/js-cookie": "^3.0.6",
"@types/moment-jalaali": "^0.7.9",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/swiper": "^5.4.3",
@@ -1864,6 +1867,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/moment-jalaali": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz",
"integrity": "sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"moment": ">=2.14.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -3516,6 +3529,12 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jalaali-js": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
"integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==",
"license": "MIT"
},
"node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
@@ -3776,6 +3795,38 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/moment-jalaali": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.10.4.tgz",
"integrity": "sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==",
"license": "MIT",
"dependencies": {
"jalaali-js": "^1.2.7",
"moment": "^2.29.4",
"moment-timezone": "^0.5.46"
}
},
"node_modules/moment-timezone": {
"version": "0.5.48",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"license": "MIT",
"dependencies": {
"moment": "^2.29.4"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4370,6 +4421,16 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/react-otp-input": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/react-otp-input/-/react-otp-input-3.1.1.tgz",
"integrity": "sha512-bjPavgJ0/Zmf/AYi4onj8FbH93IjeD+e8pWwxIJreDEWsU1ILR5fs8jEJmMGWSBe/yyvPP6X/W6Mk9UkOCkTPw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.6 || ^17.0.0 || ^18.0.0",
"react-dom": ">=16.8.6 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
+2
View File
@@ -18,6 +18,7 @@
"i18next": "^24.2.1",
"iconsax-react": "^0.0.8",
"js-cookie": "^3.0.5",
"moment-jalaali": "^0.10.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-dropzone": "^14.3.5",
@@ -35,6 +36,7 @@
"devDependencies": {
"@eslint/js": "^9.17.0",
"@types/js-cookie": "^3.0.6",
"@types/moment-jalaali": "^0.7.9",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/swiper": "^5.4.3",
+8 -36
View File
@@ -1,18 +1,16 @@
import MainRouter from './router/Main'
import i18next from 'i18next'
import { I18nextProvider } from 'react-i18next'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import 'swiper/swiper-bundle.css';
import FaJson from './langs/fa.json'
import { useState } from 'react';
import { Pages } from './config/Pages';
import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
import { useEffect } from 'react';
import AuthRouter from './router/Auth';
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { IApiErrorRepsonse } from './types/error.types';
import { refreshToken } from './pages/auth/service/AuthService';
import ToastContainer from './components/Toast';
import { getRefreshToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
i18next.init({
interpolation: { escapeValue: false },
lng: 'fa',
@@ -30,23 +28,6 @@ declare global {
}
function App() {
const [isLogin, setIsLogin] = useState<'checking' | 'isLogin' | 'isNotLogin'>('checking')
useEffect(() => {
const checkLogin = async () => {
const token = await getToken();
if (token || true) {
setIsLogin('isLogin')
} else {
setIsLogin('isNotLogin')
if (window.location.href.split('auth').length === 1) {
window.location.href = Pages.auth.login
}
}
}
checkLogin();
}, []);
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: async (error: IApiErrorRepsonse) => {
@@ -105,7 +86,6 @@ function App() {
queries: {
refetchOnWindowFocus: false,
retry: false
// staleTime: 86400000 // 1 day in milliseconds
}
}
});
@@ -115,19 +95,10 @@ function App() {
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<I18nextProvider i18n={i18next}>
{
isLogin === 'checking' ?
null
:
isLogin === 'isLogin' ?
<Routes>
<Route path="/:slug/*" element={<MainRouter />} />
<Route path="/" element={<Navigate to="/fa/dashboard" replace />} />
<Route path="/auth/*" element={<AuthRouter />} />
</Routes>
:
<AuthRouter />
}
<Routes>
<Route path="/:slug/auth/*" element={<AuthRouter />} />
<Route path="/:slug/*" element={<MainRouter />} />
</Routes>
<ToastContainer />
</I18nextProvider>
</QueryClientProvider>
@@ -135,4 +106,5 @@ function App() {
</>
)
}
export default App
+3 -4
View File
@@ -1,8 +1,7 @@
export const Pages = {
auth: {
login: "auth/login",
register: "auth/register",
forgotPassword: "auth/forgot",
login: "login",
register: "register",
},
dashboard: "dashboard",
services: {
@@ -25,7 +24,7 @@ export const Pages = {
detail: "announcement/",
},
company: {
detail: "company/detail/",
detail: "company/",
},
grade: {
records: "grade-records",
+3 -1
View File
@@ -16,8 +16,10 @@ axiosInstance.interceptors.response.use(
axiosInstance.interceptors.request.use(async function (config) {
const tokenValue = await getToken();
const token = tokenValue ? tokenValue : "";
const slug = localStorage.getItem(import.meta.env.VITE_SLUG_ID);
const slug_parse = slug ? JSON.parse(slug) : null;
config.headers.Authorization = "Bearer " + token;
config.headers["x-business-id"] = slug_parse?.danakSubscriptionId;
return config;
});
+2 -3
View File
@@ -32,7 +32,7 @@ export const timeAgo = (date: string | Date): string => {
return "همین الان";
};
export const getToken = () => {
export const getToken = async () => {
return Cookie.get(import.meta.env.VITE_TOKEN_NAME);
};
@@ -40,8 +40,7 @@ export const getRefreshToken = () => {
return Cookie.get(import.meta.env.VITE_REFRESH_TOKEN_NAME);
};
const isProduction = import.meta.env.MODE === "production";
const domain = isProduction ? ".danakcorp.com" : undefined;
const domain = window.location.hostname;
export const setToken = (token: string) => {
Cookie.set(import.meta.env.VITE_TOKEN_NAME, token, { domain });
+10
View File
@@ -1,4 +1,5 @@
import { twMerge } from "tailwind-merge";
import { useParams } from "react-router-dom";
/**
* Concatenates truthy classes into a space-separated string.
@@ -9,3 +10,12 @@ import { twMerge } from "tailwind-merge";
export const clx = (...classes: (string | boolean | undefined)[]): string => {
return twMerge(classes.filter(Boolean).join(" "));
};
export const buildPath = (path: string, slug2?: string): string => {
if (slug2) {
return `/${slug2}/${path}`;
} else {
const { slug } = useParams() as { slug: string };
return `/${slug}/${path}`;
}
};
+38
View File
@@ -0,0 +1,38 @@
import { FC, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { getToken } from '../config/func'
import { buildPath } from '../helpers/utils'
// تایپ برای کامپوننت‌های React
type ComponentType = FC<any>
// HOC برای محافظت از کامپوننت‌های نیازمند لاگین
const withAuth = (WrappedComponent: ComponentType): ComponentType => {
// کامپوننت جدید که لاگین را چک می‌کند
const WithAuthComponent: FC<any> = (props) => {
const navigate = useNavigate()
const { slug } = useParams()
useEffect(() => {
const checkAuth = async () => {
const token = await getToken()
if (!token) {
const loginPath = buildPath('auth/login', slug)
navigate(loginPath)
}
}
checkAuth()
}, [navigate, slug])
// کامپوننت اصلی را با props خودش رندر کن
return <WrappedComponent {...props} />
}
// نام نمایشی کامپوننت را برای دیباگ بهتر تنظیم کن
WithAuthComponent.displayName = `WithAuth(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`
return WithAuthComponent
}
export default withAuth
+8
View File
@@ -12,6 +12,7 @@ import { Pages } from '../../../config/Pages'
import { isEmail } from '../../../config/func'
import { useCheckHasAccount, useLoginWithOtp } from '../hooks/useAuthData'
import { ErrorType } from '../../../helpers/types'
import { ArrowLeft } from 'iconsax-react'
import { toast } from '../../../components/Toast'
const LoginStep1: FC = () => {
@@ -101,6 +102,13 @@ const LoginStep1: FC = () => {
<div>{t('auth.register')}</div>
</Link>
</div>
<a href='https://accounts.danakcorp.com' target='_blank' className='mt-4 flex gap-1 text-sm justify-center text-description'>
<div>
{t('auth.old_version')}
</div>
<ArrowLeft size={20} color='black' />
</a>
</div>
)
}
+5 -8
View File
@@ -13,13 +13,16 @@ import { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData'
import { ErrorType } from '../../../helpers/types'
import { setRefreshToken } from '../../../config/func'
import { setToken } from '../../../config/func'
import { getRedirectUrl } from '../../../config/func'
import { toast } from '../../../components/Toast'
import { useParams } from 'react-router-dom'
import { buildPath } from '../../../helpers/utils'
const LoginStep2: FC = () => {
const { t } = useTranslation('global')
const { phone, setStepLogin } = useAuthStore()
const { value, reset } = useCountDown(2, true)
const { slug } = useParams()
const otpVerify = useOtpVerify()
const loginWithOtp = useLoginWithOtp()
@@ -41,13 +44,7 @@ const LoginStep2: FC = () => {
onSuccess(data) {
setToken(data?.data?.accessToken?.token)
setRefreshToken(data?.data?.refreshToken?.token)
// Check if there's a redirect parameter in the URL
const redirectUrl = getRedirectUrl();
if (redirectUrl) {
window.location.href = redirectUrl;
return; // Prevent the default navigation
}
window.location.href = Pages.dashboard
window.location.href = buildPath(Pages.dashboard, slug)
},
onError(error: ErrorType) {
toast(error?.response?.data?.error?.message[0], 'error')
+1 -1
View File
@@ -11,11 +11,11 @@ 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 '../../../components/Toast'
import { ErrorType } from '../../../helpers/types'
import { setRefreshToken } from '../../../config/func'
import { setToken } from '../../../config/func'
import { getRedirectUrl } from '../../../config/func'
import { toast } from '../../../components/Toast'
const LoginStep3: FC = () => {
+2 -3
View File
@@ -8,7 +8,7 @@ import { useAuthStore } from '../store/AuthStore'
import Error from '../../../components/Error'
import Button from '../../../components/Button'
import { useCheckHasAccountRegister, useLoginWithOtp } from '../hooks/useAuthData'
import { toast } from '../../../components/Toast'
import { toast } from 'react-toastify'
import { ErrorType } from '../../../helpers/types'
const RegisterStep1: FC = () => {
@@ -33,13 +33,12 @@ const RegisterStep1: FC = () => {
setStepLogin(2)
},
onError(error: ErrorType) {
toast(error?.response?.data?.error?.message[0], 'error')
toast.error(error?.response?.data?.error?.message[0])
},
})
},
})
return (
<div className='flex-1'>
+5 -7
View File
@@ -13,12 +13,14 @@ import { toast } from 'react-toastify'
import { Pages } from '../../../config/Pages'
import { setToken } from '../../../config/func'
import { setRefreshToken } from '../../../config/func'
import { getRedirectUrl } from '../../../config/func'
import { useParams } from 'react-router-dom'
const RegisterStep2: FC = () => {
const { t } = useTranslation('global')
const { phone } = useAuthStore()
const register = useRegister()
const { slug } = useParams()
const formik = useFormik<RegisterType>({
initialValues: {
@@ -51,12 +53,7 @@ const RegisterStep2: FC = () => {
onSuccess(data) {
setToken(data?.data?.accessToken?.token)
setRefreshToken(data?.data?.refreshToken?.token)
const redirectUrl = getRedirectUrl();
if (redirectUrl) {
window.location.href = redirectUrl;
return; // Prevent the default navigation
}
window.location.href = Pages.dashboard
window.location.href = `/${slug}/${Pages.dashboard}`
},
onError(error: ErrorType) {
toast.error(error?.response?.data?.error?.message[0])
@@ -65,6 +62,7 @@ const RegisterStep2: FC = () => {
},
})
return (
<Fragment>
<div className='mt-5 rowTwoInput'>
+19 -49
View File
@@ -1,22 +1,29 @@
import { Calendar, CallCalling, Location, Map1, MessageMinus, Profile, Setting2, Sms } from 'iconsax-react'
import { FC } from 'react'
import { useParams } from 'react-router-dom'
import { useGetCompanyDetail } from '../home/hooks/useHomeData'
import moment from 'moment-jalaali'
import Products from './components/Products'
const CompanyDetail: FC = () => {
const { id } = useParams()
const { data } = useGetCompanyDetail(id || '')
return (
<div className='mt-4 pb-20'>
<img
src='https://picsum.photos/200/300'
src={data?.data?.company?.coverImageUrl}
className='w-full h-[220px] rounded-xl'
/>
<div className='p-6 bg-white rounded-xl -mt-4 relative'>
<div className='flex gap-3'>
<img
src='https://picsum.photos/200/300'
src={data?.data?.company?.profileImageUrl}
className='size-16 rounded-xl object-cover'
/>
<div className='flex-1 items-center flex justify-between '>
<div className='text-sm'>
شرکت شماره ۱
{data?.data?.company?.name}
</div>
<div className='flex items-center gap-1.5 text-xs'>
<Map1
@@ -38,7 +45,7 @@ const CompanyDetail: FC = () => {
color='black'
/>
<div>
مدیرعامل: مهرداد مظفری
مدیرعامل: {data?.data?.company?.user?.fullName}
</div>
</div>
<div className='flex gap-1 items-center'>
@@ -47,7 +54,7 @@ const CompanyDetail: FC = () => {
color='black'
/>
<div>
صنعت : نفت و گاز
صنعت : {data?.data?.company?.industry?.title}
</div>
</div>
</div>
@@ -58,7 +65,7 @@ const CompanyDetail: FC = () => {
color='black'
/>
<div>
شماره تماس : ۰۸۶۳۲۳۴۵۶۷۸
شماره تماس : {data?.data?.company?.user?.phone}
</div>
</div>
<div className='flex gap-1 items-center'>
@@ -67,7 +74,7 @@ const CompanyDetail: FC = () => {
color='black'
/>
<div>
شناسه ملی : ۱۲۳۴
شناسه ملی : {data?.data?.company?.identificationNumber}
</div>
</div>
</div>
@@ -78,7 +85,7 @@ const CompanyDetail: FC = () => {
color='black'
/>
<div>
ایمیل : info@example.com
ایمیل : {data?.data?.company?.user?.email}
</div>
</div>
<div className='flex gap-1 items-center'>
@@ -87,7 +94,7 @@ const CompanyDetail: FC = () => {
color='black'
/>
<div>
تاریخ تاسیس : ۱۳۸۹
تاریخ تاسیس : {moment(data?.data?.company?.dateOfEstablishment, 'jYYYY/jMM/jDD').format('jYYYY/jMM/jDD')}
</div>
</div>
</div>
@@ -98,50 +105,13 @@ const CompanyDetail: FC = () => {
color='black'
/>
<div>
آدرس : لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ
آدرس : {data?.data?.company?.address}
</div>
</div>
</div>
</div>
<div className='mt-6 flex justify-center gap-2 items-center'>
<div className='flex h-8 bg-white px-6 items-center rounded-full text-xs border-2 border-white pt-[1px]'>
محصولات
</div>
<div className='flex h-8 bg-white px-6 items-center rounded-full text-xs border-2 border-white bg-opacity-45 pt-[1px]'>
خدمات
</div>
<div className='flex h-8 bg-white px-6 items-center rounded-full text-xs border-2 border-white bg-opacity-45 pt-[1px]'>
گالری
</div>
</div>
<div className='flex gap-2 flex-wrap mt-6'>
<div className='flex-1 gap-2.5 bg-white min-w-[28%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-12 rounded-xl bg-red-100'></div>
<div className='text-xs'>
<div>
شیر صنعتی
</div>
</div>
</div>
<div className='flex-1 gap-2.5 bg-white min-w-[28%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-12 rounded-xl bg-red-100'></div>
<div className='text-xs'>
<div>
شیر صنعتی
</div>
</div>
</div>
<div className='flex-1 gap-2.5 bg-white min-w-[28%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-12 rounded-xl bg-red-100'></div>
<div className='text-xs'>
<div>
شیر صنعتی
</div>
</div>
</div>
</div>
<Products />
</div>
)
}
+70
View File
@@ -0,0 +1,70 @@
import { FC, useState } from 'react'
import { useGetCompanyDetail } from '../../home/hooks/useHomeData'
import { useParams } from 'react-router-dom'
import { ProductItemType } from '../types/CompanyTypes'
const Products: FC = () => {
const { id } = useParams()
const { data } = useGetCompanyDetail(id || '')
const [activeTab, setActiveTab] = useState<'products' | 'services' | 'gallery'>('products')
return (
<div>
<div className='mt-6 flex justify-center gap-2 items-center'>
<div className={`flex cursor-pointer h-8 bg-white px-6 items-center rounded-full text-xs border-2 border-white pt-[1px] ${activeTab === 'products' ? 'bg-opacity-100' : 'bg-opacity-45'}`} onClick={() => setActiveTab('products')}>
محصولات
</div>
<div className={`flex cursor-pointer h-8 bg-white px-6 items-center rounded-full text-xs border-2 border-white pt-[1px] ${activeTab === 'services' ? 'bg-opacity-100' : 'bg-opacity-45'}`} onClick={() => setActiveTab('services')}>
خدمات
</div>
{/* <div className={`flex cursor-pointer h-8 bg-white px-6 items-center rounded-full text-xs border-2 border-white pt-[1px] ${activeTab === 'gallery' ? 'bg-opacity-100' : 'bg-opacity-45'}`} onClick={() => setActiveTab('gallery')}>
گالری
</div> */}
</div>
{
activeTab === 'products' && (
<div className='flex gap-2 flex-wrap mt-6'>
{
data?.data?.company?.products?.map((item: ProductItemType) => (
<div className='flex-1 gap-2.5 bg-white min-w-[28%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-12 rounded-xl overflow-hidden'>
<img src={item.imageUrl} alt={item.title} className='w-full h-full object-cover' />
</div>
<div className='text-xs'>
<div>
{item.title}
</div>
</div>
</div>
))
}
</div>
)
}
{
activeTab === 'services' && (
<div className='flex gap-2 flex-wrap mt-6'>
{
data?.data?.company?.services?.map((item: ProductItemType) => (
<div className='flex-1 gap-2.5 bg-white min-w-[28%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-12 rounded-xl overflow-hidden'>
<img src={item.imageUrl} alt={item.title} className='w-full h-full object-cover' />
</div>
<div className='text-xs'>
<div>
{item.title}
</div>
</div>
</div>
))
}
</div>
)
}
</div>
)
}
export default Products
+5
View File
@@ -0,0 +1,5 @@
export type ProductItemType = {
id: string;
imageUrl: string;
title: string;
};
+133 -170
View File
@@ -1,171 +1,134 @@
import { Building, InfoCircle } from 'iconsax-react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { clx } from '../../helpers/utils'
import StatusWithText from '../../components/StatusWithText'
import TitleSeeAll from '../../components/TitleSeeAll'
const Home: FC = () => {
const { t } = useTranslation('global')
return (
<div className='mt-4'>
<div className='w-full h-10 flex gap-2 items-center px-4 rounded-xl bg-[#FCE7CE] text-[#FF7B00]'>
<InfoCircle
size={24}
color='#FF7B00'
/>
<div className='text-xs'>
درخواست شما برای ثبت شرکت، هنوز تایید نشده است
</div>
</div>
<div className='mt-6'>
<TitleSeeAll
title={t('home.last_announcement')}
link='/'
/>
</div>
<div className='mt-4'>
<div className={clx(
'p-4 rounded-2xl',
'bg-[#F8F9FB]'
)}>
<div className='flex justify-between'>
<div className='leading-6 text-xs xl:text-[12.5px] max-w-[70%]'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ
</div>
<div className='flex gap-1'>
<StatusWithText
variant='error'
text={t('announcement.important')}
/>
<StatusWithText
variant='success'
text={t('announcement.new')}
/>
</div>
</div>
<div className='m-2 flex justify-between items-center text-[11px]'>
<div className='max-w-[70%] truncate text-description'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ
</div>
<div className='text-description'>
۱۴۰۳/۰۹/۲۴
</div>
</div>
</div>
</div>
<div className='mt-6'>
<TitleSeeAll
title={t('home.companies')}
link='/'
/>
</div>
<div className='flex gap-2 mt-4 overflowX'>
<div className='h-9 bg-white rounded-full px-5 flex gap-2 items-center'>
<Building
size={20}
color='black'
/>
<div className='text-sm'>
همه
</div>
</div>
<div className='h-9 bg-white rounded-full px-5 flex gap-2 items-center'>
<Building
size={20}
color='black'
/>
<div className='text-sm'>
همه
</div>
</div>
<div className='h-9 bg-white rounded-full px-5 flex gap-2 items-center'>
<Building
size={20}
color='black'
/>
<div className='text-sm'>
همه
</div>
</div>
<div className='h-9 bg-white rounded-full px-5 flex gap-2 items-center'>
<Building
size={20}
color='black'
/>
<div className='text-sm'>
همه
</div>
</div>
<div className='h-9 bg-white rounded-full px-5 flex gap-2 items-center'>
<Building
size={20}
color='black'
/>
<div className='text-sm'>
همه
</div>
</div>
</div>
<div className='mt-6 flex gap-4 flex-wrap'>
<div className='flex-1 gap-2.5 bg-white min-w-[40%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-[64px] rounded-xl bg-red-100'></div>
<div className='text-xs'>
<div>
شرکت
</div>
<div className='text-description mt-2'>
شهرک شماره ۱
</div>
</div>
</div>
<div className='flex-1 gap-2.5 bg-white min-w-[40%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-[64px] rounded-xl bg-red-100'></div>
<div className='text-xs'>
<div>
هنرمند
</div>
<div className='text-description mt-2'>
درامز
</div>
</div>
</div>
<div className='flex-1 gap-2.5 bg-white min-w-[40%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-[64px] rounded-xl bg-red-100'></div>
<div className='text-xs'>
<div>
هنرمند
</div>
<div className='text-description mt-2'>
درامز
</div>
</div>
</div>
<div className='flex-1 gap-2.5 bg-white min-w-[40%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-[64px] rounded-xl bg-red-100'></div>
<div className='text-xs'>
<div>
هنرمند
</div>
<div className='text-description mt-2'>
درامز
</div>
</div>
</div>
</div>
</div>
)
}
import { Building, InfoCircle } from 'iconsax-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { buildPath, clx } from '../../helpers/utils'
import StatusWithText from '../../components/StatusWithText'
import TitleSeeAll from '../../components/TitleSeeAll'
import { useGetCompanies, useGetIndustry } from './hooks/useHomeData'
import { CompanyItemType, IndustryItemType } from './types/HomeTypes'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
const Home: FC = () => {
const { t } = useTranslation('global')
const [activeIndustry, setActiveIndustry] = useState<string>('')
const { data: industry } = useGetIndustry()
const { data: companies } = useGetCompanies(activeIndustry)
return (
<div className='mt-4'>
<div className='w-full h-10 flex gap-2 items-center px-4 rounded-xl bg-[#FCE7CE] text-[#FF7B00]'>
<InfoCircle
size={24}
color='#FF7B00'
/>
<div className='text-xs'>
درخواست شما برای ثبت شرکت، هنوز تایید نشده است
</div>
</div>
<div className='mt-6'>
<TitleSeeAll
title={t('home.last_announcement')}
link='/'
/>
</div>
<div className='mt-4'>
<div className={clx(
'p-4 rounded-2xl',
'bg-[#F8F9FB]'
)}>
<div className='flex justify-between'>
<div className='leading-6 text-xs xl:text-[12.5px] max-w-[70%]'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ
</div>
<div className='flex gap-1'>
<StatusWithText
variant='error'
text={t('announcement.important')}
/>
<StatusWithText
variant='success'
text={t('announcement.new')}
/>
</div>
</div>
<div className='m-2 flex justify-between items-center text-[11px]'>
<div className='max-w-[70%] truncate text-description'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ
</div>
<div className='text-description'>
۱۴۰۳/۰۹/۲۴
</div>
</div>
</div>
</div>
<div className='mt-6'>
<TitleSeeAll
title={t('home.companies')}
link='/'
/>
</div>
<div className='flex gap-2 mt-4 overflowX'>
<div className={clx(
'h-9 bg-white rounded-full px-5 flex gap-2 items-center cursor-pointer',
activeIndustry === '' ? 'bg-opacity-100' : 'bg-opacity-50'
)} onClick={() => setActiveIndustry('')}>
<Building
size={20}
color='black'
/>
<div className='text-sm'>
همه
</div>
</div>
{
industry?.data?.industries?.map((item: IndustryItemType) => {
return (
<div key={item.id} className={clx(
'h-9 bg-white rounded-full px-5 flex gap-2 items-center cursor-pointer',
activeIndustry === item.id ? 'bg-opacity-100' : 'bg-opacity-50'
)} onClick={() => setActiveIndustry(item.id)}>
<img src={item.iconUrl} alt={item.title} className='size-5' />
<div className='text-sm'>
{item.title}
</div>
</div>
)
})
}
</div>
<div className='mt-6 flex gap-4 flex-wrap'>
{
companies?.data?.companies?.map((item: CompanyItemType) => {
return (
<Link to={buildPath(Pages.company.detail + item.id)} key={item.id} className='flex-1 gap-2.5 bg-white min-w-[40%] h-20 flex items-center p-2 rounded-xl'>
<div className='size-[64px] rounded-xl overflow-hidden'>
<img src={item.profileImageUrl} alt={item.name} className='size-full object-cover' />
</div>
<div className='text-xs'>
<div>
{item.name}
</div>
<div className='text-description mt-2'>
{item.address}
</div>
</div>
</Link>
)
})
}
</div>
</div>
)
}
export default Home
+35
View File
@@ -0,0 +1,35 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import {
getCompanies,
getIdWithSlug,
getIndustry,
getCompanyDetail,
} from "../service/HomeService";
export const useGetSlugId = () => {
return useMutation({
mutationFn: (slug: string) => getIdWithSlug(slug),
});
};
export const useGetIndustry = () => {
return useQuery({
queryKey: ["industry"],
queryFn: () => getIndustry(),
});
};
export const useGetCompanies = (industryId: string) => {
return useQuery({
queryKey: ["companies", industryId],
queryFn: () => getCompanies(industryId),
});
};
export const useGetCompanyDetail = (id: string) => {
return useQuery({
queryKey: ["company", id],
queryFn: () => getCompanyDetail(id),
enabled: !!id,
});
};
+27
View File
@@ -0,0 +1,27 @@
import axios from "../../../config/axios";
export const getIdWithSlug = async (slug: string) => {
const { data } = await axios.get(`/business/slug/${slug}`);
return data;
};
export const getIndustry = async () => {
const { data } = await axios.get(`/industries/list/public`);
return data;
};
export const getCompanies = async (industryId: string) => {
const params = new URLSearchParams();
if (industryId) {
params.append("industryId", industryId);
}
const { data } = await axios.get(
`/companies/list/public?${params.toString()}`
);
return data;
};
export const getCompanyDetail = async (id: string) => {
const { data } = await axios.get(`/companies/public/${id}`);
return data;
};
+33
View File
@@ -0,0 +1,33 @@
export type IndustryItemType = {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
title: string;
iconUrl: string;
isActive: boolean;
business: string;
companiesCount: number;
};
export type CompanyItemType = {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
name: string;
identificationNumber: string;
dateOfEstablishment: string;
address: string;
mapAddressLink: string;
description: string;
websiteUrl: string;
profileImageUrl: string;
coverImageUrl: string;
isActive: boolean;
status: string;
industry: IndustryItemType;
user: string;
business: string;
invoiceCount: number;
};
+2 -1
View File
@@ -9,6 +9,7 @@ import Select from '../../components/Select'
import Textarea from '../../components/Textarea'
import { useGetProfile } from './hooks/useProfileData'
import Username from './components/Username'
import withAuth from '../../hoc/withAuth'
const Profile: FC = () => {
@@ -171,4 +172,4 @@ const Profile: FC = () => {
)
}
export default Profile
export default withAuth(Profile)
+111 -111
View File
@@ -1,112 +1,112 @@
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Tabs from '../../components/Tabs'
import { Calendar, CloseCircle, Eye, FolderOpen, MoneyChange, WalletCheck, WalletRemove, WalletSearch } from 'iconsax-react'
import Td from '../../components/Td'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
const ReceiptsList: FC = () => {
const { t } = useTranslation('global')
const [activeTab, setActiveTab] = useState<string>('unpaid')
return (
<div className='flex justify-center'>
<div className='mt-4 w-full max-w-[1200px]'>
<div>
{t('receip.Language_learners_list')}
</div>
<div className='mt-14'>
<Tabs
active={activeTab}
items={[
{
icon: <WalletRemove color={activeTab === 'unpaid' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.dont_pay'),
value: 'unpaid'
},
{
icon: <WalletSearch color={activeTab === 'not_confimed' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.dont_confrim'),
value: 'not_confimed'
},
{
icon: <WalletCheck color={activeTab === 'paid' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.paid'),
value: 'paid'
},
{
icon: <CloseCircle color={activeTab === 'canceled' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.canceled'),
value: 'canceled'
},
{
icon: <MoneyChange color={activeTab === 'returned' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.returned'),
value: 'returned'
},
{
icon: <Calendar color={activeTab === 'time' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.its_time'),
value: 'time'
},
{
icon: <FolderOpen color={activeTab === 'archive' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.archive'),
value: 'archive'
},
]}
onChange={setActiveTab}
/>
</div>
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
<table className='w-full text-sm '>
<thead className='thead'>
<tr>
<Td text={t('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={t('ticket.detail')} />
<Td text={''} />
</tr>
</thead>
<tbody>
<tr className='tr'>
<Td text={t('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={t('ticket.detail')} />
<Td text={''}>
<Link to={Pages.receipts.detail + '1'}>
<Eye size={20} color='black' />
</Link>
</Td>
</tr>
<tr className='tr'>
<Td text={t('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={t('ticket.detail')} />
<Td text={''} />
</tr>
</tbody>
</table>
</div>
</div>
</div>
)
}
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Tabs from '../../components/Tabs'
import { Calendar, CloseCircle, Eye, FolderOpen, MoneyChange, WalletCheck, WalletRemove, WalletSearch } from 'iconsax-react'
import Td from '../../components/Td'
import { Link } from 'react-router-dom'
import { Pages } from '../../config/Pages'
const ReceiptsList: FC = () => {
const { t } = useTranslation('global')
const [activeTab, setActiveTab] = useState<string>('unpaid')
return (
<div className='flex justify-center'>
<div className='mt-4 w-full max-w-[1200px]'>
<div>
{t('receip.Language_learners_list')}
</div>
<div className='mt-14'>
<Tabs
active={activeTab}
items={[
{
icon: <WalletRemove color={activeTab === 'unpaid' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.dont_pay'),
value: 'unpaid'
},
{
icon: <WalletSearch color={activeTab === 'not_confimed' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.dont_confrim'),
value: 'not_confimed'
},
{
icon: <WalletCheck color={activeTab === 'paid' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.paid'),
value: 'paid'
},
{
icon: <CloseCircle color={activeTab === 'canceled' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.canceled'),
value: 'canceled'
},
{
icon: <MoneyChange color={activeTab === 'returned' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.returned'),
value: 'returned'
},
{
icon: <Calendar color={activeTab === 'time' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.its_time'),
value: 'time'
},
{
icon: <FolderOpen color={activeTab === 'archive' ? 'black' : '#8C90A3'} size={22} />,
label: t('receip.archive'),
value: 'archive'
},
]}
onChange={setActiveTab}
/>
</div>
<div className='relative overflow-x-auto rounded-3xl mt-9 w-full'>
<table className='w-full text-sm '>
<thead className='thead'>
<tr>
<Td text={t('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={t('ticket.detail')} />
<Td text={''} />
</tr>
</thead>
<tbody>
<tr className='tr'>
<Td text={t('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={t('ticket.detail')} />
<Td text={''}>
<Link to={Pages.receipts.detail + '1'}>
<Eye size={20} color='black' />
</Link>
</Td>
</tr>
<tr className='tr'>
<Td text={t('ticket.number')} />
<Td text={t('ticket.title')} />
<Td text={t('ticket.team')} />
<Td text={t('ticket.date')} />
<Td text={t('ticket.status')} />
<Td text={t('ticket.priority')} />
<Td text={t('ticket.detail')} />
<Td text={''} />
</tr>
</tbody>
</table>
</div>
</div>
</div>
)
}
export default ReceiptsList
+3 -1
View File
@@ -5,6 +5,7 @@ import Tabs from '../../components/Tabs'
import Online from './components/Online'
import CardtoCard from './components/CardtoCard'
import Sheba from './components/Sheba'
import withAuth from '../../hoc/withAuth'
const Wallet: FC = () => {
@@ -90,4 +91,5 @@ const Wallet: FC = () => {
)
}
export default Wallet
// استفاده از HOC با آپشن‌های سفارشی
export default withAuth(Wallet)
+80 -53
View File
@@ -1,54 +1,81 @@
import Header from "../shared/Header"
import SideBar from "../shared/SideBar"
import '../assets/fonts/irancell/style.css'
import { Route, Routes } from 'react-router-dom'
import Home from '../pages/home/Home'
import ReceiptsList from '../pages/receipts/List'
import AddCriticisms from '../pages/criticisms/Add'
import Footer from "../shared/Footer"
import GradeRecordes from "../pages/grade/records"
import Certificate from "../pages/certificate/List"
import AdmitCard from "../pages/admitCard/List"
import MyCourses from "../pages/my-courses/List"
import Payment from "../pages/payment/Details"
import Wallet from "../pages/wallet/Wallet"
import ReceiptsDetail from "../pages/receipts/Detail"
import ReportCardList from "../pages/ReportCard/List"
import TransactionList from "../pages/transaction/List"
import Profile from "../pages/profile/Profile"
import CompanyDetail from "../pages/company/Detail"
const MainRouter = () => {
return (
<div className='p-4 overflow-hidden'>
<SideBar />
<Header />
<div className='flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]'>
<div className={`hide-scroll-bar overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`}>
<div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="dashboard" element={<Home />} />
<Route path="receipts" element={<ReceiptsList />} />
<Route path="receipts/:id" element={<ReceiptsDetail />} />
<Route path="company/:id" element={<CompanyDetail />} />
<Route path="criticisms" element={<AddCriticisms />} />
<Route path="grade/records" element={<GradeRecordes />} />
<Route path="transactions" element={<TransactionList />} />
<Route path="certificate" element={<Certificate />} />
<Route path="admitCard" element={<AdmitCard />} />
<Route path="myCourses" element={<MyCourses />} />
<Route path="payment" element={<Payment />} />
<Route path="wallet" element={<Wallet />} />
<Route path="reportCard/list" element={<ReportCardList />} />
<Route path="profile" element={<Profile />} />
</Routes>
</div>
</div>
</div>
<Footer />
</div>
)
}
import Header from "../shared/Header"
import SideBar from "../shared/SideBar"
import '../assets/fonts/irancell/style.css'
import { Route, Routes, useParams } from 'react-router-dom'
import Home from '../pages/home/Home'
import ReceiptsList from '../pages/receipts/List'
import AddCriticisms from '../pages/criticisms/Add'
import Footer from "../shared/Footer"
import GradeRecordes from "../pages/grade/records"
import Certificate from "../pages/certificate/List"
import AdmitCard from "../pages/admitCard/List"
import MyCourses from "../pages/my-courses/List"
import Payment from "../pages/payment/Details"
import Wallet from "../pages/wallet/Wallet"
import ReceiptsDetail from "../pages/receipts/Detail"
import ReportCardList from "../pages/ReportCard/List"
import TransactionList from "../pages/transaction/List"
import Profile from "../pages/profile/Profile"
import CompanyDetail from "../pages/company/Detail"
import { useEffect } from "react"
import { useGetSlugId } from "../pages/home/hooks/useHomeData"
import { ErrorType } from "../helpers/types"
import { toast } from "react-toastify"
const MainRouter = () => {
const { slug } = useParams()
const getSlugId = useGetSlugId()
useEffect(() => {
if (slug) {
const slug_id = localStorage.getItem(import.meta.env.VITE_SLUG_ID)
const slug_parse = slug_id ? JSON.parse(slug_id) : null
if (slug_parse && slug_parse.slug === slug) {
return
} else {
getSlugId.mutate(slug, {
onSuccess: (data) => {
localStorage.setItem(import.meta.env.VITE_SLUG_ID, JSON.stringify(data?.data?.business))
window.location.reload()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
window.location.href = 'https://danakcorp.com'
}
})
}
}
}, [slug])
return (
<div className='p-4 overflow-hidden'>
<SideBar />
<Header />
<div className='flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]'>
<div className={`hide-scroll-bar overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`}>
<div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="dashboard" element={<Home />} />
<Route path="receipts" element={<ReceiptsList />} />
<Route path="receipts/:id" element={<ReceiptsDetail />} />
<Route path="company/:id" element={<CompanyDetail />} />
<Route path="criticisms" element={<AddCriticisms />} />
<Route path="grade/records" element={<GradeRecordes />} />
<Route path="transactions" element={<TransactionList />} />
<Route path="certificate" element={<Certificate />} />
<Route path="admitCard" element={<AdmitCard />} />
<Route path="myCourses" element={<MyCourses />} />
<Route path="payment" element={<Payment />} />
<Route path="wallet" element={<Wallet />} />
<Route path="reportCard/list" element={<ReportCardList />} />
<Route path="profile" element={<Profile />} />
</Routes>
</div>
</div>
</div>
<Footer />
</div>
)
}
export default MainRouter
+50 -50
View File
@@ -1,51 +1,51 @@
import { FC } from 'react'
import Input from '../components/Input'
import { ArrowDown2, Element3, HambergerMenu, Wallet } from 'iconsax-react'
import AvatarImage from '../assets/images/Avatar.png'
import { useTranslation } from 'react-i18next'
import { Link, useParams } from 'react-router-dom'
import { Pages } from '../config/Pages'
import Notifications from '../pages/notification/Notification'
import { useSharedStore } from './store/sharedStore'
const Header: FC = () => {
const { t } = useTranslation('global')
const { setOpenSidebar, openSidebar } = useSharedStore()
const { slug } = useParams()
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-3 items-center'>
<Element3 color='black' className='xl:size-[18px] size-4' />
<Link to={`/${slug}/${Pages.wallet}`}>
<Wallet className='xl:size-[18px] size-4' color='black' />
</Link>
<Notifications />
<div className='flex gap-2 items-center'>
<div className='size-7 rounded-full bg-description overflow-hidden'>
<Link to={`/${slug}/${Pages.profile}`}>
<img src={AvatarImage} className='size-full object-cover' />
</Link>
</div>
<div className='xl:flex hidden gap-1 items-center'>
<div className='text-xs'>مهرداد مظفری</div>
<ArrowDown2 size={14} color='#8C90A3' />
</div>
</div>
</div>
</div>
)
}
import { FC } from 'react'
import Input from '../components/Input'
import { ArrowDown2, Element3, HambergerMenu, Wallet } from 'iconsax-react'
import AvatarImage from '../assets/images/Avatar.png'
import { useTranslation } from 'react-i18next'
import { Link, useParams } from 'react-router-dom'
import { Pages } from '../config/Pages'
import Notifications from '../pages/notification/Notification'
import { useSharedStore } from './store/sharedStore'
const Header: FC = () => {
const { t } = useTranslation('global')
const { setOpenSidebar, openSidebar } = useSharedStore()
const { slug } = useParams()
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-3 items-center'>
<Element3 color='black' className='xl:size-[18px] size-4' />
<Link to={`/${slug}/${Pages.wallet}`}>
<Wallet className='xl:size-[18px] size-4' color='black' />
</Link>
<Notifications />
<div className='flex gap-2 items-center'>
<div className='size-7 rounded-full bg-description overflow-hidden'>
<Link to={`/${slug}/${Pages.profile}`}>
<img src={AvatarImage} className='size-full object-cover' />
</Link>
</div>
<div className='xl:flex hidden gap-1 items-center'>
<div className='text-xs'>مهرداد مظفری</div>
<ArrowDown2 size={14} color='#8C90A3' />
</div>
</div>
</div>
</div>
)
}
export default Header
+44 -44
View File
@@ -1,45 +1,45 @@
import { FC, ReactNode } from 'react'
import { Link, useParams } from 'react-router-dom'
import { clx } from '../helpers/utils'
import { Pages } from '../config/Pages'
type Props = {
icon: ReactNode,
title: string,
isActive: boolean,
link: string, // مقدار مثل: "dashboard" یا "wallet"
isLogout?: boolean,
}
const SideBarItem: FC<Props> = (props: Props) => {
const { slug } = useParams(); // دریافت slug از URL
const fullLink = props.isLogout ? Pages.auth.login : `/${slug}/${props.link.replace(/^\//, '')}`;
const handleLogout = () => {
localStorage.removeItem(import.meta.env.VITE_TOKEN_NAME)
window.location.href = Pages.auth.login
}
// alert(slug)
return (
<Link
onClick={props.isLogout ? handleLogout : undefined}
to={fullLink}
className='flex text-xs gap-9 mt-4'
>
<div className={clx(
'w-1 bg-black h-6',
!props.isActive && 'invisible'
)}></div>
<div className='flex gap-3 items-center'>
{props.icon}
<div className={props.isActive ? 'text-black' : ''}>
{props.title}
</div>
</div>
</Link>
)
}
import { FC, ReactNode } from 'react'
import { Link, useParams } from 'react-router-dom'
import { clx } from '../helpers/utils'
import { Pages } from '../config/Pages'
type Props = {
icon: ReactNode,
title: string,
isActive: boolean,
link: string, // مقدار مثل: "dashboard" یا "wallet"
isLogout?: boolean,
}
const SideBarItem: FC<Props> = (props: Props) => {
const { slug } = useParams(); // دریافت slug از URL
const fullLink = props.isLogout ? Pages.auth.login : `/${slug}/${props.link.replace(/^\//, '')}`;
const handleLogout = () => {
localStorage.removeItem(import.meta.env.VITE_TOKEN_NAME)
window.location.href = Pages.auth.login
}
// alert(slug)
return (
<Link
onClick={props.isLogout ? handleLogout : undefined}
to={fullLink}
className='flex text-xs gap-9 mt-4'
>
<div className={clx(
'w-1 bg-black h-6',
!props.isActive && 'invisible'
)}></div>
<div className='flex gap-3 items-center'>
{props.icon}
<div className={props.isActive ? 'text-black' : ''}>
{props.title}
</div>
</div>
</Link>
)
}
export default SideBarItem
+2
View File
@@ -4,4 +4,6 @@ import { SharedStoreType } from "../types/SharedTypes";
export const useSharedStore = create<SharedStoreType>((set) => ({
openSidebar: false,
setOpenSidebar: (value) => set({ openSidebar: value }),
slug: "",
setSlug: (value) => set({ slug: value }),
}));
+5 -4
View File
@@ -1,5 +1,6 @@
export type SharedStoreType = {
openSidebar: boolean;
setOpenSidebar: (value: boolean) => void;
};
openSidebar: boolean;
setOpenSidebar: (value: boolean) => void;
slug: string;
setSlug: (value: string) => void;
};