This commit is contained in:
hamid zarghami
2025-04-15 16:37:25 +03:30
parent 9bba9156a3
commit afacc7c488
26 changed files with 458 additions and 103 deletions
File diff suppressed because one or more lines are too long
+11 -9
View File
@@ -3,6 +3,7 @@ import Image from 'next/image'
import { ArrowLeft, Calendar2, Profile } from 'iconsax-react'
import { Blog } from '../types/BlogTypes'
import { toJalaliDate } from '@/config/func'
import Link from 'next/link'
interface BigBlogItemProps {
item: Blog
@@ -38,17 +39,18 @@ const BigBlogItem: FC<BigBlogItemProps> = ({ item }) => {
</div>
</div>
<p className='mt-3 text-sm leading-5'>
{item.previewContent}
<p dangerouslySetInnerHTML={{ __html: item.previewContent }} className='mt-3 text-sm leading-5'>
</p>
<div className='mt-3 flex gap-1 text-sm text-[#8C90A3]'>
<div>ادامه ی مطلب</div>
<ArrowLeft
size={18}
color='#8C90A3'
/>
</div>
<Link href={`/blogs/${item.id}`}>
<div className='mt-3 flex gap-1 text-sm text-[#8C90A3]'>
<div>ادامه ی مطلب</div>
<ArrowLeft
size={18}
color='#8C90A3'
/>
</div>
</Link>
</div>
</div>
)
+1 -2
View File
@@ -27,8 +27,7 @@ const HeroSection: FC = () => {
{item.title}
</h4>
<p className="text-xs xl:text-sm mt-4 leading-7 truncate xl:text-clip xl:whitespace-normal">
{item.previewContent}
<p dangerouslySetInnerHTML={{ __html: item.previewContent }} className="text-xs xl:text-sm mt-4 leading-7 truncate xl:text-clip xl:whitespace-normal">
</p>
</div>
</div>
+3 -3
View File
@@ -1,9 +1,9 @@
import { FC } from 'react'
import MiniBlogItem from './MiniBlogItem'
import { useGetBlogCombined } from '../hooks/useBlogsData'
import { useGetMostVisitedBlogs } from '../hooks/useBlogsData'
const MostVisited: FC = () => {
const { data } = useGetBlogCombined()
const { data } = useGetMostVisitedBlogs()
return (
<div className='w-full rounded-4xl bg-white p-4'>
<h4>
@@ -12,7 +12,7 @@ const MostVisited: FC = () => {
<div className='flex flex-col gap-6 mt-7 text-sm'>
{
data?.data?.mostVisitedBlogs?.map((item) => {
data?.data?.blogs?.map((item) => {
return <MiniBlogItem key={item.id} item={item} />
})
}
+102
View File
@@ -0,0 +1,102 @@
'use client'
import { Profile, Calendar } from 'iconsax-react'
import { NextPage } from 'next'
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 MostVisited from './MostVisited'
type Props = {
id: string
}
const SingleBlog: NextPage<Props> = ({ id }) => {
const { data } = useGetBlogSingle(id)
if (!data) return <div></div>
return (
<div className='mt-24 max-w-maxWidth mx-auto'>
<h1 className='text-2xl font-bold'>
{data?.data?.blog?.title}
</h1>
<div className='mt-10 flex gap-8'>
<div className='flex-1 bg-white rounded-4xl p-10'>
<div className='flex gap-4'>
<div className='flex gap-1'>
<div className='size-5 overflow-hidden rounded-full'>
<Image
src={data?.data?.blog?.category?.iconUrl}
alt={data?.data?.blog?.category?.title}
width={20}
height={20}
/>
</div>
<h6 className='text-[#7D818C] text-sm'>{data?.data?.blog?.category?.title}</h6>
</div>
<div className='flex gap-1'>
<Profile size={20} color='#7D818C' />
<h6 className='text-[#7D818C] text-sm'>{data?.data?.blog?.author.firstName} {data?.data?.blog?.author.lastName}</h6>
</div>
<div className='flex gap-1'>
<Calendar size={20} color='#7D818C' />
<h6 className='text-[#7D818C] text-sm'>{toJalaliDate(data?.data?.blog?.createdAt)}</h6>
</div>
</div>
<div className='mt-6'>
<Image
src={data?.data?.blog?.imageUrl}
alt={data?.data?.blog?.title}
width={1000}
height={1000}
className='w-full rounded-4xl max-h-[400px] object-cover'
/>
</div>
<p dangerouslySetInnerHTML={{ __html: data?.data?.blog?.content }} className='mt-8 text-sm leading-6'>
</p>
<div className='mt-6 flex gap-4 border-y border-[#E5E5E5] py-4'>
<div className='text-sm'>اشتراک گذاری</div>
<Image
src='/images/instagram.svg'
alt='share'
width={20}
height={20}
/>
<Image
src='/images/instagram.svg'
alt='share'
width={20}
height={20}
/>
<Image
src='/images/instagram.svg'
alt='share'
width={20}
height={20}
/>
</div>
</div>
<div className='w-[261px]'>
<MostVisited />
<div className='mt-6'>
<ContactUs />
</div>
<div className='mt-6'>
{/* <MostVisited /> */}
</div>
</div>
</div>
</div>
)
}
export default SingleBlog
+22
View File
@@ -8,6 +8,12 @@ export const useGetBlogs = () =>
queryFn: api.getBlogs,
});
export const useGetMostVisitedBlogs = () =>
useQuery({
queryKey: ["most-visited-blogs"],
queryFn: api.getMostVisitedBlogs,
});
export const useGetBlogsPinned = () =>
useQuery({
queryKey: ["blogs-pinned"],
@@ -26,12 +32,28 @@ export const useGetBlogCombined = () =>
queryFn: api.getCombinedBlogs,
});
export const useGetBlogSingle = (id: string) =>
useQuery({
queryKey: ["blog-single", id],
queryFn: () => api.getBlogSingle(id),
});
// برای server prefetch
export const blogsQueryOptions = {
queryKey: ["blogs"],
queryFn: api.getBlogs,
};
export const blogSingleQueryOptions = (id: string) => ({
queryKey: ["blog-single", id],
queryFn: () => api.getBlogSingle(id),
});
export const mostVisitedBlogsQueryOptions = {
queryKey: ["most-visited-blogs"],
queryFn: api.getMostVisitedBlogs,
};
export const blogsPinnedQueryOptions = {
queryKey: ["blogs-pinned"],
queryFn: api.getBlogsPinned,
+2 -1
View File
@@ -1,7 +1,7 @@
import { HydrationBoundary, dehydrate } from '@tanstack/react-query'
import { QueryClient } from '@tanstack/react-query'
import BlogList from './components/BlogList'
import { blogCategoriesQueryOptions, blogsQueryOptions, blogCombinedQueryOptions } from './hooks/useBlogsData'
import { blogCategoriesQueryOptions, blogsQueryOptions, blogCombinedQueryOptions, mostVisitedBlogsQueryOptions } from './hooks/useBlogsData'
export const metadata = {
title: 'مجله داناک | مقالات و آموزش‌ها',
@@ -14,6 +14,7 @@ export default async function BlogsPage() {
await queryClient.prefetchQuery(blogsQueryOptions)
await queryClient.prefetchQuery(blogCategoriesQueryOptions)
await queryClient.prefetchQuery(blogCombinedQueryOptions)
await queryClient.prefetchQuery(mostVisitedBlogsQueryOptions)
return (
<HydrationBoundary state={dehydrate(queryClient)}>
+11
View File
@@ -3,6 +3,7 @@ import {
BlogCategoryResponse,
BlogCombinedResponse,
BlogResponse,
BlogSingleResponse,
} from "../types/BlogTypes";
export const getBlogs = async () => {
@@ -10,6 +11,11 @@ export const getBlogs = async () => {
return data;
};
export const getMostVisitedBlogs = async () => {
const { data } = await axios.get<BlogResponse>(`/blogs/most-visited`);
return data;
};
export const getBlogCategories = async () => {
const { data } = await axios.get<BlogCategoryResponse>(
`/blogs/categories/public`
@@ -26,3 +32,8 @@ export const getCombinedBlogs = async () => {
const { data } = await axios.get<BlogCombinedResponse>(`/blogs/combined`);
return data;
};
export const getBlogSingle = async (id: string) => {
const { data } = await axios.get<BlogSingleResponse>(`/blogs/${id}`);
return data;
};
+5
View File
@@ -24,6 +24,7 @@ export interface Blog {
viewCount: number;
author: Author;
category: Category;
content: string;
}
export interface Pager {
@@ -57,3 +58,7 @@ export type BlogStoreType = {
selectedCategory: string;
setSelectedCategory: (category: string) => void;
};
export type BlogSingleResponse = ApiResponse<{
blog: Blog;
}>;
+8
View File
@@ -0,0 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import * as api from "../service/ContactService";
export const useCreateContact = () => {
return useMutation({
mutationFn: api.createContact,
});
};
+49 -1
View File
@@ -1,11 +1,47 @@
'use client'
import Button from '@/components/Button'
import Input from '@/components/Input'
import Textarea from '@/components/Textarea'
import { CallCalling, Location, SmsTracking } from 'iconsax-react'
import { NextPage } from 'next'
import React from 'react'
import { CreateContactType } from './types/ContactTypes'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useCreateContact } from './hooks/useContactData'
import { toast } from 'react-toastify'
import { ErrorType } from '@/helpers/types'
const Contact: NextPage = () => {
const createContact = useCreateContact()
const formik = useFormik<CreateContactType>({
initialValues: {
fullName: '',
email: '',
phone: '',
businessName: '',
content: '',
},
validationSchema: Yup.object({
fullName: Yup.string().required('نام وارد کنید'),
email: Yup.string().email('ایمیل معتبر وارد کنید').required('ایمیل وارد کنید'),
phone: Yup.string().required('شماره تماس وارد کنید'),
businessName: Yup.string().required('نام شرکت وارد کنید'),
content: Yup.string().required('پیام وارد کنید'),
}),
onSubmit: (values) => {
createContact.mutate(values, {
onSuccess: () => {
toast.success('پیام شما با موفقیت ثبت شد')
formik.resetForm()
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error.message[0])
}
})
},
})
return (
<div className='mt-24 max-w-maxWidth mx-auto'>
<h1 className='text-2xl font-bold'>
@@ -62,23 +98,33 @@ const Contact: NextPage = () => {
<div className='rowTwoInput mt-8'>
<Input
placeholder='نام و نام خانوادگی'
{...formik.getFieldProps('fullName')}
error_text={formik.touched.fullName && formik.errors.fullName ? formik.errors.fullName : undefined}
/>
<Input
placeholder='نام شرکت '
{...formik.getFieldProps('businessName')}
error_text={formik.touched.businessName && formik.errors.businessName ? formik.errors.businessName : undefined}
/>
</div>
<div className='rowTwoInput mt-8'>
<Input
placeholder='شماره تماس'
{...formik.getFieldProps('phone')}
error_text={formik.touched.phone && formik.errors.phone ? formik.errors.phone : undefined}
/>
<Input
placeholder='ایمیل'
{...formik.getFieldProps('email')}
error_text={formik.touched.email && formik.errors.email ? formik.errors.email : undefined}
/>
</div>
<div className='mt-8'>
<Textarea
label='پیام'
{...formik.getFieldProps('content')}
error_text={formik.touched.content && formik.errors.content ? formik.errors.content : undefined}
/>
</div>
@@ -86,6 +132,8 @@ const Contact: NextPage = () => {
<Button
label='ارسال'
className='w-fit px-8'
onClick={() => formik.handleSubmit()}
isLoading={createContact.isPending}
/>
</div>
</div>
@@ -0,0 +1,7 @@
import axios from "@/config/axios";
import { CreateContactType } from "../types/ContactTypes";
export const createContact = async (params: CreateContactType) => {
const { data } = await axios.post("/contact-us", params);
return data;
};
+7
View File
@@ -0,0 +1,7 @@
export type CreateContactType = {
fullName: string;
email: string;
phone: string;
businessName: string;
content: string;
};
+5
View File
@@ -94,3 +94,8 @@ tbody tr {
.swiper-button-prev {
color: white !important;
}
.Toastify__toast {
direction: rtl;
font-family: "irancell" !important;
}
+1 -1
View File
@@ -9,7 +9,7 @@ const Blog: FC = () => {
return (
<div className='xl:mt-24 mt-14 max-w-maxWidth mx-auto'>
<SectionHeader
title='بلاگ داناک'
title='مجله داناک'
/>
<div className='flex xl:flex-row flex-col gap-8 xl:mt-16 mt-10 items-center'>
+2
View File
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "swiper/css";
import "./globals.css";
import "@/assets/fonts/irancell/style.css";
import { ToastContainer } from 'react-toastify';
import Header from "@/shared/Header";
import Footer from "@/shared/Footer";
import SideBar from "@/shared/SideBar";
@@ -43,6 +44,7 @@ export default function RootLayout({
<Footer />
</div>
<ReactQueryDevtools initialIsOpen={false} />
<ToastContainer />
</QueryProvider>
</body>
@@ -0,0 +1,30 @@
'use client'
import { FC } from 'react'
import ServiceItem from '@/components/ServiceItem'
import { useGetDanakServiceSuggested } from './hooks/useProductData'
const DanakSuggestedService: FC = () => {
const { data } = useGetDanakServiceSuggested()
return (
<div className='max-w-maxWidth mx-auto xl:mt-20 mt-12'>
<h2 className='xl:text-2xl text-sm font-bold text-center xl:text-right'>
سرویس های پیشنهادی داناک
</h2>
<div className='xl:mt-14 mt-10 flex xl:flex-row flex-col gap-8'>
{
data?.data?.danakServices?.map((item) => {
return (
<ServiceItem key={item.id} item={item} />
)
})
}
</div>
</div>
)
}
export default DanakSuggestedService
@@ -2,7 +2,7 @@
import { ArrowLeft } from 'iconsax-react'
import Image from 'next/image'
import { Swiper, SwiperSlide } from 'swiper/react'
import DanakSuggestedService from '../home/DanakSuggestedService'
import DanakSuggestedService from './DanakSuggestedService'
import { FC } from 'react'
import DanakServices from './DanakServices'
+11
View File
@@ -7,11 +7,22 @@ export const useGetServicesByCategory = (categoryId?: string) =>
queryFn: () => api.getServicesByCategory(categoryId),
});
export const useGetDanakServiceSuggested = () =>
useQuery({
queryKey: ["danak-service-suggested"],
queryFn: () => api.getDanakServiceSuggested(),
});
export const servicesQueryOptions = {
queryKey: ["services", ""],
queryFn: () => api.getServicesByCategory(undefined),
};
export const danakServiceSuggestedQueryOptions = {
queryKey: ["danak-service-suggested"],
queryFn: () => api.getDanakServiceSuggested(),
};
export const useGetCategories = () =>
useQuery({
queryKey: ["service-categories"],
+3 -2
View File
@@ -2,12 +2,13 @@ import { NextPage } from 'next'
import { HydrationBoundary } from '@tanstack/react-query'
import { dehydrate } from '@tanstack/react-query'
import { QueryClient } from '@tanstack/react-query'
import { categoriesQueryOptions, servicesQueryOptions } from './hooks/useProductData'
import Products from './Products'
import { categoriesQueryOptions, danakServiceSuggestedQueryOptions, servicesQueryOptions } from './hooks/useProductData'
import Products from './ProductsPage'
const ProductsPage: NextPage = async () => {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(servicesQueryOptions)
await queryClient.prefetchQuery(categoriesQueryOptions)
await queryClient.prefetchQuery(danakServiceSuggestedQueryOptions)
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<Products />
+12 -1
View File
@@ -1,5 +1,9 @@
import axios from "@/config/axios";
import { CategoryResponse, ServiceResponse } from "../types/ProductTypes";
import {
CategoryResponse,
DanakServiceSuggestedResponse,
ServiceResponse,
} from "../types/ProductTypes";
export const getServicesByCategory = async (categoryId?: string) => {
let query = ``;
@@ -18,3 +22,10 @@ export const getCategoriesPublic = async () => {
);
return data;
};
export const getDanakServiceSuggested = async () => {
const { data } = await axios.get<DanakServiceSuggestedResponse>(
`/danak-services/suggested`
);
return data;
};
+4
View File
@@ -21,3 +21,7 @@ export type ServiceCategoryType = {
parentId: string | null;
deletedAt: string | null;
};
export type DanakServiceSuggestedResponse = ApiResponse<{
danakServices: DanakSuggestService[];
}>;
+1 -2
View File
@@ -25,8 +25,7 @@ const BlogItem: FC<Props> = ({ item }) => {
{item.title}
</h6>
<p className='mt-4 text-sm'>
{item.previewContent}
<p className='mt-4 text-sm' dangerouslySetInnerHTML={{ __html: item.previewContent }}>
</p>
</div>
</div>
+1 -1
View File
@@ -85,7 +85,7 @@ const SideBar: FC = () => {
<SideBarItem
icon={<DocumentText variant={isActive('blogs') ? 'Bold' : 'Outline'} color={isActive('blogs') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={'بلاگ'}
title={'مجله'}
isActive={isActive('blogs')}
link={'/blogs'}
/>