redirect url + accordion + blog + ...

This commit is contained in:
hamid zarghami
2025-04-21 12:09:01 +03:30
parent 486bf6a0a7
commit dbee916ad3
26 changed files with 452 additions and 103 deletions
+10 -42
View File
@@ -1,7 +1,8 @@
import InfoBox from '@/components/InfoBox' import InfoBox from '@/components/InfoBox'
import { AddCircle, TickCircle } from 'iconsax-react' import { TickCircle } from 'iconsax-react'
import { NextPage } from 'next' import { NextPage } from 'next'
import React from 'react' import React from 'react'
import Accordion from '@/components/Accordion'
import WhyDanak from '../home/WhyDanak' import WhyDanak from '../home/WhyDanak'
const About: NextPage = () => { const About: NextPage = () => {
@@ -63,19 +64,19 @@ const About: NextPage = () => {
صادرکنندگان نرم افزار صادرکنندگان نرم افزار
</div> </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 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>
<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 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>
<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 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>
<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 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>
<div className='flex-1 min-w-[30%] bg-[#ECEFF6] h-10 rounded-xl font-bold flex justify-center items-center'> <div className='flex-1 min-w-[30%] bg-[#ECEFF6] h-10 rounded-xl font-bold flex justify-center items-center'>
صادرکنندگان نرم افزار SSAE18
</div> </div>
</div> </div>
</div> </div>
@@ -93,42 +94,9 @@ const About: NextPage = () => {
</div> </div>
<div className='flex-1 flex flex-col gap-4'> <div className='flex-1 flex flex-col gap-4'>
<div className='bg-white rounded-xl p-3 flex-1 h-fit'> <Accordion title='تعهد' content='ما در داناک متعهد هستیم که بهترین خدمات و محصولات را با کیفیت بالا به مشتریان خود ارائه دهیم و همواره به دنبال بهبود و ارتقای سطح خدمات خود هستیم.' />
<div className='flex justify-between'> <Accordion title='تعهد' content='ما در داناک متعهد هستیم که بهترین خدمات و محصولات را با کیفیت بالا به مشتریان خود ارائه دهیم و همواره به دنبال بهبود و ارتقای سطح خدمات خود هستیم.' />
<div className='font-bold'> <Accordion title='تعهد' content='ما در داناک متعهد هستیم که بهترین خدمات و محصولات را با کیفیت بالا به مشتریان خود ارائه دهیم و همواره به دنبال بهبود و ارتقای سطح خدمات خود هستیم.' />
تعهد
</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>
</div> </div>
</div> </div>
-2
View File
@@ -14,7 +14,6 @@ interface PageProps {
const SingleBlog: NextPage<PageProps> = async ({ params }) => { const SingleBlog: NextPage<PageProps> = async ({ params }) => {
const { id } = await params const { id } = await params
console.log("loading", id);
return ( return (
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
@@ -24,7 +23,6 @@ const SingleBlog: NextPage<PageProps> = async ({ params }) => {
} }
async function BlogContent({ id }: { id: string }) { async function BlogContent({ id }: { id: string }) {
console.log(id)
const queryClient = new QueryClient() const queryClient = new QueryClient()
await Promise.all([ await Promise.all([
+112
View File
@@ -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
+64 -3
View File
@@ -1,20 +1,21 @@
'use client' 'use client'
import { Profile, Calendar } from 'iconsax-react' import { Profile, Calendar, Tag } from 'iconsax-react'
import { FC } from 'react' import { FC } from 'react'
import Image from 'next/image' import Image from 'next/image'
// import MostVisited from '../components/MostVisited' // import MostVisited from '../components/MostVisited'
import ContactUs from '../components/ContactUs' import ContactUs from '../components/ContactUs'
import { useGetBlogSingle } from '../hooks/useBlogsData' import { useGetBlogSingle } from '../hooks/useBlogsData'
import { toJalaliDate } from '@/config/func' import { timeAgo, toJalaliDate } from '@/config/func'
import MostVisited from './MostVisited' import MostVisited from './MostVisited'
import { TelegramShareButton, WhatsappShareButton } from 'react-share' import { TelegramShareButton, WhatsappShareButton } from 'react-share'
import CreateReview from './CreateReview'
type Props = { type Props = {
id: string id: string
} }
const SingleBlogPage: FC<Props> = ({ id }) => { const SingleBlogPage: FC<Props> = ({ id }) => {
const { data } = useGetBlogSingle(id) const { data, refetch } = useGetBlogSingle(id)
if (!data) return null if (!data) return null
@@ -64,6 +65,20 @@ const SingleBlogPage: FC<Props> = ({ id }) => {
</p> </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='mt-6 flex gap-4 border-y border-[#E5E5E5] py-4'>
<div className='text-sm'>اشتراک گذاری</div> <div className='text-sm'>اشتراک گذاری</div>
@@ -84,6 +99,8 @@ const SingleBlogPage: FC<Props> = ({ id }) => {
/> />
</TelegramShareButton> </TelegramShareButton>
</div> </div>
</div> </div>
<div className='xl:w-[261px]'> <div className='xl:w-[261px]'>
@@ -96,6 +113,50 @@ const SingleBlogPage: FC<Props> = ({ id }) => {
</div> </div>
</div> </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> </div>
) )
} }
+8 -1
View File
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import * as api from "../service/BlogService"; import * as api from "../service/BlogService";
import { CreateBlogCommentType } from "../types/BlogTypes";
// برای client // برای client
export const useGetBlogs = (categoryId: string) => export const useGetBlogs = (categoryId: string) =>
@@ -38,6 +39,12 @@ export const useGetBlogSingle = (id: string) =>
queryFn: () => api.getBlogSingle(id), queryFn: () => api.getBlogSingle(id),
}); });
export const useCreateBlogComment = () =>
useMutation({
mutationFn: (variables: { id: string; params: CreateBlogCommentType }) =>
api.createBlogComment(variables.id, variables.params),
});
// برای server prefetch // برای server prefetch
export const blogsQueryOptions = { export const blogsQueryOptions = {
queryKey: ["blogs", ""], queryKey: ["blogs", ""],
+12
View File
@@ -4,6 +4,7 @@ import {
BlogCombinedResponse, BlogCombinedResponse,
BlogResponse, BlogResponse,
BlogSingleResponse, BlogSingleResponse,
CreateBlogCommentType,
} from "../types/BlogTypes"; } from "../types/BlogTypes";
export const getBlogs = async (categoryId: string) => { 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}`); const { data } = await axios.get<BlogSingleResponse>(`/blogs/slug/${slug}`);
return data; return data;
}; };
export const createBlogComment = async (
id: string,
params: CreateBlogCommentType
) => {
const { data } = await axios.post<BlogSingleResponse>(
`/blogs/${id}/comments`,
params
);
return data;
};
+23
View File
@@ -25,6 +25,7 @@ export interface Blog {
author: Author; author: Author;
category: Category; category: Category;
content: string; content: string;
tags: string[];
} }
export interface Pager { export interface Pager {
@@ -59,6 +60,28 @@ export type BlogStoreType = {
setSelectedCategory: (category: string) => void; 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<{ export type BlogSingleResponse = ApiResponse<{
blog: Blog; blog: Blog;
comments: Comment[];
}>; }>;
export type CreateBlogCommentType = {
title: string;
content: string;
};
+2 -2
View File
@@ -59,7 +59,7 @@ const Contact: NextPage = () => {
color='black' color='black'
/> />
<div className='text-sm'> <div className='text-sm'>
۰۲۱-۲۱۲۳۲۱۲۳ | ۰۸۶-۲۱۰۰۲۰۰۱ ۰۲۱-۹۱۶۹۳۵۳۳
</div> </div>
</div> </div>
@@ -85,7 +85,7 @@ const Contact: NextPage = () => {
color='black' color='black'
/> />
<div className='text-sm'> <div className='text-sm'>
یوسف آباد، فتجی شقایق، پلاک ۵ ، واحد ۱۳ تهران - یوسف آباد - فتحی شقاقی - پلاک ۵ - واحد ۱۳
</div> </div>
</div> </div>
</div> </div>
@@ -12,7 +12,7 @@ const DanakServicesContent: FC = () => {
</h1> </h1>
<div className='max-w-[300px] text-sm leading-6'> <div className='max-w-[300px] text-sm leading-6'>
ما در داناک با ترکیب خلاقیت، تخصص و تکنولوژیهای روز، راهکارهای نرمافزاری منحصربهفردی ارائه میدهیم که کسبوکار شما را به سطحی بالاتر میبرد. داناک از ابتدای مسیر انتخاب تا استفاده از راهکارهای نرمافزاری در کنار شما خواهد بود.
</div> </div>
</div> </div>
+1 -1
View File
@@ -12,7 +12,7 @@ const Developers: NextPage = () => {
</h1> </h1>
<div className='flex xl:flex-row flex-col xl:gap-24 gap-10 mt-14'> <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>
<div className='flex-[3] text-sm'> <div className='flex-[3] text-sm'>
+40 -33
View File
@@ -110,40 +110,47 @@ const DetailService: FC<{ id: string }> = ({ id }) => {
</div> </div>
{ {
getDetailService.data?.data.danakService?.reviews?.map((item: ReviewItem) => { getDetailService.data?.data.danakService?.reviews?.length === 0 ?
return ( <div className='flex justify-center items-center h-full mt-7'>
<div key={item.id} className='mt-5 border border-border rounded-3xl p-6'> <div className='text-description text-sm'>
<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>
}) :
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>
</div> </div>
@@ -8,7 +8,7 @@ import Link from 'next/link'
import { DanakService } from '../types/ProductTypes' import { DanakService } from '../types/ProductTypes'
import RadioGroup from '@/components/RadioGroup' import RadioGroup from '@/components/RadioGroup'
import Image from 'next/image' import Image from 'next/image'
import { CONSOLE_URL } from '@/config/const'
type Props = { type Props = {
data: DanakService, data: DanakService,
} }
@@ -25,8 +25,8 @@ const ServiceHeader: FC<Props> = (props: Props) => {
const plan: any = data?.subscriptionPlans.find((item) => item.id === planSelected) const plan: any = data?.subscriptionPlans.find((item) => item.id === planSelected)
setFinalPrice(plan.finalPrice) setFinalPrice(plan.price)
setPrice(plan?.price) setPrice(plan?.originalPrice)
}, [data?.subscriptionPlans, planSelected]) }, [data?.subscriptionPlans, planSelected])
@@ -82,11 +82,11 @@ const ServiceHeader: FC<Props> = (props: Props) => {
{NumberFormat(Number(price))} {NumberFormat(Number(price))}
</div> </div>
} }
<div>{NumberFormat(Number(price))}</div> <div>{NumberFormat(Number(finalPrice))}</div>
</div> </div>
<span>تومان</span> <span>تومان</span>
</div> </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 <Button
// onClick={handleBuy} // onClick={handleBuy}
// isLoading={buyService.isPending} // isLoading={buyService.isPending}
+47
View File
@@ -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
+2 -2
View File
@@ -33,7 +33,7 @@ const Input: FC<Props> = (props: Props) => {
const [search, setSearch] = useState<string>('') const [search, setSearch] = useState<string>('')
const inputClass = clx( 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.readOnly && 'bg-gray-100 border-0 text-description',
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10', props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
props.className props.className
@@ -80,7 +80,7 @@ const Input: FC<Props> = (props: Props) => {
return ( return (
<div className='w-full'> <div className='w-full'>
<label className='text-sm'> <label className='text-sm text-black'>
{props.label} {props.label}
</label> </label>
+2 -2
View File
@@ -12,7 +12,7 @@ const Textarea: FC<Props> = (props: Props) => {
<div className='w-full relative'> <div className='w-full relative'>
{ {
props.label && props.label &&
<div className='text-sm' > <div className='text-sm text-black' >
{props.label} {props.label}
</div> </div>
} }
@@ -20,7 +20,7 @@ const Textarea: FC<Props> = (props: Props) => {
<textarea <textarea
{...props} {...props}
className={clx( 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 props.className
)} )}
> >
+13 -1
View File
@@ -1,10 +1,22 @@
'use client' '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 { ReactNode, useState } from 'react'
import { removeToken } from './func'
import { useSharedStore } from '@/shared/store/sharedStore'
export default function QueryProvider({ children }: { children: ReactNode }) { export default function QueryProvider({ children }: { children: ReactNode }) {
const { setIsLogin } = useSharedStore()
const [client] = useState(() => new QueryClient({ const [client] = useState(() => new QueryClient({
queryCache: new QueryCache({
onError: async (error: IApiErrorRepsonse) => {
if (error.response?.status === 401) {
removeToken()
setIsLogin(false)
}
}
}),
defaultOptions: { defaultOptions: {
queries: { queries: {
staleTime: 1000 * 60 * 5, // داده‌ها برای 5 دقیقه معتبر هستند staleTime: 1000 * 60 * 5, // داده‌ها برای 5 دقیقه معتبر هستند
+2 -1
View File
@@ -1,10 +1,11 @@
import axios from "axios"; import axios from "axios";
import { getToken } from "./func"; import { getToken } from "./func";
import { BASE_URL } from "./const";
// import { Pages } from "./Pages"; // import { Pages } from "./Pages";
const axiosInstance = axios.create({ const axiosInstance = axios.create({
// baseURL: process.env.NEXT_PUBLIC_BASE_URL, // baseURL: process.env.NEXT_PUBLIC_BASE_URL,
baseURL: "https://api.danakcorp.com", baseURL: BASE_URL,
}); });
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
+5
View File
@@ -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
View File
@@ -1,4 +1,5 @@
import Cookie from "js-cookie"; import Cookie from "js-cookie";
import { TOKEN_NAME, REFRESH_TOKEN_NAME } from "./const";
export function isEmail(input: string): boolean { export function isEmail(input: string): boolean {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; 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 "همین الان"; return "همین الان";
}; };
export const getToken = () => { export const getToken = async () => {
return Cookie.get(process.env.NEXT_PUBLIC_BASE_URL || ""); return Cookie.get(TOKEN_NAME);
}; };
export const getRefreshToken = () => { 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"; const isProduction = process.env.NODE_ENV === "production";
@@ -55,7 +56,7 @@ export const setRefreshToken = (refreshToken: string) => {
}; };
export const removeToken = () => { export const removeToken = () => {
Cookie.remove(process.env.NEXT_PUBLIC_TOKEN_NAME || "", { Cookie.remove(TOKEN_NAME, {
domain, domain,
path: "/", path: "/",
}); });
+28 -3
View File
@@ -1,13 +1,34 @@
'use client' 'use client'
import { FC, Fragment } from 'react' import { FC, Fragment, useEffect } from 'react'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { HambergerMenu, Profile, SearchNormal } from 'iconsax-react' import { HambergerMenu, Profile, SearchNormal } from 'iconsax-react'
import Button from '@/components/Button' import Button from '@/components/Button'
import { useSharedStore } from '@/shared/store/sharedStore' 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 Header: FC = () => {
const { openSidebar, setOpenSidebar } = useSharedStore() 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 ( return (
<Fragment> <Fragment>
<div className='w-full h-[100px] bg-white/26 rounded-4xl border-[3px] border-white px-10 xl:block hidden'> <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> </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 <Profile
size={20} size={20}
color='black' color='black'
/> />
<div>ورود | ثبت نام</div> {isLogin ? (
<div>{`${data?.data?.user?.firstName || ''} ${data?.data?.user?.lastName || ''}`}</div>
) : (
<div>ورود | ثبت نام</div>
)}
</a> </a>
<Link href='/contact'> <Link href='/contact'>
+13
View File
@@ -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,
});
};
+7
View File
@@ -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;
};
+2
View File
@@ -4,4 +4,6 @@ import { SharedStoreType } from "../types/SharedTypes";
export const useSharedStore = create<SharedStoreType>((set) => ({ export const useSharedStore = create<SharedStoreType>((set) => ({
openSidebar: false, openSidebar: false,
setOpenSidebar: (value) => set({ openSidebar: value }), setOpenSidebar: (value) => set({ openSidebar: value }),
isLogin: false,
setIsLogin: (value) => set({ isLogin: value }),
})); }));
+27
View File
@@ -1,6 +1,9 @@
export type SharedStoreType = { export type SharedStoreType = {
openSidebar: boolean; openSidebar: boolean;
setOpenSidebar: (value: boolean) => void; setOpenSidebar: (value: boolean) => void;
isLogin: boolean;
setIsLogin: (value: boolean) => void;
}; };
export interface ApiResponse<T> { export interface ApiResponse<T> {
@@ -8,3 +11,27 @@ export interface ApiResponse<T> {
success: boolean; success: boolean;
data: T; 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
}>;
};
}>;
+14
View File
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
export interface IBaseResponse {
status: number;
success: boolean;
}
export interface IResponse<T> extends IBaseResponse {
data: T;
}