landing api

This commit is contained in:
hamid zarghami
2025-04-14 15:31:57 +03:30
parent 15af39fc84
commit 74c47893dd
25 changed files with 3365 additions and 124 deletions
+2859 -14
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -9,7 +9,11 @@
"lint": "next lint"
},
"dependencies": {
"@tanstack/react-query": "^5.73.3",
"@tanstack/react-query-devtools": "^5.73.3",
"axios": "^1.8.4",
"iconsax-react": "^0.0.8",
"js-cookie": "^3.0.5",
"next": "15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -21,11 +25,14 @@
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.2.3",
"i": "^0.3.7",
"npm": "^11.3.0",
"tailwindcss": "^4",
"typescript": "^5"
}
+13 -6
View File
@@ -1,13 +1,20 @@
import { FC } from 'react'
import Image from 'next/image'
import { ArrowLeft, Calendar2, Profile } from 'iconsax-react'
const BigBlogItem: FC = () => {
import { Blog } from '../types/BlogTypes'
import { toJalaliDate } from '@/config/func'
interface BigBlogItemProps {
item: Blog
}
const BigBlogItem: FC<BigBlogItemProps> = ({ item }) => {
return (
<div className='w-full rounded-4xl bg-white p-7 flex xl:flex-row flex-col gap-6'>
<Image src='https://picsum.photos/seed/picsum/200/300' alt='blog' width={300} height={300} className='size-[150px] object-cover mx-auto xl:mx-0 rounded-4xl' />
<Image src={item.imageUrl} alt='blog' width={300} height={300} className='size-[150px] object-cover mx-auto xl:mx-0 rounded-4xl' />
<div className='flex-1'>
<h4 className='font-bold text-sm leading-6'>
سفارش نرمافزار اختصاصی: سرمایه‌ای برای آینده یا فقط هزینهای برای امروز؟
{item.title}
</h4>
<div className='mt-3 flex gap-3 text-xs'>
@@ -17,7 +24,7 @@ const BigBlogItem: FC = () => {
color='#7D818C'
/>
<h6 className='text-sm'>
نویسنده
{item.author.firstName} {item.author.lastName}
</h6>
</div>
<div className='flex text-[#7D818C] items-center gap-1'>
@@ -26,13 +33,13 @@ const BigBlogItem: FC = () => {
color='#7D818C'
/>
<h6 className='text-sm'>
۱۴۰۳/۱۲/۰۱
{toJalaliDate(item.createdAt)}
</h6>
</div>
</div>
<p className='mt-3 text-sm leading-5'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است چاپگرها و متون بلکه...
{item.previewContent}
</p>
<div className='mt-3 flex gap-1 text-sm text-[#8C90A3]'>
+46
View File
@@ -0,0 +1,46 @@
'use client'
import { FC } from 'react'
import HeroSection from './HeroSection'
import Category from './Category'
import BigBlogItem from './BigBlogItem'
import MostVisited from './MostVisited'
import ContactUs from './ContactUs'
import { useGetBlogs } from '../hooks/useBlogsData'
const BlogList: FC = () => {
const { data } = useGetBlogs()
return (
<div className=''>
<HeroSection />
<h1 className='text-2xl font-bold mt-10'>
مجله داناک
</h1>
<div className='mt-10 flex xl:flex-row flex-col gap-8'>
<div className='xl:w-[261px] w-full'>
<Category />
</div>
<div className='flex-1 flex flex-col gap-8'>
{data?.data.blogs.map((item) => (
<BigBlogItem key={item.id} item={item} />
))}
</div>
<div className='xl:w-[261px] w-full'>
<MostVisited />
<div className='mt-6'>
<ContactUs />
</div>
<div className='mt-6'>
<MostVisited />
</div>
</div>
</div>
</div>
)
}
export default BlogList
+15
View File
@@ -0,0 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import { getBlogs } from "../service/BlogService";
// برای client
export const useGetBlogs = () =>
useQuery({
queryKey: ["blogs"],
queryFn: getBlogs,
});
// برای server prefetch
export const blogsQueryOptions = {
queryKey: ["blogs"],
queryFn: getBlogs,
};
+17 -38
View File
@@ -1,41 +1,20 @@
import { FC } from 'react'
import HeroSection from './components/HeroSection'
import Category from './components/Category'
import BigBlogItem from './components/BigBlogItem'
import MostVisited from './components/MostVisited'
import ContactUs from './components/ContactUs'
const Blogs: FC = () => {
return (
<div className=''>
<HeroSection />
import { HydrationBoundary, dehydrate } from '@tanstack/react-query'
import { QueryClient } from '@tanstack/react-query'
import BlogList from './components/BlogList'
import { blogsQueryOptions } from './hooks/useBlogsData'
<h1 className='text-2xl font-bold mt-10'>
مجله داناک
</h1>
<div className='mt-10 flex xl:flex-row flex-col gap-8'>
<div className='xl:w-[261px] w-full'>
<Category />
</div>
<div className='flex-1 flex flex-col gap-8'>
<BigBlogItem />
<BigBlogItem />
<BigBlogItem />
</div>
<div className='xl:w-[261px] w-full'>
<MostVisited />
<div className='mt-6'>
<ContactUs />
</div>
<div className='mt-6'>
<MostVisited />
</div>
</div>
</div>
</div>
)
export const metadata = {
title: 'مجله داناک | مقالات و آموزش‌ها',
description: 'مقالات جذاب، نکات آموزشی و اخبار دنیای نرم‌افزار در مجله داناک',
}
export default Blogs
export default async function BlogsPage() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(blogsQueryOptions)
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<BlogList />
</HydrationBoundary>
)
}
+12
View File
@@ -0,0 +1,12 @@
import axios from "@/config/axios";
import { BlogResponse } from "../types/BlogTypes";
export const getBlogs = async () => {
const { data } = await axios.get<BlogResponse>(`/blogs`);
return data;
};
export const getBlogCategories = async () => {
const { data } = await axios.get<BlogResponse>(`/blogs/categories/public`);
return data;
};
+43
View File
@@ -0,0 +1,43 @@
export interface Author {
id: string;
firstName: string;
lastName: string;
}
export interface Category {
id: string;
title: string;
iconUrl: string;
}
export interface Blog {
id: string;
createdAt: string;
title: string;
slug: string;
previewContent: string;
imageUrl: string;
metaTitle: string;
metaDescription: string;
viewCount: number;
author: Author;
category: Category;
}
export interface Pager {
page: number;
limit: number;
totalItems: number;
totalPages: number;
prevPage: boolean;
nextPage: boolean;
}
export interface BlogResponse {
statusCode: number;
success: boolean;
data: {
pager: Pager;
blogs: Blog[];
};
}
+8
View File
@@ -86,3 +86,11 @@ tbody tr {
.rowTwoInput {
@apply flex flex-col sm:flex-row sm:gap-5 gap-5;
}
.swiper-button-next {
color: white !important;
}
.swiper-button-prev {
color: white !important;
}
+11 -3
View File
@@ -1,8 +1,11 @@
import BlogItem from '@/components/BlogItem'
import SectionHeader from '@/components/SectionHeader'
import { FC } from 'react'
import { useGetLanding } from './hooks/useHomeData'
const Blog: FC = () => {
const { data } = useGetLanding()
return (
<div className='xl:mt-24 mt-14 max-w-maxWidth mx-auto'>
<SectionHeader
@@ -10,9 +13,14 @@ const Blog: FC = () => {
/>
<div className='flex xl:flex-row flex-col gap-8 xl:mt-16 mt-10 items-center'>
<BlogItem />
<BlogItem />
<BlogItem />
{
data?.data?.pinnedBlogs?.map((item) => {
return (
<BlogItem key={item.id} item={item} />
)
})
}
</div>
</div>
)
+3 -1
View File
@@ -4,9 +4,10 @@ import ServiceItem from '@/components/ServiceItem'
import { clx } from '@/helpers/utils'
import { FC, useState } from 'react'
import { Swiper, SwiperSlide } from 'swiper/react'
import { useGetLanding } from './hooks/useHomeData'
const DanakServices: FC = () => {
const { data } = useGetLanding()
const [categoryId, setCategoryId] = useState<string>('')
return (
@@ -49,6 +50,7 @@ const DanakServices: FC = () => {
</div>
<div className='xl:mt-16 mt-10 flex flex-wrap gap-8'>
<div className='xl:min-w-[30%] min-w-[70%] flex-1'>
<ServiceItem
isShort
+13 -2
View File
@@ -1,7 +1,12 @@
'use client'
import { FC } from 'react'
import ServiceItem from '@/components/ServiceItem'
import { useGetLanding } from './hooks/useHomeData'
const DanakSuggestedService: FC = () => {
const { data } = useGetLanding()
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'>
@@ -9,8 +14,14 @@ const DanakSuggestedService: FC = () => {
</h2>
<div className='xl:mt-14 mt-10 flex xl:flex-row flex-col gap-8'>
<ServiceItem />
<ServiceItem />
{
data?.data?.danakSuggestServices?.map((item) => {
return (
<ServiceItem key={item.id} item={item} />
)
})
}
</div>
</div>
)
+36 -19
View File
@@ -1,30 +1,47 @@
import 'swiper/css/navigation';
import Button from '@/components/Button'
import Image from 'next/image'
import { FC } from 'react'
import { Navigation } from 'swiper/modules'
import { Swiper, SwiperSlide } from 'swiper/react'
import { useGetLanding } from './hooks/useHomeData';
const HeroSection: FC = () => {
const { data } = useGetLanding()
return (
<div className='relative'>
<Image
src={'/images/banner.webp'}
alt='banner'
width={1920}
height={1080}
className='w-full rounded-4xl object-cover xl:mt-14 mt-8 min-h-[210px]'
/>
<Swiper navigation={true} modules={[Navigation]} className="mySwiper">
{
data?.data?.sliders?.map((item) => {
return (
<SwiperSlide key={item.id}>
<div className='relative'>
<Image
src={item.imageUrl}
alt='banner'
width={1920}
height={1080}
className='w-full rounded-4xl object-cover xl:mt-14 mt-8 min-h-[210px]'
/>
<div className='absolute flex items-center z-1 top-0 left-0 w-full h-full bg-black/10 rounded-4xl'>
<div className='max-w-maxWidth w-full mx-auto px-10 xl:text-4xl text-lg text-white'>
<p>داناک،</p>
<p className='xl:mt-5 font-bold'>تلفیق هنر و تکنولوژی</p>
<div className='absolute flex items-center z-1 top-0 left-0 w-full h-full bg-black/10 rounded-4xl'>
<div className='max-w-maxWidth w-full mx-auto px-10 xl:text-4xl text-lg text-white'>
<p>{item.title}</p>
<p className='xl:mt-5 font-bold'>{item.description}</p>
<Button
className='xl:mt-12 mt-5 !w-fit px-10 bg-white !text-black'
label='درباره ی ما'
/>
</div>
</div>
</div>
<Button
className='xl:mt-12 mt-5 !w-fit px-10 bg-white !text-black'
label='درباره ی ما'
/>
</div>
</div>
</div>
</SwiperSlide>
)
})
}
</Swiper>
)
}
+33
View File
@@ -0,0 +1,33 @@
'use client'
import { NextPage } from 'next'
import HeroSection from './HeroSection'
import DanakSuggestedService from './DanakSuggestedService'
import WhyDanak from './WhyDanak'
import Blog from './Blog'
import Collaboration from './Collaboration'
const Home: NextPage = () => {
return (
<div>
<HeroSection />
<DanakSuggestedService />
<WhyDanak />
{/* <DanakServices /> */}
<Blog />
<Collaboration />
</div>
)
}
export default Home
+15
View File
@@ -0,0 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import * as api from "../service/HomeService";
// برای client
export const useGetLanding = () =>
useQuery({
queryKey: ["landing"],
queryFn: api.getLanding,
});
// برای server prefetch
export const landingQueryOptions = {
queryKey: ["landing"],
queryFn: api.getLanding,
};
+7
View File
@@ -0,0 +1,7 @@
import axios from "@/config/axios";
import { LandingResponse } from "../types/HomeTypes";
export const getLanding = async () => {
const { data } = await axios.get<LandingResponse>(`/landing`);
return data;
};
+43
View File
@@ -0,0 +1,43 @@
import { Blog } from "@/app/blogs/types/BlogTypes";
export interface LandingResponse {
statusCode: number;
success: boolean;
data: {
sliders: SliderItemType[];
pinnedBlogs: Blog[];
danakSuggestServices: DanakSuggestService[];
};
}
export type SliderItemType = {
id: string;
createdAt: string;
updatedAt: string;
title: string;
description: string;
imageUrl: string;
link: string;
order: number;
isActive: boolean;
};
export interface DanakSuggestService {
id: string;
createdAt: string;
updatedAt: string;
name: string;
title: string;
isDanakSuggest: boolean;
description: string;
author: string;
createDate: string;
userCount: number;
serviceLanguage: string;
softwareLanguage: string;
metaDescription: string;
isActive: boolean;
link: string;
icon: string;
deletedAt: string | null;
}
+9 -6
View File
@@ -6,6 +6,7 @@ import "@/assets/fonts/irancell/style.css";
import Header from "@/shared/Header";
import Footer from "@/shared/Footer";
import SideBar from "@/shared/SideBar";
import QueryProvider from "@/config/QueryProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -32,12 +33,14 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<div className="font-irancell xl:p-16 p-4">
<SideBar />
<Header />
{children}
<Footer />
</div>
<QueryProvider>
<div className="font-irancell xl:p-16 p-4">
<SideBar />
<Header />
{children}
<Footer />
</div>
</QueryProvider>
</body>
</html>
);
+11 -25
View File
@@ -1,31 +1,17 @@
import { NextPage } from 'next'
import HeroSection from './home/HeroSection'
import DanakSuggestedService from './home/DanakSuggestedService'
import WhyDanak from './home/WhyDanak'
import DanakServices from './home/DanakServices'
import Blog from './home/Blog'
import Collaboration from './home/Collaboration'
import { HydrationBoundary } from '@tanstack/react-query'
import { dehydrate } from '@tanstack/react-query'
import { QueryClient } from '@tanstack/react-query'
import { landingQueryOptions } from './home/hooks/useHomeData'
import LangingPage from './home/LangingPage'
const Home: NextPage = async () => {
const queryClient = new QueryClient()
await queryClient.prefetchQuery(landingQueryOptions)
const Home: NextPage = () => {
return (
<div>
<HeroSection />
<DanakSuggestedService />
<WhyDanak />
<DanakServices />
<Blog />
<Collaboration />
</div>
<HydrationBoundary state={dehydrate(queryClient)}>
<LangingPage />
</HydrationBoundary>
)
}
+10 -5
View File
@@ -1,27 +1,32 @@
import { Blog } from '@/app/blogs/types/BlogTypes'
import Image from 'next/image'
import { FC } from 'react'
const BlogItem: FC = () => {
interface Props {
item: Blog
}
const BlogItem: FC<Props> = ({ item }) => {
return (
<div className='flex-1 w-full min-h-[390px] rounded-4xl overflow-hidden relative'>
<Image
src='https://picsum.photos/seed/picsum/200/300'
src={item.imageUrl}
layout='fill'
objectFit='cover'
alt='blog'
/>
<div className='absolute top-6 left-6 bg-black text-white h-8 px-5 rounded-xl flex items-center text-sm'>
دسته بندی
{item.category?.title}
</div>
<div className='absolute bottom-0 left-0 w-full modalGlass3 h-[180px] rounded-b-3xl text-white p-6'>
<h6 className='font-bold text-sm'>
سفارش نرمافزار اختصاصی: سرمایه‌ای برای آینده یا فقط هزینهای برای امروز؟
{item.title}
</h6>
<p className='mt-4 text-sm'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است چاپگرها و متون بلکه...
{item.previewContent}
</p>
</div>
</div>
+9 -4
View File
@@ -1,26 +1,31 @@
import { DanakSuggestService } from '@/app/home/types/HomeTypes'
import { ArrowLeft } from 'iconsax-react'
import Image from 'next/image'
import { FC } from 'react'
type Props = {
isShort?: boolean
item: DanakSuggestService
}
const ServiceItem: FC<Props> = (props) => {
return (
<div className='flex-1 bg-white rounded-4xl xl:p-7 p-4'>
<div className='flex justify-between'>
<div className='xl:size-[65px] size-[50px] rounded-2xl bg-orange-400'></div>
<div className='xl:size-[65px] size-[50px] rounded-2xl overflow-hidden'>
<Image src={props.item.icon} className='size-full object-cover' alt={props.item.name} width={65} height={65} />
</div>
<div className='h-8 bg-primary text-white px-4 rounded-2xl text-sm flex items-center'>
دسته بندی
</div>
</div>
<h3 className='mt-6 font-bold'>
دی منو
{props.item?.name}
</h3>
<div className='text-xs font-light mt-1 flex justify-between'>
<p>منو دیجیتال رستوران</p>
<p>{props.item.title}</p>
{
props.isShort &&
<div className='flex'>
@@ -35,7 +40,7 @@ const ServiceItem: FC<Props> = (props) => {
{
!props.isShort &&
<p className='mt-6 text-sm leading-7'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم استلورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است
{props.item.description}
</p>
}
+23
View File
@@ -0,0 +1,23 @@
'use client'
import { QueryClient, QueryClientProvider, HydrationBoundary } from '@tanstack/react-query'
import { ReactNode, useState } from 'react'
export default function QueryProvider({ children }: { children: ReactNode }) {
const [client] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // داده‌ها برای 5 دقیقه معتبر هستند
}
}
}))
return (
<QueryClientProvider client={client}>
<HydrationBoundary state={{}}>
{children}
</HydrationBoundary>
</QueryClientProvider>
)
}
+24
View File
@@ -0,0 +1,24 @@
import axios from "axios";
import { getToken } from "./func";
// import { Pages } from "./Pages";
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_BASE_URL,
});
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.request.use(async function (config) {
const tokenValue = await getToken();
const token = tokenValue ? tokenValue : "";
config.headers.Authorization = "Bearer " + token;
return config;
});
export default axiosInstance;
+97
View File
@@ -0,0 +1,97 @@
import Cookie from "js-cookie";
export function isEmail(input: string): boolean {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(input);
}
export function NumberFormat(number: number): string {
return Intl.NumberFormat("fa-IR").format(number);
// return Intl.NumberFormat("fa-IR").format(number);
}
export const timeAgo = (date: string | Date): string => {
const now = new Date();
const past = new Date(date);
const diff = Math.floor((now.getTime() - past.getTime()) / 1000); // اختلاف بر حسب ثانیه
const units = [
{ name: "سال", value: 60 * 60 * 24 * 365 },
{ name: "ماه", value: 60 * 60 * 24 * 30 },
{ name: "روز", value: 60 * 60 * 24 },
{ name: "ساعت", value: 60 * 60 },
{ name: "دقیقه", value: 60 },
{ name: "ثانیه", value: 1 },
];
for (const unit of units) {
const amount = Math.floor(diff / unit.value);
if (amount >= 1) return `${amount} ${unit.name} پیش`;
}
return "همین الان";
};
export const getToken = () => {
return Cookie.get(process.env.NEXT_PUBLIC_BASE_URL || "");
};
export const getRefreshToken = () => {
return Cookie.get(process.env.VITE_REFRESH_TOKEN_NAME || "");
};
const isProduction = process.env.NODE_ENV === "production";
const domain = isProduction ? ".danakcorp.com" : undefined;
export const setToken = (token: string) => {
Cookie.set(process.env.NEXT_PUBLIC_TOKEN_NAME || "", token, {
domain: domain || "",
});
};
export const setRefreshToken = (refreshToken: string) => {
Cookie.set(process.env.NEXT_PUBLIC_REFRESH_TOKEN_NAME || "", refreshToken, {
domain: domain || "",
});
};
export const removeToken = () => {
Cookie.remove(process.env.NEXT_PUBLIC_TOKEN_NAME || "", {
domain,
path: "/",
});
};
export const removeRefreshToken = () => {
Cookie.remove(process.env.NEXT_PUBLIC_REFRESH_TOKEN_NAME || "", {
domain,
path: "/",
});
};
export function toJalaliDate(
date: Date | string | number,
options?: Intl.DateTimeFormatOptions,
locale: string = "fa-IR-u-ca-persian"
): string {
const parsedDate = new Date(date);
if (isNaN(parsedDate.getTime())) {
throw new Error("Invalid date provided to toJalaliDate");
}
const defaultOptions: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "numeric",
day: "numeric",
// weekday: "long",
// hour: "numeric",
// minute: "numeric",
};
const formatter = new Intl.DateTimeFormat(locale, {
...defaultOptions,
...options,
});
return formatter.format(parsedDate);
}
+1 -1
View File
@@ -39,7 +39,7 @@ const Header: FC = () => {
</li>
</Link>
<Link href='/blog'>
<Link href='/blogs'>
<li>
مجله
</li>