redirect url + accordion + blog + ...
This commit is contained in:
+10
-42
@@ -1,7 +1,8 @@
|
||||
import InfoBox from '@/components/InfoBox'
|
||||
import { AddCircle, TickCircle } from 'iconsax-react'
|
||||
import { TickCircle } from 'iconsax-react'
|
||||
import { NextPage } from 'next'
|
||||
import React from 'react'
|
||||
import Accordion from '@/components/Accordion'
|
||||
import WhyDanak from '../home/WhyDanak'
|
||||
|
||||
const About: NextPage = () => {
|
||||
@@ -63,19 +64,19 @@ const About: NextPage = () => {
|
||||
صادرکنندگان نرم افزار
|
||||
</div>
|
||||
<div className='flex-1 xl:min-w-[30%] min-w-[90%] bg-[#ECEFF6] h-10 rounded-xl font-bold flex justify-center items-center'>
|
||||
صادرکنندگان نرم افزار
|
||||
مشاوره استقرار ISMS
|
||||
</div>
|
||||
<div className='flex-1 xl:min-w-[30%] min-w-[90%] bg-[#ECEFF6] h-10 rounded-xl font-bold flex justify-center items-center'>
|
||||
صادرکنندگان نرم افزار
|
||||
نظام صنفی
|
||||
</div>
|
||||
<div className='flex-1 xl:min-w-[30%] min-w-[90%] bg-[#ECEFF6] h-10 rounded-xl font-bold flex justify-center items-center'>
|
||||
صادرکنندگان نرم افزار
|
||||
GDPR
|
||||
</div>
|
||||
<div className='flex-1 xl:min-w-[30%] min-w-[90%] bg-[#ECEFF6] h-10 rounded-xl font-bold flex justify-center items-center'>
|
||||
صادرکنندگان نرم افزار
|
||||
ISO:27001
|
||||
</div>
|
||||
<div className='flex-1 min-w-[30%] bg-[#ECEFF6] h-10 rounded-xl font-bold flex justify-center items-center'>
|
||||
صادرکنندگان نرم افزار
|
||||
SSAE18
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,42 +94,9 @@ const About: NextPage = () => {
|
||||
</div>
|
||||
|
||||
<div className='flex-1 flex flex-col gap-4'>
|
||||
<div className='bg-white rounded-xl p-3 flex-1 h-fit'>
|
||||
<div className='flex justify-between'>
|
||||
<div className='font-bold'>
|
||||
تعهد
|
||||
</div>
|
||||
|
||||
<AddCircle
|
||||
size={24}
|
||||
color='black'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-white rounded-xl p-3 flex-1 h-fit'>
|
||||
<div className='flex justify-between'>
|
||||
<div className='font-bold'>
|
||||
تعهد
|
||||
</div>
|
||||
|
||||
<AddCircle
|
||||
size={24}
|
||||
color='black'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-white rounded-xl p-3 flex-1 h-fit'>
|
||||
<div className='flex justify-between'>
|
||||
<div className='font-bold'>
|
||||
تعهد
|
||||
</div>
|
||||
|
||||
<AddCircle
|
||||
size={24}
|
||||
color='black'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Accordion title='تعهد' content='ما در داناک متعهد هستیم که بهترین خدمات و محصولات را با کیفیت بالا به مشتریان خود ارائه دهیم و همواره به دنبال بهبود و ارتقای سطح خدمات خود هستیم.' />
|
||||
<Accordion title='تعهد' content='ما در داناک متعهد هستیم که بهترین خدمات و محصولات را با کیفیت بالا به مشتریان خود ارائه دهیم و همواره به دنبال بهبود و ارتقای سطح خدمات خود هستیم.' />
|
||||
<Accordion title='تعهد' content='ما در داناک متعهد هستیم که بهترین خدمات و محصولات را با کیفیت بالا به مشتریان خود ارائه دهیم و همواره به دنبال بهبود و ارتقای سطح خدمات خود هستیم.' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ interface PageProps {
|
||||
const SingleBlog: NextPage<PageProps> = async ({ params }) => {
|
||||
|
||||
const { id } = await params
|
||||
console.log("loading", id);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
@@ -24,7 +23,6 @@ const SingleBlog: NextPage<PageProps> = async ({ params }) => {
|
||||
}
|
||||
|
||||
async function BlogContent({ id }: { id: string }) {
|
||||
console.log(id)
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
await Promise.all([
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { FC } from 'react'
|
||||
import Input from '../../../components/Input'
|
||||
import Textarea from '../../../components/Textarea'
|
||||
import Button from '../../../components/Button'
|
||||
import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import { ErrorType } from '../../../helpers/types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useCreateBlogComment } from '../hooks/useBlogsData'
|
||||
import { CreateBlogCommentType } from '../types/BlogTypes'
|
||||
import { useSharedStore } from '@/shared/store/sharedStore'
|
||||
import Link from 'next/link'
|
||||
import { LOGIN_URL } from '@/config/const'
|
||||
type Props = {
|
||||
refetch: () => void
|
||||
id: string
|
||||
}
|
||||
|
||||
const CreateReview: FC<Props> = ({ refetch, id }) => {
|
||||
|
||||
const { isLogin } = useSharedStore()
|
||||
|
||||
const createReview = useCreateBlogComment()
|
||||
|
||||
const formik = useFormik<CreateBlogCommentType>({
|
||||
initialValues: {
|
||||
title: '',
|
||||
content: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
title: Yup.string().required('این فیلد الزامی است'),
|
||||
content: Yup.string().required('این فیلد الزامی است'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
createReview.mutate({ id: id, params: values }, {
|
||||
onSuccess: () => {
|
||||
formik.resetForm()
|
||||
refetch()
|
||||
},
|
||||
onError: (error: ErrorType) => {
|
||||
if (error.response && 'status' in error.response && error.response.status === 401) {
|
||||
window.location.href = LOGIN_URL + '?redirect=' + window.location.href
|
||||
} else {
|
||||
toast.error(error.response?.data?.error?.message?.[0] || 'An error occurred')
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='bg-white p-6 rounded-3xl xl:w-[360px] w-full'>
|
||||
<div>
|
||||
ارسال نظر
|
||||
</div>
|
||||
|
||||
|
||||
{
|
||||
!isLogin ? (
|
||||
<div className='mt-6 text-description text-sm'>
|
||||
<div>
|
||||
برای ارسال نظر لطفا وارد حساب کاربری خود شوید
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex justify-center'>
|
||||
<Link href={LOGIN_URL + '?redirect=' + window.location.href}>
|
||||
<Button
|
||||
label={'ورود | ثبت نام'}
|
||||
className='w-fit px-7'
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-6 text-description text-sm'>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Input
|
||||
label={'عنوان'}
|
||||
placeholder={'عنوان'}
|
||||
{...formik.getFieldProps('title')}
|
||||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Textarea
|
||||
label={'نظر'}
|
||||
placeholder={'نظر'}
|
||||
{...formik.getFieldProps('content')}
|
||||
error_text={formik.touched.content && formik.errors.content ? formik.errors.content : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Button
|
||||
label={'ارسال نظر'}
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={createReview.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateReview
|
||||
@@ -1,20 +1,21 @@
|
||||
'use client'
|
||||
import { Profile, Calendar } from 'iconsax-react'
|
||||
import { Profile, Calendar, Tag } from 'iconsax-react'
|
||||
import { FC } from 'react'
|
||||
import Image from 'next/image'
|
||||
// import MostVisited from '../components/MostVisited'
|
||||
import ContactUs from '../components/ContactUs'
|
||||
import { useGetBlogSingle } from '../hooks/useBlogsData'
|
||||
import { toJalaliDate } from '@/config/func'
|
||||
import { timeAgo, toJalaliDate } from '@/config/func'
|
||||
import MostVisited from './MostVisited'
|
||||
import { TelegramShareButton, WhatsappShareButton } from 'react-share'
|
||||
import CreateReview from './CreateReview'
|
||||
|
||||
type Props = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const SingleBlogPage: FC<Props> = ({ id }) => {
|
||||
const { data } = useGetBlogSingle(id)
|
||||
const { data, refetch } = useGetBlogSingle(id)
|
||||
|
||||
if (!data) return null
|
||||
|
||||
@@ -64,6 +65,20 @@ const SingleBlogPage: FC<Props> = ({ id }) => {
|
||||
|
||||
</p>
|
||||
|
||||
<div className='mt-6 flex items-center gap-2 text-description text-sm'>
|
||||
<Tag size={20} color='#7D818C' />
|
||||
<div>برچسب ها :</div>
|
||||
{
|
||||
data?.data?.blog?.tags.map((tag) => {
|
||||
return (
|
||||
<div key={tag} className='text-xs flex items-center h-6 rounded-md bg-[#EAEDF5] text-[#999999] px-4'>
|
||||
{tag}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex gap-4 border-y border-[#E5E5E5] py-4'>
|
||||
<div className='text-sm'>اشتراک گذاری</div>
|
||||
|
||||
@@ -84,6 +99,8 @@ const SingleBlogPage: FC<Props> = ({ id }) => {
|
||||
/>
|
||||
</TelegramShareButton>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className='xl:w-[261px]'>
|
||||
@@ -96,6 +113,50 @@ const SingleBlogPage: FC<Props> = ({ id }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex xl:flex-row flex-col gap-8 items-start'>
|
||||
<CreateReview id={data?.data?.blog?.id} refetch={refetch} />
|
||||
<div className='flex-1 w-full max-w-full bg-white p-6 rounded-3xl'>
|
||||
<div className='text-sm'>
|
||||
نظرات کاربران
|
||||
</div>
|
||||
|
||||
{
|
||||
data?.data?.comments?.length === 0 ?
|
||||
<div className='flex justify-center items-center h-full mt-7'>
|
||||
<div className='text-description text-sm'>
|
||||
هیچ نظری ثبت نشده است
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
data?.data?.comments?.map((comment) => {
|
||||
return (
|
||||
<div key={comment.id} className='mt-5 border border-border rounded-3xl p-6'>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Image alt='avatar' width={24} height={24} src={comment.user?.profilePic || '/images/default-avatar.png'} className='size-6 rounded-full object-cover' />
|
||||
<div className='text-description text-xs'>
|
||||
{comment.user?.firstName + ' ' + comment.user?.lastName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 flex justify-between'>
|
||||
<div className='text-sm max-w-[70%] truncate'>
|
||||
{comment.title}
|
||||
</div>
|
||||
<div className='text-xs text-description'>
|
||||
{timeAgo(comment.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 text-[11px] leading-6'>
|
||||
{comment.content}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import * as api from "../service/BlogService";
|
||||
import { CreateBlogCommentType } from "../types/BlogTypes";
|
||||
|
||||
// برای client
|
||||
export const useGetBlogs = (categoryId: string) =>
|
||||
@@ -38,6 +39,12 @@ export const useGetBlogSingle = (id: string) =>
|
||||
queryFn: () => api.getBlogSingle(id),
|
||||
});
|
||||
|
||||
export const useCreateBlogComment = () =>
|
||||
useMutation({
|
||||
mutationFn: (variables: { id: string; params: CreateBlogCommentType }) =>
|
||||
api.createBlogComment(variables.id, variables.params),
|
||||
});
|
||||
|
||||
// برای server prefetch
|
||||
export const blogsQueryOptions = {
|
||||
queryKey: ["blogs", ""],
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
BlogCombinedResponse,
|
||||
BlogResponse,
|
||||
BlogSingleResponse,
|
||||
CreateBlogCommentType,
|
||||
} from "../types/BlogTypes";
|
||||
|
||||
export const getBlogs = async (categoryId: string) => {
|
||||
@@ -41,3 +42,14 @@ export const getBlogSingle = async (slug: string) => {
|
||||
const { data } = await axios.get<BlogSingleResponse>(`/blogs/slug/${slug}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createBlogComment = async (
|
||||
id: string,
|
||||
params: CreateBlogCommentType
|
||||
) => {
|
||||
const { data } = await axios.post<BlogSingleResponse>(
|
||||
`/blogs/${id}/comments`,
|
||||
params
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface Blog {
|
||||
author: Author;
|
||||
category: Category;
|
||||
content: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface Pager {
|
||||
@@ -59,6 +60,28 @@ export type BlogStoreType = {
|
||||
setSelectedCategory: (category: string) => void;
|
||||
};
|
||||
|
||||
export type Comment = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
title: string;
|
||||
content: string;
|
||||
status: string;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
profilePic: string;
|
||||
};
|
||||
|
||||
export type BlogSingleResponse = ApiResponse<{
|
||||
blog: Blog;
|
||||
comments: Comment[];
|
||||
}>;
|
||||
|
||||
export type CreateBlogCommentType = {
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ const Contact: NextPage = () => {
|
||||
color='black'
|
||||
/>
|
||||
<div className='text-sm'>
|
||||
۰۲۱-۲۱۲۳۲۱۲۳ | ۰۸۶-۲۱۰۰۲۰۰۱
|
||||
۰۲۱-۹۱۶۹۳۵۳۳
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,7 +85,7 @@ const Contact: NextPage = () => {
|
||||
color='black'
|
||||
/>
|
||||
<div className='text-sm'>
|
||||
یوسف آباد، فتجی شقایق، پلاک ۵ ، واحد ۱۳
|
||||
تهران - یوسف آباد - فتحی شقاقی - پلاک ۵ - واحد ۱۳
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ const DanakServicesContent: FC = () => {
|
||||
</h1>
|
||||
|
||||
<div className='max-w-[300px] text-sm leading-6'>
|
||||
ما در داناک با ترکیب خلاقیت، تخصص و تکنولوژیهای روز، راهکارهای نرمافزاری منحصربهفردی ارائه میدهیم که کسبوکار شما را به سطحی بالاتر میبرد.
|
||||
داناک از ابتدای مسیر انتخاب تا استفاده از راهکارهای نرمافزاری در کنار شما خواهد بود.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const Developers: NextPage = () => {
|
||||
</h1>
|
||||
|
||||
<div className='flex xl:flex-row flex-col xl:gap-24 gap-10 mt-14'>
|
||||
<div className='flex-[2] font-bold'>
|
||||
<div className='flex-[2] leading-6 font-bold'>
|
||||
چه در قالب فرد و یا شرکت ایده منحصر به فرد و خلاقانه ای دارید که نیاز به مشتری و گسترش بازار دارید؟
|
||||
</div>
|
||||
<div className='flex-[3] text-sm'>
|
||||
|
||||
@@ -110,40 +110,47 @@ const DetailService: FC<{ id: string }> = ({ id }) => {
|
||||
</div>
|
||||
|
||||
{
|
||||
getDetailService.data?.data.danakService?.reviews?.map((item: ReviewItem) => {
|
||||
return (
|
||||
<div key={item.id} className='mt-5 border border-border rounded-3xl p-6'>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Image alt='avatar' width={24} height={24} src={item.user?.profilePic || defaultAvatar} className='size-6 rounded-full object-cover' />
|
||||
<div className='text-description text-xs'>
|
||||
{item.user?.firstName + ' ' + item.user?.lastName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 flex justify-between'>
|
||||
<div className='text-sm max-w-[70%] truncate'>
|
||||
{item.title}
|
||||
</div>
|
||||
<div className='text-xs text-description'>
|
||||
{moment(item.createdAt).fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-2'>
|
||||
<Rate
|
||||
disabled
|
||||
value={item.rating}
|
||||
count={5}
|
||||
style={{ fontSize: 14 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 text-[11px] leading-6'>
|
||||
{item.comment}
|
||||
</div>
|
||||
getDetailService.data?.data.danakService?.reviews?.length === 0 ?
|
||||
<div className='flex justify-center items-center h-full mt-7'>
|
||||
<div className='text-description text-sm'>
|
||||
هیچ نظری ثبت نشده است
|
||||
</div>
|
||||
)
|
||||
})
|
||||
</div>
|
||||
:
|
||||
getDetailService.data?.data.danakService?.reviews?.map((item: ReviewItem) => {
|
||||
return (
|
||||
<div key={item.id} className='mt-5 border border-border rounded-3xl p-6'>
|
||||
<div className='flex gap-2 items-center'>
|
||||
<Image alt='avatar' width={24} height={24} src={item.user?.profilePic || defaultAvatar} className='size-6 rounded-full object-cover' />
|
||||
<div className='text-description text-xs'>
|
||||
{item.user?.firstName + ' ' + item.user?.lastName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 flex justify-between'>
|
||||
<div className='text-sm max-w-[70%] truncate'>
|
||||
{item.title}
|
||||
</div>
|
||||
<div className='text-xs text-description'>
|
||||
{moment(item.createdAt).fromNow()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-2'>
|
||||
<Rate
|
||||
disabled
|
||||
value={item.rating}
|
||||
count={5}
|
||||
style={{ fontSize: 14 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 text-[11px] leading-6'>
|
||||
{item.comment}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import Link from 'next/link'
|
||||
import { DanakService } from '../types/ProductTypes'
|
||||
import RadioGroup from '@/components/RadioGroup'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { CONSOLE_URL } from '@/config/const'
|
||||
type Props = {
|
||||
data: DanakService,
|
||||
}
|
||||
@@ -25,8 +25,8 @@ const ServiceHeader: FC<Props> = (props: Props) => {
|
||||
|
||||
const plan: any = data?.subscriptionPlans.find((item) => item.id === planSelected)
|
||||
|
||||
setFinalPrice(plan.finalPrice)
|
||||
setPrice(plan?.price)
|
||||
setFinalPrice(plan.price)
|
||||
setPrice(plan?.originalPrice)
|
||||
|
||||
}, [data?.subscriptionPlans, planSelected])
|
||||
|
||||
@@ -82,11 +82,11 @@ const ServiceHeader: FC<Props> = (props: Props) => {
|
||||
{NumberFormat(Number(price))}
|
||||
</div>
|
||||
}
|
||||
<div>{NumberFormat(Number(price))}</div>
|
||||
<div>{NumberFormat(Number(finalPrice))}</div>
|
||||
</div>
|
||||
<span>تومان</span>
|
||||
</div>
|
||||
<Link target='_blank' href={`https://console.danakcorp.com/services/detail/${data?.id}`}>
|
||||
<Link target='_blank' href={`${CONSOLE_URL}/other-service/buy/${data?.id}/${planSelected}`}>
|
||||
<Button
|
||||
// onClick={handleBuy}
|
||||
// isLoading={buyService.isPending}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
import { AddCircle } from 'iconsax-react'
|
||||
import React, { FC, useState, useRef, useEffect } from 'react'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const Accordion: FC<Props> = ({ title, content }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const [contentHeight, setContentHeight] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
setContentHeight(isOpen ? contentRef.current.scrollHeight : 0)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
return (
|
||||
<div className='bg-white rounded-xl p-3 flex-1 h-fit'>
|
||||
<div className='flex justify-between items-center cursor-pointer accordion-trigger' onClick={() => setIsOpen(!isOpen)}>
|
||||
<div className='font-bold'>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<AddCircle
|
||||
size={24}
|
||||
color='black'
|
||||
className={`transition-transform duration-300 accordion-icon ${isOpen ? 'rotate-45' : 'rotate-0'}`}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={contentRef}
|
||||
className='overflow-hidden transition-all duration-300 accordion-content'
|
||||
style={{ maxHeight: `${contentHeight}px` }}
|
||||
>
|
||||
<p className='text-sm leading-6 py-2 text-[#7D818C]'>
|
||||
{content}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Accordion
|
||||
@@ -33,7 +33,7 @@ const Input: FC<Props> = (props: Props) => {
|
||||
const [search, setSearch] = useState<string>('')
|
||||
|
||||
const inputClass = clx(
|
||||
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
|
||||
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-[#D0D0D0]',
|
||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
||||
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
|
||||
props.className
|
||||
@@ -80,7 +80,7 @@ const Input: FC<Props> = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<label className='text-sm'>
|
||||
<label className='text-sm text-black'>
|
||||
{props.label}
|
||||
</label>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const Textarea: FC<Props> = (props: Props) => {
|
||||
<div className='w-full relative'>
|
||||
{
|
||||
props.label &&
|
||||
<div className='text-sm' >
|
||||
<div className='text-sm text-black' >
|
||||
{props.label}
|
||||
</div>
|
||||
}
|
||||
@@ -20,7 +20,7 @@ const Textarea: FC<Props> = (props: Props) => {
|
||||
<textarea
|
||||
{...props}
|
||||
className={clx(
|
||||
'border border-border leading-7 w-full rounded-xl px-4 py-3 min-h-[100px] mt-1 text-xs',
|
||||
'border border-[#D0D0D0] leading-7 w-full rounded-xl px-4 py-3 min-h-[100px] mt-1 text-xs',
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import { QueryClient, QueryClientProvider, HydrationBoundary } from '@tanstack/react-query'
|
||||
import { IApiErrorRepsonse } from '@/types/error.types'
|
||||
import { QueryClient, QueryClientProvider, HydrationBoundary, QueryCache } from '@tanstack/react-query'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { removeToken } from './func'
|
||||
import { useSharedStore } from '@/shared/store/sharedStore'
|
||||
|
||||
export default function QueryProvider({ children }: { children: ReactNode }) {
|
||||
const { setIsLogin } = useSharedStore()
|
||||
const [client] = useState(() => new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: async (error: IApiErrorRepsonse) => {
|
||||
if (error.response?.status === 401) {
|
||||
removeToken()
|
||||
setIsLogin(false)
|
||||
}
|
||||
}
|
||||
}),
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // دادهها برای 5 دقیقه معتبر هستند
|
||||
|
||||
+2
-1
@@ -1,10 +1,11 @@
|
||||
import axios from "axios";
|
||||
import { getToken } from "./func";
|
||||
import { BASE_URL } from "./const";
|
||||
// import { Pages } from "./Pages";
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
// baseURL: process.env.NEXT_PUBLIC_BASE_URL,
|
||||
baseURL: "https://api.danakcorp.com",
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const BASE_URL = "https://api.danakcorp.com";
|
||||
export const REFRESH_TOKEN_NAME = "dsc_refresh_token";
|
||||
export const TOKEN_NAME = "dsc_token";
|
||||
export const LOGIN_URL = "https://console.danakcorp.com/auth/login";
|
||||
export const CONSOLE_URL = "https://console.danakcorp.com";
|
||||
+5
-4
@@ -1,4 +1,5 @@
|
||||
import Cookie from "js-cookie";
|
||||
import { TOKEN_NAME, REFRESH_TOKEN_NAME } from "./const";
|
||||
|
||||
export function isEmail(input: string): boolean {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
@@ -31,12 +32,12 @@ export const timeAgo = (date: string | Date): string => {
|
||||
|
||||
return "همین الان";
|
||||
};
|
||||
export const getToken = () => {
|
||||
return Cookie.get(process.env.NEXT_PUBLIC_BASE_URL || "");
|
||||
export const getToken = async () => {
|
||||
return Cookie.get(TOKEN_NAME);
|
||||
};
|
||||
|
||||
export const getRefreshToken = () => {
|
||||
return Cookie.get(process.env.VITE_REFRESH_TOKEN_NAME || "");
|
||||
return Cookie.get(REFRESH_TOKEN_NAME);
|
||||
};
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
@@ -55,7 +56,7 @@ export const setRefreshToken = (refreshToken: string) => {
|
||||
};
|
||||
|
||||
export const removeToken = () => {
|
||||
Cookie.remove(process.env.NEXT_PUBLIC_TOKEN_NAME || "", {
|
||||
Cookie.remove(TOKEN_NAME, {
|
||||
domain,
|
||||
path: "/",
|
||||
});
|
||||
|
||||
+28
-3
@@ -1,13 +1,34 @@
|
||||
'use client'
|
||||
import { FC, Fragment } from 'react'
|
||||
import { FC, Fragment, useEffect } from 'react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { HambergerMenu, Profile, SearchNormal } from 'iconsax-react'
|
||||
import Button from '@/components/Button'
|
||||
import { useSharedStore } from '@/shared/store/sharedStore'
|
||||
import { getToken } from '@/config/func'
|
||||
import { useGetMe } from './hooks/useShareData'
|
||||
import { CONSOLE_URL, LOGIN_URL } from '@/config/const'
|
||||
|
||||
const Header: FC = () => {
|
||||
const { openSidebar, setOpenSidebar } = useSharedStore()
|
||||
|
||||
const { setIsLogin, isLogin } = useSharedStore()
|
||||
|
||||
const { data } = useGetMe()
|
||||
|
||||
useEffect(() => {
|
||||
handleCheckLogin()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const handleCheckLogin = async () => {
|
||||
const token = await getToken()
|
||||
if (!token) {
|
||||
setIsLogin(false)
|
||||
} else {
|
||||
setIsLogin(true)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<div className='w-full h-[100px] bg-white/26 rounded-4xl border-[3px] border-white px-10 xl:block hidden'>
|
||||
@@ -56,12 +77,16 @@ const Header: FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a href='https://console.danakcorp.com/auth/login' target='_blank' className='border-l-2 whitespace-nowrap border-[#D8DCE4] pl-4 flex gap-2'>
|
||||
<a href={isLogin ? CONSOLE_URL + '/profile' : `${LOGIN_URL}?redirect=${typeof window !== 'undefined' ? window.location.href : ''}`} target='_blank' className='border-l-2 whitespace-nowrap border-[#D8DCE4] pl-4 flex gap-2'>
|
||||
<Profile
|
||||
size={20}
|
||||
color='black'
|
||||
/>
|
||||
<div>ورود | ثبت نام</div>
|
||||
{isLogin ? (
|
||||
<div>{`${data?.data?.user?.firstName || ''} ${data?.data?.user?.lastName || ''}`}</div>
|
||||
) : (
|
||||
<div>ورود | ثبت نام</div>
|
||||
)}
|
||||
</a>
|
||||
|
||||
<Link href='/contact'>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as api from "../service/SharedService";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useSharedStore } from "../store/sharedStore";
|
||||
|
||||
export const useGetMe = () => {
|
||||
const { isLogin } = useSharedStore();
|
||||
return useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: api.getMe,
|
||||
enabled: isLogin,
|
||||
retry: false,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import axios from "@/config/axios";
|
||||
import { UserMeResponseType } from "../types/SharedTypes";
|
||||
|
||||
export const getMe = async () => {
|
||||
const { data } = await axios.get<UserMeResponseType>("/users/me");
|
||||
return data;
|
||||
};
|
||||
@@ -4,4 +4,6 @@ import { SharedStoreType } from "../types/SharedTypes";
|
||||
export const useSharedStore = create<SharedStoreType>((set) => ({
|
||||
openSidebar: false,
|
||||
setOpenSidebar: (value) => set({ openSidebar: value }),
|
||||
isLogin: false,
|
||||
setIsLogin: (value) => set({ isLogin: value }),
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
export type SharedStoreType = {
|
||||
openSidebar: boolean;
|
||||
setOpenSidebar: (value: boolean) => void;
|
||||
|
||||
isLogin: boolean;
|
||||
setIsLogin: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
@@ -8,3 +11,27 @@ export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export type UserMeResponseType = ApiResponse<{
|
||||
user: {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
phone: string;
|
||||
birthDate: string;
|
||||
nationalCode: string;
|
||||
financialType: string;
|
||||
profilePic: string | null;
|
||||
userName: string | null;
|
||||
roles: Array<{
|
||||
id: string;
|
||||
createdAt: string;
|
||||
name: string;
|
||||
// Add other role properties as needed
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DefaultError } from "@tanstack/react-query";
|
||||
import { IBaseResponse } from "./response.types";
|
||||
|
||||
interface IError {
|
||||
message: string[];
|
||||
}
|
||||
|
||||
export interface IErrorResponse extends IBaseResponse {
|
||||
error: IError;
|
||||
}
|
||||
|
||||
export interface IApiErrorRepsonse extends DefaultError {
|
||||
response?: IErrorResponse;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface IBaseResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
}
|
||||
export interface IResponse<T> extends IBaseResponse {
|
||||
data: T;
|
||||
}
|
||||
Reference in New Issue
Block a user