Compare commits
10 Commits
f011f4369c
...
e37c42a276
| Author | SHA1 | Date | |
|---|---|---|---|
| e37c42a276 | |||
| 596f27f238 | |||
| b84ce0aa4b | |||
| 093ddbf195 | |||
| 2658f06f1c | |||
| fc206b3392 | |||
| 27eb5af7fd | |||
| 27ea922168 | |||
| 4eec15eb65 | |||
| a6d7533075 |
@@ -7,3 +7,8 @@ VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
|
|||||||
|
|
||||||
VITE_LOGIN_URL = 'https://console.danakcorp.com/auth/login'
|
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
@@ -1,21 +1,56 @@
|
|||||||
FROM node:22-alpine AS builder
|
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
|
# Install tzdata to support timezone settings
|
||||||
RUN apk add --no-cache tzdata
|
RUN apk add --no-cache tzdata
|
||||||
|
|
||||||
RUN npm install -g corepack@latest
|
RUN npm install -g corepack@latest
|
||||||
RUN corepack enable && corepack prepare pnpm@10 --activate
|
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
|
# Set the timezone to Asia/Tehran
|
||||||
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
|
||||||
|
|
||||||
WORKDIR /build
|
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 ./
|
COPY package*.json pnpm-lock.yaml ./
|
||||||
RUN pnpm install --frozen-lockfile --loglevel info
|
RUN pnpm install --frozen-lockfile --loglevel info
|
||||||
COPY . ./
|
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
|
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/dist /usr/share/nginx/html
|
||||||
|
|
||||||
COPY --from=builder /build/nginx.con[f] /etc/nginx/conf.d/default.conf
|
COPY --from=builder /build/nginx.con[f] /etc/nginx/conf.d/default.conf
|
||||||
|
|||||||
+4
-1
@@ -15,6 +15,8 @@ import { getRefreshToken, removeRefreshToken, removeToken, setRefreshToken, setT
|
|||||||
import { refreshToken } from './pages/auth/service/Service';
|
import { refreshToken } from './pages/auth/service/Service';
|
||||||
import BuySpace from './shared/components/BuySpace'
|
import BuySpace from './shared/components/BuySpace'
|
||||||
import useConvertNumbers from './hooks/useConvertNumbers'
|
import useConvertNumbers from './hooks/useConvertNumbers'
|
||||||
|
import CheckWorkSpace from './components/CheckWorkSpace'
|
||||||
|
import ReportBug from './pages/guide/ReportBug';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -102,8 +104,9 @@ const App: FC = () => {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<I18nextProvider i18n={i18next}>
|
<I18nextProvider i18n={i18next}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<CheckWorkSpace />
|
||||||
<Main />
|
<Main />
|
||||||
|
<ReportBug />
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<BuySpace />
|
<BuySpace />
|
||||||
</QueryClientProvider>
|
</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 |
@@ -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
|
||||||
@@ -28,7 +28,7 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
|||||||
<Fragment>
|
<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 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='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%]'>
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom'
|
||||||
import { More } from 'iconsax-react';
|
import { More } from 'iconsax-react'
|
||||||
|
|
||||||
export interface RowActionItem {
|
export interface RowActionItem {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -12,106 +12,134 @@ export interface RowActionItem {
|
|||||||
interface RowActionsDropdownProps {
|
interface RowActionsDropdownProps {
|
||||||
actions: RowActionItem[];
|
actions: RowActionItem[];
|
||||||
className?: string;
|
className?: string;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
topOffset?: number;
|
||||||
|
leftOffset?: number;
|
||||||
|
topOffsetMobile?: number;
|
||||||
|
leftOffsetMobile?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({ actions, className = '' }) => {
|
const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
actions,
|
||||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
className = '',
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
trigger,
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
setIsOpen(false);
|
setIsOpen(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
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) => {
|
const handleToggle = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
|
|
||||||
if (!isOpen && buttonRef.current) {
|
if (!isOpen && buttonRef.current) {
|
||||||
const rect = buttonRef.current.getBoundingClientRect();
|
const rect = buttonRef.current.getBoundingClientRect()
|
||||||
const isMobile = window.innerWidth < 768;
|
const calculatedPosition = calculatePosition(rect)
|
||||||
const dropdownWidth = isMobile ? 140 : 150;
|
setDropdownPosition(calculatedPosition)
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// بررسی اینکه منو از سمت چپ خارج نشود
|
setIsOpen(!isOpen)
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleActionClick = (action: RowActionItem) => {
|
const handleActionClick = (action: RowActionItem) => {
|
||||||
action.onClick();
|
action.onClick()
|
||||||
setIsOpen(false);
|
setIsOpen(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const renderDropdown = () => {
|
const renderDropdown = () => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
ref={dropdownRef}
|
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={{
|
style={{
|
||||||
top: `${position.top}px`,
|
top: `${dropdownPosition.top}px`,
|
||||||
left: `${position.left}px`,
|
left: `${dropdownPosition.left}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{actions.map((action, index) => (
|
{actions.map((action, index) => (
|
||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => handleActionClick(action)}
|
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' : ''
|
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.className || ''}`}
|
|
||||||
>
|
>
|
||||||
{action.icon && <span className="ml-2">{action.icon}</span>}
|
{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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<button
|
<button
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
onClick={handleToggle}
|
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>
|
</button>
|
||||||
|
|
||||||
{renderDropdown()}
|
{renderDropdown()}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default RowActionsDropdown;
|
export default RowActionsDropdown
|
||||||
@@ -267,3 +267,8 @@ textarea::placeholder {
|
|||||||
|
|
||||||
backdrop-filter: blur(44px);
|
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
@@ -116,6 +116,10 @@
|
|||||||
"setting_imap_description": "تنظیمات IMAP خود را با دقت وارد کنید.",
|
"setting_imap_description": "تنظیمات IMAP خود را با دقت وارد کنید.",
|
||||||
"imap_server": "IMAP سرور",
|
"imap_server": "IMAP سرور",
|
||||||
"imap_port": "IMAP پورت",
|
"imap_port": "IMAP پورت",
|
||||||
|
"setting_pop3": "تنظیمات POP3",
|
||||||
|
"setting_pop3_description": "تنظیمات POP3 خود را با دقت وارد کنید.",
|
||||||
|
"pop3_server": "POP3 سرور",
|
||||||
|
"pop3_port": "POP3 پورت",
|
||||||
"encryption": "نوع رمزنگاری",
|
"encryption": "نوع رمزنگاری",
|
||||||
"record_dns": "رکوردهای DNS",
|
"record_dns": "رکوردهای DNS",
|
||||||
"record_dns_description": "در این قسمت میتوانید وضعیت رکوردهای DNS سرویس ایمیل خود را بررسی کنید.",
|
"record_dns_description": "در این قسمت میتوانید وضعیت رکوردهای DNS سرویس ایمیل خود را بررسی کنید.",
|
||||||
@@ -159,7 +163,8 @@
|
|||||||
"add_social": "اضافه کردن شبکه اجتماعی",
|
"add_social": "اضافه کردن شبکه اجتماعی",
|
||||||
"update_social": "بروزرسانی شبکه اجتماعی",
|
"update_social": "بروزرسانی شبکه اجتماعی",
|
||||||
"delete": "حذف",
|
"delete": "حذف",
|
||||||
"preview": "پیش نمایش"
|
"preview": "پیش نمایش",
|
||||||
|
"dashboard": "میزکار"
|
||||||
},
|
},
|
||||||
"save": "ذخیره",
|
"save": "ذخیره",
|
||||||
"export_html": "خروجی HTML",
|
"export_html": "خروجی HTML",
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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),
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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,
|
password: params.password || undefined,
|
||||||
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
quota: params.quotaMB ? params.quotaMB * 1024 * 1024 : undefined,
|
||||||
forwarders: forwarders,
|
forwarders: forwarders,
|
||||||
templateId: params.templateId,
|
templateId: params.templateId ? params.templateId : undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
updateAddress({ id: selectedAddress.id, params: updateParams }, {
|
updateAddress({ id: selectedAddress.id, params: updateParams }, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success')
|
toast(data?.data?.message || 'آپدیت با موفقیت انجام شد', 'success')
|
||||||
|
|||||||
@@ -100,6 +100,44 @@ const MailServer: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const List: FC = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</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) => {
|
data?.data?.templates?.map((item: TemplateResponseType) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const Templete: FC<{
|
|||||||
|
|
||||||
return (
|
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'>
|
<div className='h-[140px] bg-gray-200 rounded-3xl'>
|
||||||
<img src={item.thumbnailUrl} alt={item.name} className='w-full h-full object-contain' />
|
<img src={item.thumbnailUrl} alt={item.name} className='w-full h-full object-contain' />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+25
-2
@@ -1,6 +1,6 @@
|
|||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
// import Input from '../components/Input'
|
// 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 AvatarImage from '../assets/images/avatar_image.png'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
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 { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
|
||||||
import { removeRefreshToken, removeToken } from '../config/func'
|
import { removeRefreshToken, removeToken } from '../config/func'
|
||||||
import { clx } from '../helpers/utils'
|
import { clx } from '../helpers/utils'
|
||||||
|
import RowActionsDropdown from '@/components/RowActionsDropdown'
|
||||||
|
|
||||||
const Header: FC = () => {
|
const Header: FC = () => {
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [popoverKey, setPopoverKey] = useState(0);
|
const [popoverKey, setPopoverKey] = useState(0);
|
||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const { setOpenSidebar, openSidebar, hasSubMenu } = useSharedStore()
|
const { setOpenSidebar, openSidebar, hasSubMenu, setOpenReportBug } = useSharedStore()
|
||||||
const { data } = useGetProfile()
|
const { data } = useGetProfile()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -54,6 +55,28 @@ const Header: FC = () => {
|
|||||||
{/* <Link className='xl:hidden' to={Pages.wallet}>
|
{/* <Link className='xl:hidden' to={Pages.wallet}>
|
||||||
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
|
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
|
||||||
</Link> */}
|
</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 />
|
<Notifications />
|
||||||
{
|
{
|
||||||
data && (
|
data && (
|
||||||
|
|||||||
+30
-2
@@ -1,6 +1,6 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import LogoImage from '../assets/images/logo.svg'
|
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 SideBarItem from './SideBarItem'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { useSharedStore } from './store/sharedStore'
|
import { useSharedStore } from './store/sharedStore'
|
||||||
@@ -8,17 +8,31 @@ import { clx } from '../helpers/utils'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Paths } from '@/utils/Paths'
|
import { Paths } from '@/utils/Paths'
|
||||||
import Storage from './components/Storage'
|
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 SideBar: FC = () => {
|
||||||
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { openSidebar, setOpenSidebar } = useSharedStore()
|
const { openSidebar, setOpenSidebar } = useSharedStore()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const { data: domains } = useGetDomains()
|
||||||
|
|
||||||
const isActive = (path: string) => location.pathname === path
|
const isActive = (path: string) => location.pathname === path
|
||||||
|
const isDomainActive = !!domains?.data?.domain?.isVerified
|
||||||
const iconSizeSideBar = 20
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
@@ -40,11 +54,19 @@ const SideBar: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='text-xs text-[#8C90A3]'>
|
<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
|
<SideBarItem
|
||||||
icon={<Setting3 variant={isActive(Paths.settingMailServer) ? 'Bold' : 'Outline'} color={isActive(Paths.settingMailServer) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
icon={<Setting3 variant={isActive(Paths.settingMailServer) ? 'Bold' : 'Outline'} color={isActive(Paths.settingMailServer) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
|
||||||
title={t('setting.mail_server')}
|
title={t('setting.mail_server')}
|
||||||
isActive={isActive(Paths.settingMailServer)}
|
isActive={isActive(Paths.settingMailServer)}
|
||||||
link={Paths.settingMailServer}
|
link={Paths.settingMailServer}
|
||||||
|
onClick={handleItemClick(Paths.settingMailServer)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
@@ -52,6 +74,7 @@ const SideBar: FC = () => {
|
|||||||
title={t('setting.domain')}
|
title={t('setting.domain')}
|
||||||
isActive={isActive(Paths.settingDomain)}
|
isActive={isActive(Paths.settingDomain)}
|
||||||
link={Paths.settingDomain}
|
link={Paths.settingDomain}
|
||||||
|
onClick={handleItemClick(Paths.settingDomain)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
@@ -59,6 +82,7 @@ const SideBar: FC = () => {
|
|||||||
title={t('setting.address')}
|
title={t('setting.address')}
|
||||||
isActive={isActive(Paths.settingAddress)}
|
isActive={isActive(Paths.settingAddress)}
|
||||||
link={Paths.settingAddress}
|
link={Paths.settingAddress}
|
||||||
|
onClick={handleItemClick(Paths.settingAddress)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
@@ -66,6 +90,7 @@ const SideBar: FC = () => {
|
|||||||
title={t('setting.personality')}
|
title={t('setting.personality')}
|
||||||
isActive={isActive(Paths.settingPersonalityList)}
|
isActive={isActive(Paths.settingPersonalityList)}
|
||||||
link={Paths.settingPersonalityList}
|
link={Paths.settingPersonalityList}
|
||||||
|
onClick={handleItemClick(Paths.settingPersonalityList)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <SideBarItem
|
{/* <SideBarItem
|
||||||
@@ -80,10 +105,12 @@ const SideBar: FC = () => {
|
|||||||
title={t('setting.signature')}
|
title={t('setting.signature')}
|
||||||
isActive={isActive(Paths.settingSignature)}
|
isActive={isActive(Paths.settingSignature)}
|
||||||
link={Paths.settingSignature}
|
link={Paths.settingSignature}
|
||||||
|
onClick={handleItemClick(Paths.settingSignature)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex-1 flex flex-col justify-end mt-10 md:mt-14 pb-6 md:pb-8'>
|
<div className='flex-1 flex flex-col justify-end mt-10 md:mt-14 pb-6 md:pb-8'>
|
||||||
|
<PlanRemaining />
|
||||||
<Storage />
|
<Storage />
|
||||||
<div className='text-xs text-[#8C90A3]'>
|
<div className='text-xs text-[#8C90A3]'>
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
@@ -92,6 +119,7 @@ const SideBar: FC = () => {
|
|||||||
isActive={isActive('logout')}
|
isActive={isActive('logout')}
|
||||||
link={`#`}
|
link={`#`}
|
||||||
isLogout
|
isLogout
|
||||||
|
onClick={handleItemClick('#', true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC, ReactNode } from 'react'
|
import { FC, ReactNode, MouseEvent } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { clx } from '../helpers/utils'
|
import { clx } from '../helpers/utils'
|
||||||
|
|
||||||
@@ -9,13 +9,14 @@ type Props = {
|
|||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
link: string,
|
link: string,
|
||||||
isLogout?: boolean,
|
isLogout?: boolean,
|
||||||
isWithoutLine?: boolean
|
isWithoutLine?: boolean,
|
||||||
|
onClick?: (event: MouseEvent<HTMLAnchorElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SideBarItem: FC<Props> = (props: Props) => {
|
const SideBarItem: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
return (
|
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 &&
|
!props.isWithoutLine &&
|
||||||
<div className={clx(
|
<div className={clx(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -10,4 +10,6 @@ export const useSharedStore = create<SharedStoreType>((set) => ({
|
|||||||
setOpenBuySpace: (value) => set({ openBuySpace: value }),
|
setOpenBuySpace: (value) => set({ openBuySpace: value }),
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
setSubtMenu: (value: boolean) => set({ hasSubMenu: value }),
|
setSubtMenu: (value: boolean) => set({ hasSubMenu: value }),
|
||||||
|
openReportBug: false,
|
||||||
|
setOpenReportBug: (value) => set({ openReportBug: value }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -7,4 +7,6 @@ export type SharedStoreType = {
|
|||||||
setOpenBuySpace: (value: boolean) => void;
|
setOpenBuySpace: (value: boolean) => void;
|
||||||
hasSubMenu: boolean;
|
hasSubMenu: boolean;
|
||||||
setSubtMenu: (value: boolean) => void;
|
setSubtMenu: (value: boolean) => void;
|
||||||
|
openReportBug: boolean;
|
||||||
|
setOpenReportBug: (value: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user