support plan

This commit is contained in:
hamid zarghami
2025-05-05 16:59:48 +03:30
parent b945ffb0a3
commit f45273fc96
25 changed files with 552 additions and 9 deletions
+209
View File
@@ -0,0 +1,209 @@
import { FC, useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useTranslation } from 'react-i18next'
import SidebarHint from './components/SidebarHint'
import SupportImage from '../../assets/images/support.png'
import Button from '../../components/Button'
import { CloseCircle, Document, Element3, TickCircle } from 'iconsax-react'
import Tabs from '../../components/Tabs'
import { useBuySupportPlan, useGetSupportPlans } from './hooks/useSupportData'
import { NumberFormat } from '../../config/func'
import { PlanItemType, featureItemType } from './types/SupportTypes'
import SupportSkeleton from './components/SupportSkeleton'
import SupportIcon from './components/SupportIcon'
import { SupportPlanFeatureKey } from './enum/SupportEnum'
import { toast } from '../../components/Toast'
import { ErrorType } from '../../helpers/types'
import { useNavigate } from 'react-router-dom'
import { Pages } from '../../config/Pages'
const Support: FC = () => {
const { data, isLoading } = useGetSupportPlans()
const { t } = useTranslation('global')
const [activeTab, setActiveTab] = useState<string>('')
const [loadingPlanId, setLoadingPlanId] = useState<string>('')
const { mutate: buySupportPlan } = useBuySupportPlan()
const navigate = useNavigate()
useEffect(() => {
if (data?.data?.supportPlans?.length > 0 && !activeTab) {
setActiveTab(data.data.supportPlans[0].id)
}
}, [data, activeTab])
const handleBuy = (planId: string) => {
setLoadingPlanId(planId)
buySupportPlan(planId, {
onSuccess: (data) => {
toast(data?.data?.message, 'success')
navigate(Pages.receipts.detail + data?.data?.invoice?.id)
setLoadingPlanId('')
},
onError: (error: ErrorType) => {
toast(error?.response?.data?.error.message[0], 'error')
setLoadingPlanId('')
}
})
}
if (isLoading || !data?.data) {
return <SupportSkeleton />
}
return (
<div className='mt-4'>
<Helmet>
<title>
داناک | {t('support.title')}
</title>
</Helmet>
<div>
{t('support.title')}
</div>
<div className='xl:hidden mt-5'>
<Tabs
items={data?.data?.supportPlans?.map((plan: PlanItemType) => ({
icon: <Element3 color={activeTab === plan.id ? 'black' : '#8C90A3'} size={22} />,
label: plan.name,
value: plan.id
}))}
active={activeTab}
onChange={setActiveTab}
/>
</div>
<div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
<div className='xl:flex hidden justify-between mt-10'>
<div className='flex-1 flex justify-center'>
<img src={SupportImage} alt='support' className='w-[160px]' />
</div>
{
data?.data?.supportPlans?.map((item: PlanItemType) => {
return (
<div key={item.id} className='flex-1 flex flex-col items-center'>
<div className='text-center w-full'>
<div className='font-bold'>
{item.name}
</div>
<div className='text-sm text-[#595C67] mt-4'>
ماهیانه
</div>
<div className='font-bold text-sm mt-1'>
{NumberFormat(item.price)}
</div>
<div className='mt-1 text-sm'>
تومان
</div>
<Button
label='ثبت پلن'
className='mt-4 mx-auto w-fit px-5'
onClick={() => handleBuy(item.id)}
isLoading={loadingPlanId === item.id}
/>
</div>
</div>
)
})
}
</div>
<div className='xl:flex hidden flex-1 mt-5'>
<div className='flex-1 flex flex-col'>
{
data?.data?.supportPlans?.[0]?.features.map((row: featureItemType) => (
<div className='flex h-16 px-4 plan items-center justify-between'>
<div className='flex-1 flex items-center gap-2'>
<div className='size-8 rounded-2.5 bg-[#EEF0F7] flex items-center justify-center'>
<SupportIcon keySupport={row.featureKey as SupportPlanFeatureKey} />
</div>
<div className='text-xs'>{t(`support.${row.featureKey}`)}</div>
</div>
</div>
))
}
</div>
{
data?.data?.supportPlans?.map((item: PlanItemType) => (
<div className='flex-1 flex flex-col'>
{
item.features.map((row) => (
<div className='flex h-16 px-4 plan items-center justify-center'>
{
row.featureType === 'boolean' ?
row.featureValue === 'true' ?
<TickCircle className='xl:size-8 size-6' color='#01A560' variant='Bold' />
:
<CloseCircle className='xl:size-8 size-6' color='#D50303' variant='Bold' />
:
<div className='text-xs'>{row.featureValue}</div>
}
</div>
))
}
</div>
))
}
</div>
{/* Mobile View */}
<div className='xl:hidden block mt-5'>
{
data?.data?.supportPlans?.map((plan: PlanItemType) => (
activeTab === plan.id && (
<div key={plan.id}>
<div className='flex justify-center mb-6'>
<img src={SupportImage} alt='support' className='w-[120px]' />
</div>
<div className='text-center mb-6'>
<div className='font-bold text-lg'>{plan.name}</div>
<div className='text-sm text-[#595C67] mt-2'>ماهیانه</div>
<div className='font-bold text-sm mt-1'>{NumberFormat(plan.price)}</div>
<div className='mt-1 text-sm'>تومان</div>
<Button
label='ثبت پلن'
className='mt-4 w-fit px-5 mx-auto'
onClick={() => handleBuy(plan.id)}
isLoading={loadingPlanId === plan.id}
/>
</div>
<div className='space-y-4'>
{
plan.features.map((row: featureItemType) => (
<div className='flex items-center justify-between p-4 bg-gray-50 rounded-xl'>
<div className='flex items-center gap-2'>
<div className='size-8 rounded-2.5 bg-[#EEF0F7] flex items-center justify-center'>
<Document size={16} color='#595C67' variant='Bold' />
</div>
<div className='text-xs'>{t(`support.${row.featureKey}`)}</div>
</div>
{
row.featureType === 'boolean' ?
row.featureValue === 'true' ?
<TickCircle className='size-6' color='#01A560' variant='Bold' />
:
<CloseCircle className='size-6' color='#D50303' variant='Bold' />
:
<div className='text-sm'>{row.featureValue}</div>
}
</div>
))
}
</div>
</div>
)
))
}
</div>
</div>
<SidebarHint />
</div>
</div>
)
}
export default Support
@@ -0,0 +1,67 @@
import { TickSquare } from 'iconsax-react'
import { FC } from 'react'
const SidebarHint: FC = () => {
return (
<div className='bg-white w-sidebar xl:block hidden py-10 px-5 h-fit rounded-3xl'>
<div className='text-sm'>
جهت انتخاب بهتر به مسائل زیر توجه بفرمایید:
</div>
<div className='mt-6'>
<div className='flex items-start gap-2 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
سوالات - مشکلاتی که به یک موضوع مربوط میشوند را در یک درخواست پشتیبانی پیگیر باشید و چند درخواست برای یک موضوع باز نکنید.
</div>
</div>
<div className='flex items-start gap-2 mt-6 border-b pb-5'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
پاسخگویی 24 ساعته تلفنی را تنها از میهن وب هاست می توانید انتظار داشته باشید .بخش پشتیبانی در هر ساعتی حتی در روز های تعطیل آماده پیگیری سریع مشکلات کاربران است.
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
</div>
</div>
<div className='flex items-start gap-2 mt-6'>
<div>
<TickSquare size={20} color='black' variant='Bold' />
</div>
<div className='text-description text-xs leading-5'>
لطفاً برای بررسی و رفع مشکلات احتمالی صبور باشید بررسی و رفع مشکلات در برخی موارد زمان گیر است.
</div>
</div>
</div>
</div>
)
}
export default SidebarHint
@@ -0,0 +1,45 @@
import { FC } from 'react'
import { SupportPlanFeatureKey } from '../enum/SupportEnum'
import {
Document,
MessageQuestion,
Clock,
Call,
Save2,
Profile2User,
Location,
Teacher,
Ticket
} from 'iconsax-react'
type Props = {
keySupport: SupportPlanFeatureKey
}
const SupportIcon: FC<Props> = ({ keySupport }) => {
switch (keySupport) {
case SupportPlanFeatureKey.LEARNING_DOCS:
return <Document size={16} color='#595C67' variant='Bold' />
case SupportPlanFeatureKey.TICKET_SUPPORT:
return <MessageQuestion size={16} color='#595C67' variant='Bold' />
case SupportPlanFeatureKey.RESPONSE_TIME:
return <Clock size={16} color='#595C67' variant='Bold' />
case SupportPlanFeatureKey.PHONE_SUPPORT:
return <Call size={16} color='#595C67' variant='Bold' />
case SupportPlanFeatureKey.BACKUP_VERSION:
return <Save2 size={16} color='#595C67' variant='Bold' />
case SupportPlanFeatureKey.TECHNICAL_EXPERT_ACCESS:
return <Profile2User size={16} color='#595C67' variant='Bold' />
case SupportPlanFeatureKey.ON_SITE_SUPPORT:
return <Location size={16} color='#595C67' variant='Bold' />
case SupportPlanFeatureKey.ON_SITE_TRAINING:
return <Teacher size={16} color='#595C67' variant='Bold' />
case SupportPlanFeatureKey.TICKET_LIMIT:
return <Ticket size={16} color='#595C67' variant='Bold' />
default:
return <Document size={16} color='#595C67' variant='Bold' />
}
}
export default SupportIcon
@@ -0,0 +1,94 @@
import { FC } from 'react'
import { Document } from 'iconsax-react'
const SupportSkeleton: FC = () => {
return (
<div className='mt-4'>
<div className='flex gap-6 xl:mt-8 mt-4'>
<div className='flex-1 bg-white py-8 xl:px-10 px-4 rounded-3xl'>
{/* Desktop View */}
<div className='xl:flex hidden justify-between mt-10'>
<div className='flex-1 flex justify-center'>
<div className='w-[160px] h-[160px] bg-gray-200 rounded-xl animate-pulse' />
</div>
{[1, 2].map((item) => (
<div key={item} className='flex-1 flex flex-col items-center'>
<div className='text-center w-full'>
<div className='h-6 w-32 bg-gray-200 rounded-lg animate-pulse mx-auto' />
<div className='h-4 w-20 bg-gray-200 rounded-lg animate-pulse mx-auto mt-4' />
<div className='h-5 w-24 bg-gray-200 rounded-lg animate-pulse mx-auto mt-1' />
<div className='h-4 w-16 bg-gray-200 rounded-lg animate-pulse mx-auto mt-1' />
<div className='h-10 w-28 bg-gray-200 rounded-lg animate-pulse mx-auto mt-4' />
</div>
</div>
))}
</div>
<div className='xl:flex hidden flex-1 mt-5'>
<div className='flex-1 flex flex-col'>
{[1, 2, 3, 4, 5].map((item) => (
<div key={item} className='flex h-16 px-4 plan items-center justify-between'>
<div className='flex-1 flex items-center gap-2'>
<div className='size-8 rounded-2.5 bg-gray-200 animate-pulse' />
<div className='h-4 w-32 bg-gray-200 rounded-lg animate-pulse' />
</div>
</div>
))}
</div>
{[1, 2].map((item) => (
<div key={item} className='flex-1 flex flex-col'>
{[1, 2, 3, 4, 5].map((feature) => (
<div key={feature} className='flex h-16 px-4 plan items-center justify-center'>
<div className='size-8 bg-gray-200 rounded-full animate-pulse' />
</div>
))}
</div>
))}
</div>
{/* Mobile View */}
<div className='xl:hidden block mt-5'>
<div className='flex justify-center mb-6'>
<div className='w-[120px] h-[120px] bg-gray-200 rounded-xl animate-pulse' />
</div>
<div className='text-center mb-6'>
<div className='h-6 w-40 bg-gray-200 rounded-lg animate-pulse mx-auto' />
<div className='h-4 w-24 bg-gray-200 rounded-lg animate-pulse mx-auto mt-2' />
<div className='h-5 w-32 bg-gray-200 rounded-lg animate-pulse mx-auto mt-1' />
<div className='h-4 w-20 bg-gray-200 rounded-lg animate-pulse mx-auto mt-1' />
<div className='h-10 w-32 bg-gray-200 rounded-lg animate-pulse mx-auto mt-4' />
</div>
<div className='space-y-4'>
{[1, 2, 3, 4, 5].map((item) => (
<div key={item} className='flex items-center justify-between p-4 bg-gray-50 rounded-xl'>
<div className='flex items-center gap-2'>
<div className='size-8 rounded-2.5 bg-gray-200 animate-pulse' />
<div className='h-4 w-32 bg-gray-200 rounded-lg animate-pulse' />
</div>
<div className='size-6 bg-gray-200 rounded-full animate-pulse' />
</div>
))}
</div>
</div>
</div>
<div className='xl:block hidden w-[300px]'>
<div className='bg-white p-6 rounded-3xl'>
<div className='h-6 w-32 bg-gray-200 rounded-lg animate-pulse mb-4' />
<div className='space-y-3'>
{[1, 2, 3].map((item) => (
<div key={item} className='flex items-center gap-2'>
<div className='size-6 bg-gray-200 rounded-full animate-pulse' />
<div className='h-4 w-40 bg-gray-200 rounded-lg animate-pulse' />
</div>
))}
</div>
</div>
</div>
</div>
</div>
)
}
export default SupportSkeleton
+11
View File
@@ -0,0 +1,11 @@
export enum SupportPlanFeatureKey {
LEARNING_DOCS = "learning_docs", // راهنما و مستندات آموزشی
TICKET_SUPPORT = "ticket_support", // ارسال تیکت
RESPONSE_TIME = "response_time", // زمان پاسخگویی به تیکت
PHONE_SUPPORT = "phone_support", // پاسخگویی تلفنی
BACKUP_VERSION = "backup_version", // نسخه بکاپ
TECHNICAL_EXPERT_ACCESS = "technical_expert_access", // دسترسی به متخصصین فنی
ON_SITE_SUPPORT = "on_site_support", // پشتیبانی در محل
ON_SITE_TRAINING = "on_site_training", // آموزش در محل
TICKET_LIMIT = "ticket_limit", // ظرفیت ارسال تیکت
}
+12
View File
@@ -0,0 +1,12 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { getSupportPlans, buySupportPlan } from "../service/SupportService";
export const useGetSupportPlans = () => {
return useQuery({ queryKey: ["support-plans"], queryFn: getSupportPlans });
};
export const useBuySupportPlan = () => {
return useMutation({
mutationFn: (planId: string) => buySupportPlan(planId),
});
};
@@ -0,0 +1,11 @@
import axios from "../../../config/axios";
export const getSupportPlans = async () => {
const { data } = await axios.get("/support-plans");
return data;
};
export const buySupportPlan = async (planId: string) => {
const { data } = await axios.post(`/support-plans/${planId}/subscribe`);
return data;
};
+22
View File
@@ -0,0 +1,22 @@
export type featureItemType = {
id: string;
createdAt: string;
updatedAt: string;
featureKey: string;
featureValue: string;
featureType: string;
icon: string | null;
};
export type PlanItemType = {
id: string;
createdAt: string;
updatedAt: string;
name: string;
description: string;
price: number;
duration: number;
isActive: boolean;
features: featureItemType[];
deletedAt: string | null;
};