Compare commits

...

10 Commits

Author SHA1 Message Date
hamid zarghami e37c42a276 update dockerfile
deploy to danak / build_and_deploy (push) Has been cancelled
2026-05-31 12:14:01 +03:30
hamid zarghami 596f27f238 fix bug 2025-11-13 11:21:37 +03:30
hamid zarghami b84ce0aa4b Add plan remaining 2025-11-13 09:56:29 +03:30
hamid zarghami 093ddbf195 add pop3 in mail server setting + optional select template 2025-09-04 11:27:43 +03:30
hamid zarghami 2658f06f1c template design bug 2025-08-31 12:39:15 +03:30
hamid zarghami fc206b3392 report bug fix 2025-08-23 15:18:07 +03:30
hamid zarghami 27eb5af7fd report bug 2025-08-23 09:52:58 +03:30
hamid zarghami 27ea922168 fix bug security 2025-08-21 12:57:48 +03:30
hamid zarghami 4eec15eb65 dashboard 2025-08-21 12:44:40 +03:30
hamid zarghami a6d7533075 sidebar item disable if domain not verified 2025-08-17 15:49:51 +03:30
28 changed files with 555 additions and 79 deletions
+6 -1
View File
@@ -6,4 +6,9 @@ VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
VITE_LOGIN_URL = 'https://console.danakcorp.com/auth/login'
VITE_CONSOLE_URL = 'https://console.danakcorp.com'
VITE_CONSOLE_URL = 'https://console.danakcorp.com'
VITE_WORKSPACE_ID = 'workspace_id'
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
VITE_HELP_URL = 'https://help.danakcorp.com'
+36 -1
View File
@@ -1,21 +1,56 @@
FROM node:22-alpine AS builder
# Change Alpine repo
RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirror.de.velop.ir/alpine|g' /etc/apk/repositories
# Configure npm registry mirror (Liara) — pnpm uses the same registry
ENV NPM_CONFIG_REGISTRY=https://package-mirror.liara.ir/repository/npm/
RUN npm config set registry https://package-mirror.liara.ir/repository/npm/
# Install tzdata to support timezone settings
RUN apk add --no-cache tzdata
RUN npm install -g corepack@latest
RUN corepack enable && corepack prepare pnpm@10 --activate
RUN pnpm config set registry https://package-mirror.liara.ir/repository/npm/
# Set the timezone to Asia/Tehran
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
WORKDIR /build
# Optional build-args (CapRover / docker build --build-arg). When unset, Vite reads .env from COPY.
ARG VITE_TOKEN_NAME
ARG VITE_REFRESH_TOKEN_NAME
ARG VITE_DANAK_BASE_URL
ARG VITE_BASE_URL
ARG VITE_SERVICE_ID
ARG VITE_LOGIN_URL
ARG VITE_CONSOLE_URL
ARG VITE_WORKSPACE_ID
ARG VITE_HELP_URL
COPY package*.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --loglevel info
COPY . ./
RUN pnpm run build
RUN set -e; \
env_args=""; \
[ -n "$VITE_TOKEN_NAME" ] && env_args="$env_args VITE_TOKEN_NAME=$VITE_TOKEN_NAME"; \
[ -n "$VITE_REFRESH_TOKEN_NAME" ] && env_args="$env_args VITE_REFRESH_TOKEN_NAME=$VITE_REFRESH_TOKEN_NAME"; \
[ -n "$VITE_DANAK_BASE_URL" ] && env_args="$env_args VITE_DANAK_BASE_URL=$VITE_DANAK_BASE_URL"; \
[ -n "$VITE_BASE_URL" ] && env_args="$env_args VITE_BASE_URL=$VITE_BASE_URL"; \
[ -n "$VITE_SERVICE_ID" ] && env_args="$env_args VITE_SERVICE_ID=$VITE_SERVICE_ID"; \
[ -n "$VITE_LOGIN_URL" ] && env_args="$env_args VITE_LOGIN_URL=$VITE_LOGIN_URL"; \
[ -n "$VITE_CONSOLE_URL" ] && env_args="$env_args VITE_CONSOLE_URL=$VITE_CONSOLE_URL"; \
[ -n "$VITE_WORKSPACE_ID" ] && env_args="$env_args VITE_WORKSPACE_ID=$VITE_WORKSPACE_ID"; \
[ -n "$VITE_HELP_URL" ] && env_args="$env_args VITE_HELP_URL=$VITE_HELP_URL"; \
eval "env$env_args pnpm run build"
FROM nginx:stable-alpine AS production-stage
# Change Alpine repo
RUN sed -i 's|https://dl-cdn.alpinelinux.org/alpine|https://mirror.de.velop.ir/alpine|g' /etc/apk/repositories
COPY --from=builder /build/dist /usr/share/nginx/html
COPY --from=builder /build/nginx.con[f] /etc/nginx/conf.d/default.conf
+4 -1
View File
@@ -15,6 +15,8 @@ import { getRefreshToken, removeRefreshToken, removeToken, setRefreshToken, setT
import { refreshToken } from './pages/auth/service/Service';
import BuySpace from './shared/components/BuySpace'
import useConvertNumbers from './hooks/useConvertNumbers'
import CheckWorkSpace from './components/CheckWorkSpace'
import ReportBug from './pages/guide/ReportBug';
declare global {
interface Window {
@@ -102,8 +104,9 @@ const App: FC = () => {
<BrowserRouter>
<I18nextProvider i18n={i18next}>
<QueryClientProvider client={queryClient}>
<CheckWorkSpace />
<Main />
<ReportBug />
<ToastContainer />
<BuySpace />
</QueryClientProvider>
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

+24
View File
@@ -0,0 +1,24 @@
import { FC, useEffect } from 'react'
import { useWorkspaces } from '@/pages/home/hooks/useHomeData'
import { workspaceItem } from '@/pages/home/types/HomeTypes'
const CheckWorkSpace: FC = () => {
const getWorkspaces = useWorkspaces()
useEffect(() => {
if (getWorkspaces.data?.data?.workspaces) {
const workspaceId = localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID)
const workspace = getWorkspaces.data?.data?.workspaces.find((item: workspaceItem) => item.id === workspaceId)
if (!workspace) {
localStorage.setItem(import.meta.env.VITE_WORKSPACE_ID, getWorkspaces.data?.data?.workspaces[0].id)
window.location.reload()
}
}
}, [getWorkspaces])
return null
}
export default CheckWorkSpace
+1 -1
View File
@@ -28,7 +28,7 @@ const DefaulModal: FC<Props> = (props: Props) => {
<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 max-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'>
<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 max-h-[85%]'>
{
+92 -64
View File
@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { More } from 'iconsax-react';
import React, { useState, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { More } from 'iconsax-react'
export interface RowActionItem {
label: string;
@@ -12,106 +12,134 @@ export interface RowActionItem {
interface RowActionsDropdownProps {
actions: RowActionItem[];
className?: string;
trigger?: React.ReactNode;
topOffset?: number;
leftOffset?: number;
topOffsetMobile?: number;
leftOffsetMobile?: number;
}
const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({ actions, className = '' }) => {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({
actions,
className = '',
trigger,
topOffset = 0,
leftOffset = 0,
topOffsetMobile = 0,
leftOffsetMobile = 0
}) => {
const [isOpen, setIsOpen] = useState(false)
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 })
const dropdownRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
setIsOpen(false)
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation();
if (!isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
const isMobile = window.innerWidth < 768;
const dropdownWidth = isMobile ? 140 : 150;
const dropdownHeight = actions.length * (isMobile ? 36 : 40); // تخمین ارتفاع منو
let left = rect.left + window.scrollX - dropdownWidth + rect.width;
let top = rect.bottom + window.scrollY;
// بررسی اینکه منو از سمت راست خارج نشود
if (left + dropdownWidth > window.innerWidth) {
left = rect.left + window.scrollX - dropdownWidth;
}
// بررسی اینکه منو از سمت چپ خارج نشود
if (left < 0) {
left = isMobile ? 20 : 40;
}
// بررسی اینکه منو از پایین خارج نشود
if (top + dropdownHeight > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - dropdownHeight;
}
setPosition({ top, left });
}
setIsOpen(!isOpen);
};
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
const calculatePosition = (rect: DOMRect) => {
const isMobile = window.innerWidth < 768
const dropdownWidth = isMobile ? 140 : 150
const dropdownHeight = actions.length * (isMobile ? 36 : 40)
// انتخاب offset بر اساس سایز صفحه
const currentTopOffset = isMobile ? topOffsetMobile : topOffset
const currentLeftOffset = isMobile ? leftOffsetMobile : leftOffset
let left = rect.left + window.scrollX - dropdownWidth + rect.width + currentLeftOffset
let top = rect.bottom + window.scrollY + currentTopOffset
if (left + dropdownWidth > window.innerWidth) {
left = rect.left + window.scrollX - dropdownWidth + currentLeftOffset
}
if (left < 0) {
left = isMobile ? 20 : 40
}
if (top + dropdownHeight > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - dropdownHeight + currentTopOffset
}
// اطمینان از اینکه dropdown از صفحه خارج نشود
if (left < 0) left = 20
if (left + dropdownWidth > window.innerWidth) left = window.innerWidth - dropdownWidth - 20
if (top < 0) top = 20
if (top + dropdownHeight > window.innerHeight) top = window.innerHeight - dropdownHeight - 20
return { top, left }
}
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation()
if (!isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect()
const calculatedPosition = calculatePosition(rect)
setDropdownPosition(calculatedPosition)
}
setIsOpen(!isOpen)
}
const handleActionClick = (action: RowActionItem) => {
action.onClick();
setIsOpen(false);
};
action.onClick()
setIsOpen(false)
}
const renderDropdown = () => {
if (!isOpen) return null;
if (!isOpen) return null
return createPortal(
<div
ref={dropdownRef}
className="fixed py-1 md:py-2 bg-white rounded-xl md:rounded-2xl shadow-lg z-[9999] min-w-[140px] md:min-w-[150px]"
className="fixed py-3 md:py-3 bg-white dark:bg-[#252526] rounded-xl md:rounded-2xl shadow-lg z-[5] min-w-[140px] md:min-w-[150px] border border-[#d0d0d0]"
style={{
top: `${position.top}px`,
left: `${position.left}px`,
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
}}
>
{actions.map((action, index) => (
<button
key={index}
onClick={() => handleActionClick(action)}
className={`w-full text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 ${index === 0 ? 'rounded-t-lg' : ''
} ${action.className || ''}`}
className={`w-full mt-1 text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 dark:hover:bg-[#2d2d30] ${index === 0 ? 'rounded-t-lg' : ''} ${action.className || ''}`}
>
{action.icon && <span className="ml-2">{action.icon}</span>}
{action.label}
<span className={`${action.label === 'حذف' ? 'text-red-500' : ''} dark:text-gray-200`}>{action.label}</span>
</button>
))}
</div>,
document.body
);
};
)
}
return (
<div className={className}>
<button
ref={buttonRef}
onClick={handleToggle}
className="p-1 hover:bg-gray-100 rounded-full transition-colors"
className="mt-2 hover:bg-gray-100 dark:hover:bg-[#2d2d30] rounded-full transition-colors"
aria-haspopup="menu"
aria-expanded={isOpen}
>
<More size={14} color="black" className="rotate-90" />
{trigger ? trigger : (
<More size={16} color={'black'} className="rotate-90" />
)}
</button>
{renderDropdown()}
</div>
);
};
)
}
export default RowActionsDropdown;
export default RowActionsDropdown
+5
View File
@@ -267,3 +267,8 @@ textarea::placeholder {
backdrop-filter: blur(44px);
}
.filterBlue {
filter: brightness(0) saturate(100%) invert(9%) sepia(100%) saturate(7246%)
hue-rotate(236deg) brightness(111%) contrast(112%);
}
+6 -1
View File
@@ -116,6 +116,10 @@
"setting_imap_description": "تنظیمات IMAP خود را با دقت وارد کنید.",
"imap_server": "IMAP سرور",
"imap_port": "IMAP پورت",
"setting_pop3": "تنظیمات POP3",
"setting_pop3_description": "تنظیمات POP3 خود را با دقت وارد کنید.",
"pop3_server": "POP3 سرور",
"pop3_port": "POP3 پورت",
"encryption": "نوع رمزنگاری",
"record_dns": "رکوردهای DNS",
"record_dns_description": "در این قسمت میتوانید وضعیت رکوردهای DNS سرویس ایمیل خود را بررسی کنید.",
@@ -159,7 +163,8 @@
"add_social": "اضافه کردن شبکه اجتماعی",
"update_social": "بروزرسانی شبکه اجتماعی",
"delete": "حذف",
"preview": "پیش نمایش"
"preview": "پیش نمایش",
"dashboard": "میزکار"
},
"save": "ذخیره",
"export_html": "خروجی HTML",
+127
View File
@@ -0,0 +1,127 @@
import React, { useMemo, useState } from 'react'
import DefaulModal from '@/components/DefaulModal'
import Textarea from '@/components/Textarea'
import Button from '@/components/Button'
import { useSharedStore } from '@/shared/store/sharedStore'
import { useSendFeedback } from './hooks/useFeedbackData'
import { toast } from '@/components/Toast'
import { ErrorType } from '@/helpers/types'
// Import images
import sad1 from '@/assets/images/sad1.svg'
import sad2 from '@/assets/images/sad2.svg'
import sad3 from '@/assets/images/sad3.svg'
import smile from '@/assets/images/smile.svg'
import smile2 from '@/assets/images/smile2.svg'
import smile3 from '@/assets/images/smile3.svg'
import { useGetProfile } from '../profile/hooks/useProfileData'
const ReportBug: React.FC = () => {
const { openReportBug, setOpenReportBug } = useSharedStore()
const [description, setDescription] = useState('')
const [feeling, setFeeling] = useState<string | null>(null)
const { data } = useGetProfile()
const { mutate: sendReportBug, isPending } = useSendFeedback()
const isValid = useMemo(() => description.trim().length > 0, [description])
const getRatingFromFeeling = (feeling: string | null): number => {
if (!feeling) return 0
const feelingMap: Record<string, number> = {
'angry': 1, // sad1
'crying': 2, // sad2
'sad': 3, // sad3
'neutral': 4, // smile
'smile': 6, // smile2
'love': 5, // smile3
}
return feelingMap[feeling] || 0
}
const handleSubmit = async () => {
if (!isValid) return
sendReportBug({
content: description,
rating: getRatingFromFeeling(feeling),
serviceId: import.meta.env.VITE_SERVICE_ID,
email: data?.data?.user?.email,
phone: data?.data?.user?.phone,
}, {
onSuccess: () => {
toast('گزارش با موفقیت ارسال شد', 'success')
setDescription('')
setFeeling(null)
setOpenReportBug(false)
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error?.message[0], 'error')
}
})
}
const feelings = [
{ id: 'neutral', image: smile, alt: 'خنثی' },
{ id: 'smile', image: smile2, alt: 'راضی' },
{ id: 'love', image: smile3, alt: 'راضی' },
{ id: 'angry', image: sad1, alt: 'ناراضی' },
{ id: 'crying', image: sad2, alt: 'ناراضی' },
{ id: 'sad', image: sad3, alt: 'ناراضی' },
]
return (
<DefaulModal open={openReportBug} close={() => setOpenReportBug(false)} isHeader title_header='گزارش اشکالات' width={500}>
<div className='space-y-6 mt-4'>
<div className='text-xs text-muted-foreground leading-5'>
لطفاً در صورت مشاهده هرگونه اشکال یا باگ در سایت، از طریق این فرم گزارش دهید. همچنین میتوانید بازخورد، نظر یا پیشنهادات خود را درباره سایت با ما به اشتراک بگذارید. اطلاعات دقیق شما به ما کمک میکند مشکلات را سریعتر برطرف کنیم و کیفیت سایت را بهبود بخشیم.
</div>
<Textarea
label='متن گزارش'
placeholder='اینجا بنویسید...'
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<div className='flex justify-start'>
<div className='flex w-full justify-between gap-4'>
{feelings.map((f, idx) => (
<div key={f.id} className='flex flex-col items-center gap-1'>
<button
type='button'
onClick={() => setFeeling(f.id)}
className={`p-1 rounded-lg transition-all`}
>
<img
src={f.image}
alt={f.alt}
className={`w-8 object-contain ${feeling === f.id ? 'filterBlue' : ''}`}
/>
</button>
{idx === 0 && (
<div className='text-xs mt-2 text-muted-foreground'>راضی</div>
)}
{idx === feelings.length - 1 && (
<div className='text-xs mt-2 text-muted-foreground'>ناراضی</div>
)}
</div>
))}
</div>
</div>
<div className='text-[11px] text-muted-foreground leading-6'>
با ادامه و ارسال، شما تأیید میکنید که «دیمیل» میتواند پاسخها و اطلاعات حساب شما را بهمنظور بهبود خدمات جمعآوری و استفاده کند. برای جزئیات بیشتر لطفاً به <a target='_blank' href='https://danakcorp.com/conditions' className='text-blue-500'>قوانین و شرایط و سیاست حریم خصوصی</a> مراجعه کنید.
</div>
<div className='w-full flex justify-end'>
<Button onClick={handleSubmit} disabled={!isValid} loading={isPending} className='px-10 w-fit'>ارسال</Button>
</div>
</div>
</DefaulModal>
)
}
export default ReportBug
+9
View File
@@ -0,0 +1,9 @@
import { useMutation } from "@tanstack/react-query";
import { sendFeedback } from "../service/Service";
import { FeedbackType } from "../types/Types";
export const useSendFeedback = () => {
return useMutation({
mutationFn: (params: FeedbackType) => sendFeedback(params),
});
};
+7
View File
@@ -0,0 +1,7 @@
import axios from "@/config/axiosDanak";
import { FeedbackType } from "../types/Types";
export const sendFeedback = async (params: FeedbackType) => {
const { data } = await axios.post("/danak-services/feedback", params);
return data;
};
+7
View File
@@ -0,0 +1,7 @@
export type FeedbackType = {
content: string;
rating: number;
serviceId: string;
phone?: string;
email?: string;
};
@@ -88,8 +88,10 @@ const AddressForm: FC<AddressFormProps> = ({ selectedAddress, isEditMode, onCanc
password: params.password || undefined,
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
forwarders: forwarders,
templateId: params.templateId,
templateId: params.templateId ? params.templateId : undefined,
}
updateAddress({ id: selectedAddress.id, params: updateParams }, {
onSuccess: (data) => {
toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success')
@@ -100,6 +100,44 @@ const MailServer: FC = () => {
</div>
</div>
</div>
{/* POP3 Settings */}
<div className='mt-9 border-t pt-9'>
<div className='flex xl:flex-row gap-4 flex-col justify-between pb-10'>
<div className='flex-1'>
<div className='xl:text-lg'>
{t('setting.setting_pop3')}
</div>
<div className='xl:text-sm text-xs text-description mt-1.5'>
{t('setting.setting_pop3_description')}
</div>
</div>
<div className='flex-1'>
<Input
label={t('setting.pop3_server')}
value={data?.data?.setupInfo?.pop3Settings?.server || ''}
readOnly
/>
<div className='mt-8'>
<Input
label={t('setting.pop3_port')}
value={data?.data?.setupInfo?.pop3Settings?.port?.toString() || ''}
readOnly
/>
</div>
<div className='mt-8'>
<Input
label={t('setting.encryption')}
value={data?.data?.setupInfo?.pop3Settings?.encryption || ''}
readOnly
/>
</div>
</div>
</div>
</div>
</div>
)
}
+1 -1
View File
@@ -44,7 +44,7 @@ const List: FC = () => {
</Link>
</div>
<div className='mt-9 flex flex-wrap gap-10'>
<div className='mt-9 flex flex-col lg:flex-row flex-wrap lg:gap-10 gap-5'>
{
data?.data?.templates?.map((item: TemplateResponseType) => {
return (
@@ -51,7 +51,7 @@ const Templete: FC<{
return (
<>
<div key={item.id} className='flex-1 max-w-[25%] min-w-[20%] w-full bg-white rounded-3xl p-4'>
<div key={item.id} className='flex-1 lg:max-w-[25%] min-w-[20%] w-full bg-white rounded-3xl p-4'>
<div className='h-[140px] bg-gray-200 rounded-3xl'>
<img src={item.thumbnailUrl} alt={item.name} className='w-full h-full object-contain' />
</div>
+25 -2
View File
@@ -1,6 +1,6 @@
import { FC, useEffect, useState } from 'react'
// import Input from '../components/Input'
import { ArrowDown2, CloseCircle, Element4, HambergerMenu, Logout } from 'iconsax-react'
import { ArrowDown2, CloseCircle, Element4, HambergerMenu, Logout, MessageQuestion } from 'iconsax-react'
import AvatarImage from '../assets/images/avatar_image.png'
import { useTranslation } from 'react-i18next'
import { Link, useLocation } from 'react-router-dom'
@@ -10,13 +10,14 @@ import { useGetProfile } from '@/pages/profile/hooks/useProfileData'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { removeRefreshToken, removeToken } from '../config/func'
import { clx } from '../helpers/utils'
import RowActionsDropdown from '@/components/RowActionsDropdown'
const Header: FC = () => {
const location = useLocation();
const [popoverKey, setPopoverKey] = useState(0);
const { t } = useTranslation('global')
const { setOpenSidebar, openSidebar, hasSubMenu } = useSharedStore()
const { setOpenSidebar, openSidebar, hasSubMenu, setOpenReportBug } = useSharedStore()
const { data } = useGetProfile()
useEffect(() => {
@@ -54,6 +55,28 @@ const Header: FC = () => {
{/* <Link className='xl:hidden' to={Pages.wallet}>
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
</Link> */}
<RowActionsDropdown
actions={[
{
label: 'راهنما',
onClick: () => window.open(
import.meta.env.VITE_HELP_URL + `/${import.meta.env.VITE_SERVICE_ID}`,
'_blank'
),
},
{
label: 'گزارش اشکالات',
onClick: () => setOpenReportBug(true),
},
]}
trigger={<MessageQuestion size={20} color='black' />}
topOffset={23}
leftOffset={70}
topOffsetMobile={15}
leftOffsetMobile={70}
/>
<Notifications />
{
data && (
+30 -2
View File
@@ -1,6 +1,6 @@
import { FC } from 'react'
import LogoImage from '../assets/images/logo.svg'
import { Brush2, Global, Logout, PenClose, People, Setting3 } from 'iconsax-react'
import { Brush2, Element3, Global, Logout, PenClose, People, Setting3 } from 'iconsax-react'
import SideBarItem from './SideBarItem'
import { useLocation } from 'react-router-dom'
import { useSharedStore } from './store/sharedStore'
@@ -8,17 +8,31 @@ import { clx } from '../helpers/utils'
import { useTranslation } from 'react-i18next'
import { Paths } from '@/utils/Paths'
import Storage from './components/Storage'
import { useGetDomains } from '@/pages/setting/domain/hooks/useDomainData'
import { toast } from '@/components/Toast'
import PlanRemaining from './components/PlanRemaining'
const SideBar: FC = () => {
const { t } = useTranslation()
const { openSidebar, setOpenSidebar } = useSharedStore()
const location = useLocation()
const { data: domains } = useGetDomains()
const isActive = (path: string) => location.pathname === path
const isDomainActive = !!domains?.data?.domain?.isVerified
const iconSizeSideBar = 20
const handleItemClick = (link: string, isLogout?: boolean) => (e: React.MouseEvent<HTMLAnchorElement>) => {
if ((!isDomainActive && link !== Paths.settingDomain && !isLogout) && (link !== Paths.home)) {
e.preventDefault()
toast('ابتدا باید یک دامنه تایید شده داشته باشید', 'error')
return
}
setOpenSidebar(false)
}
return (
<>
{
@@ -40,11 +54,19 @@ const SideBar: FC = () => {
</div>
<div className='text-xs text-[#8C90A3]'>
<SideBarItem
icon={<Element3 variant={isActive(Paths.home) ? 'Bold' : 'Outline'} color={isActive(Paths.home) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={t('setting.dashboard')}
isActive={isActive(Paths.home)}
link={Paths.home}
onClick={handleItemClick(Paths.home)}
/>
<SideBarItem
icon={<Setting3 variant={isActive(Paths.settingMailServer) ? 'Bold' : 'Outline'} color={isActive(Paths.settingMailServer) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={t('setting.mail_server')}
isActive={isActive(Paths.settingMailServer)}
link={Paths.settingMailServer}
onClick={handleItemClick(Paths.settingMailServer)}
/>
<SideBarItem
@@ -52,6 +74,7 @@ const SideBar: FC = () => {
title={t('setting.domain')}
isActive={isActive(Paths.settingDomain)}
link={Paths.settingDomain}
onClick={handleItemClick(Paths.settingDomain)}
/>
<SideBarItem
@@ -59,6 +82,7 @@ const SideBar: FC = () => {
title={t('setting.address')}
isActive={isActive(Paths.settingAddress)}
link={Paths.settingAddress}
onClick={handleItemClick(Paths.settingAddress)}
/>
<SideBarItem
@@ -66,6 +90,7 @@ const SideBar: FC = () => {
title={t('setting.personality')}
isActive={isActive(Paths.settingPersonalityList)}
link={Paths.settingPersonalityList}
onClick={handleItemClick(Paths.settingPersonalityList)}
/>
{/* <SideBarItem
@@ -80,10 +105,12 @@ const SideBar: FC = () => {
title={t('setting.signature')}
isActive={isActive(Paths.settingSignature)}
link={Paths.settingSignature}
onClick={handleItemClick(Paths.settingSignature)}
/>
</div>
<div className='flex-1 flex flex-col justify-end mt-10 md:mt-14 pb-6 md:pb-8'>
<PlanRemaining />
<Storage />
<div className='text-xs text-[#8C90A3]'>
<SideBarItem
@@ -92,6 +119,7 @@ const SideBar: FC = () => {
isActive={isActive('logout')}
link={`#`}
isLogout
onClick={handleItemClick('#', true)}
/>
</div>
</div>
+4 -3
View File
@@ -1,4 +1,4 @@
import { FC, ReactNode } from 'react'
import { FC, ReactNode, MouseEvent } from 'react'
import { Link } from 'react-router-dom'
import { clx } from '../helpers/utils'
@@ -9,13 +9,14 @@ type Props = {
isActive: boolean,
link: string,
isLogout?: boolean,
isWithoutLine?: boolean
isWithoutLine?: boolean,
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void
}
const SideBarItem: FC<Props> = (props: Props) => {
return (
<Link to={props.link} className='flex text-xs gap-9 mt-4'>
<Link to={props.link} className='flex text-xs gap-9 mt-4' onClick={props.onClick}>
{
!props.isWithoutLine &&
<div className={clx(
+71
View File
@@ -0,0 +1,71 @@
import { FC, useMemo } from 'react'
import { Calendar } from 'iconsax-react'
import moment from 'moment-jalaali'
import Button from '@/components/Button'
import { useWorkspaces } from '@/pages/home/hooks/useHomeData'
import { workspaceItem } from '@/pages/home/types/HomeTypes'
const PlanRemaining: FC = () => {
const getWorkspaces = useWorkspaces()
const currentWorkspace = useMemo(() => {
if (getWorkspaces.data?.data?.workspaces) {
const workspaceId = localStorage.getItem(import.meta.env.VITE_WORKSPACE_ID)
return getWorkspaces.data?.data?.workspaces.find((item: workspaceItem) => item.id === workspaceId)
}
return null
}, [getWorkspaces.data])
const remainingDays = useMemo(() => {
if (currentWorkspace?.endDate) {
const endDate = moment(currentWorkspace.endDate)
const now = moment()
const days = endDate.diff(now, 'days')
return days > 0 ? days : 0
}
return null
}, [currentWorkspace])
if (!currentWorkspace || remainingDays === null) {
return null
}
return (
<div className='px-10 pb-5 mb-5 border-b border-[#ECEEF5]'>
<div className='flex gap-2 items-center'>
<Calendar size={20} color='#8C90A3' />
<div className='text-xs text-description'>
زمان باقیمانده پلن
</div>
</div>
<div className='mt-3 text-xs text-[#8C90A3]'>
{remainingDays > 0 ? (
<span className='text-[#F59E0B] font-medium'>
{remainingDays} روز تا پایان پلن
</span>
) : (
<span className='text-red-500 font-medium'>
پلن شما منقضی شده است
</span>
)}
</div>
{currentWorkspace.endDate && (
<div className='mt-2 text-xs text-[#8C90A3]'>
تاریخ پایان: {moment(currentWorkspace.endDate).format('jYYYY/jMM/jDD')}
</div>
)}
<div className='mt-4'>
<Button
variant='primary'
label='تمدید پلن'
onClick={() => {
window.open(`${import.meta.env.VITE_CONSOLE_URL}/services/detail/${currentWorkspace?.plan?.service?.slug}?id=${currentWorkspace?.id}`, '_blank')
}}
/>
</div>
</div>
)
}
export default PlanRemaining
+2
View File
@@ -10,4 +10,6 @@ export const useSharedStore = create<SharedStoreType>((set) => ({
setOpenBuySpace: (value) => set({ openBuySpace: value }),
hasSubMenu: false,
setSubtMenu: (value: boolean) => set({ hasSubMenu: value }),
openReportBug: false,
setOpenReportBug: (value) => set({ openReportBug: value }),
}));
+2
View File
@@ -7,4 +7,6 @@ export type SharedStoreType = {
setOpenBuySpace: (value: boolean) => void;
hasSubMenu: boolean;
setSubtMenu: (value: boolean) => void;
openReportBug: boolean;
setOpenReportBug: (value: boolean) => void;
};