redirect url + accordion + blog + ...
This commit is contained in:
+10
-42
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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'
|
'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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", ""],
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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'>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 [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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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 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
@@ -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'>
|
||||||
|
|||||||
@@ -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) => ({
|
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 }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -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
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|||||||
@@ -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