From 1086135ecbed9b9ad703fec1c08475d9889cbfb7 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 20 Sep 2025 16:14:57 +0330 Subject: [PATCH] blogs --- src/app/blogs/components/BlogItem.tsx | 30 +++-- src/app/blogs/components/Categories.tsx | 64 +++++++-- src/app/blogs/components/LastPost.tsx | 12 +- src/app/blogs/components/MiniBlogItem.tsx | 42 +++--- src/app/blogs/components/Pagination.tsx | 138 ++++++++++++++++++++ src/app/blogs/hooks/useBlogData.ts | 37 ++++++ src/app/blogs/page.tsx | 104 +++++++++++---- src/app/blogs/service/Service.ts | 36 +++++ src/app/blogs/types/Types.ts | 65 +++++++++ src/app/home/components/BlogCard.tsx | 84 ++++++++---- src/app/home/components/Blogs.tsx | 38 ++++-- src/app/home/components/PopularProducts.tsx | 59 +++++---- src/app/home/hooks/useHomeData.ts | 7 + src/app/home/service/Service.ts | 5 + src/config/const.ts | 4 +- 15 files changed, 593 insertions(+), 132 deletions(-) create mode 100644 src/app/blogs/components/Pagination.tsx create mode 100644 src/app/blogs/hooks/useBlogData.ts create mode 100644 src/app/blogs/service/Service.ts create mode 100644 src/app/blogs/types/Types.ts diff --git a/src/app/blogs/components/BlogItem.tsx b/src/app/blogs/components/BlogItem.tsx index 44ef914..9aa1a0f 100644 --- a/src/app/blogs/components/BlogItem.tsx +++ b/src/app/blogs/components/BlogItem.tsx @@ -1,34 +1,42 @@ import { PRIMARY_COLOR } from '@/config/const' import { ArrowLeft } from 'iconsax-react' import Image from 'next/image' +import Link from 'next/link' import React from 'react' +import { Blog } from '../types/Types' -const BlogItem = () => { +interface BlogItemProps { + blog: Blog +} + +const BlogItem: React.FC = ({ blog }) => { return ( -
+
blog
- سامسونگ اولین لپ‌تاپ OLED یکپارچه را معرفی می‌کند + {blog.title_fa}

- لپ تاپ گلکسی بوک 3 سامسونگ دارای یک پنل OLED خواهد بود که در آن از فناوری سلولی استفاده می‌شود و حسگرها مستقیماً در نمایشگر قرار می‌گیرند تا صفحه لمسی نازک‌تر و سبک‌تری داشته باشد. + {blog.description}

-
-
- ادامه مطلب + +
+
+ ادامه مطلب +
+
- -
+
) diff --git a/src/app/blogs/components/Categories.tsx b/src/app/blogs/components/Categories.tsx index f72080e..47487fa 100644 --- a/src/app/blogs/components/Categories.tsx +++ b/src/app/blogs/components/Categories.tsx @@ -1,6 +1,30 @@ import { FC } from 'react' +import { BlogCategory } from '../types/Types' + +interface CategoriesProps { + categories: BlogCategory[] + selectedCategory?: string | null + onCategorySelect?: (slug: string | null) => void +} + +const Categories: FC = ({ + categories, + selectedCategory, + onCategorySelect +}) => { + if (!categories || categories.length === 0) { + return ( +
+
+ دسته بندی مطالب +
+
+ هیچ دسته‌بندی یافت نشد +
+
+ ) + } -const Categories: FC = () => { return (
@@ -8,18 +32,34 @@ const Categories: FC = () => {
-
-
موبایل
-
-
-
-
موبایل
-
-
-
-
موبایل
-
+ {/* All Categories */} +
onCategorySelect?.(null)} + className={`h-14 rounded-[10px] px-4 flex items-center justify-between transition-colors cursor-pointer ${selectedCategory === null + ? 'bg-primary text-white' + : 'bg-[#F8F8F8] text-[#8C8C8C] hover:bg-[#E8E8E8]' + }`} + > +
همه مطالب
+
+ + {/* Category Items */} + {categories.map((category) => ( +
onCategorySelect?.(category.slug)} + className={`h-14 rounded-[10px] px-4 flex items-center justify-between transition-colors cursor-pointer ${selectedCategory === category.slug + ? 'bg-primary text-white' + : 'bg-[#F8F8F8] text-[#8C8C8C] hover:bg-[#E8E8E8]' + }`} + > +
{category.title_fa}
+
+
+ ))}
) diff --git a/src/app/blogs/components/LastPost.tsx b/src/app/blogs/components/LastPost.tsx index fb7d88f..54f493c 100644 --- a/src/app/blogs/components/LastPost.tsx +++ b/src/app/blogs/components/LastPost.tsx @@ -1,7 +1,12 @@ import { FC } from 'react' import MiniBlogItem from './MiniBlogItem' +import { Blog } from '../types/Types' -const LastPost: FC = () => { +interface LastPostProps { + blogs: Blog[] +} + +const LastPost: FC = ({ blogs }) => { return (
@@ -9,8 +14,9 @@ const LastPost: FC = () => {
- - + {blogs.map((blog) => ( + + ))}
) diff --git a/src/app/blogs/components/MiniBlogItem.tsx b/src/app/blogs/components/MiniBlogItem.tsx index 218fe9b..4178542 100644 --- a/src/app/blogs/components/MiniBlogItem.tsx +++ b/src/app/blogs/components/MiniBlogItem.tsx @@ -1,27 +1,35 @@ import Image from 'next/image' +import Link from 'next/link' import React from 'react' +import { Blog } from '../types/Types' -const MiniBlogItem = () => { +interface MiniBlogItemProps { + blog: Blog +} + +const MiniBlogItem: React.FC = ({ blog }) => { return ( -
- last post + +
+ {blog.title_fa} -
-
- رندرهای گلکسی M54 5G -
+
+
+ {blog.title_fa} +
-

- طبق رندرها، گلکسی M54 5G فاقد ماژول دوربین است و سنسورهای دوربین هر.... -

+

+ {blog.description} +

+
-
+ ) } diff --git a/src/app/blogs/components/Pagination.tsx b/src/app/blogs/components/Pagination.tsx new file mode 100644 index 0000000..71cc7c5 --- /dev/null +++ b/src/app/blogs/components/Pagination.tsx @@ -0,0 +1,138 @@ +import { FC } from 'react' +import { Button } from '@/components/ui/button' +import { ChevronLeft, ChevronRight } from 'lucide-react' + +interface PaginationProps { + currentPage: number + totalPages: number + totalItems: number + itemsPerPage: number + onPageChange: (page: number) => void +} + +const Pagination: FC = ({ + currentPage, + totalPages, + totalItems, + itemsPerPage, + onPageChange +}) => { + if (totalPages <= 1) return null + + const startItem = (currentPage - 1) * itemsPerPage + 1 + const endItem = Math.min(currentPage * itemsPerPage, totalItems) + + const getPageNumbers = () => { + const pages = [] + const maxVisiblePages = 5 + + if (totalPages <= maxVisiblePages) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i) + } + } else { + if (currentPage <= 3) { + for (let i = 1; i <= 5; i++) { + pages.push(i) + } + } else if (currentPage >= totalPages - 2) { + for (let i = totalPages - 4; i <= totalPages; i++) { + pages.push(i) + } + } else { + for (let i = currentPage - 2; i <= currentPage + 2; i++) { + pages.push(i) + } + } + } + + return pages + } + + const pageNumbers = getPageNumbers() + + return ( +
+ {/* Pagination Info */} +
+ نمایش {startItem} تا {endItem} از {totalItems} مطلب +
+ + {/* Pagination Controls */} +
+ + +
+ {/* First page */} + {currentPage > 3 && totalPages > 5 && ( + <> + + {currentPage > 4 && ( + ... + )} + + )} + + {/* Page numbers */} + {pageNumbers.map((pageNumber) => ( + + ))} + + {/* Last page */} + {currentPage < totalPages - 2 && totalPages > 5 && ( + <> + {currentPage < totalPages - 3 && ( + ... + )} + + + )} +
+ + +
+
+ ) +} + +export default Pagination diff --git a/src/app/blogs/hooks/useBlogData.ts b/src/app/blogs/hooks/useBlogData.ts new file mode 100644 index 0000000..cfefba1 --- /dev/null +++ b/src/app/blogs/hooks/useBlogData.ts @@ -0,0 +1,37 @@ +import * as api from "../service/Service"; +import { useQuery } from "@tanstack/react-query"; +import { BlogResponse, BlogCategory } from "../types/Types"; + +export const useGetBlogs = (page = 1, limit = 20) => { + return useQuery({ + queryKey: ["blogs", page, limit], + queryFn: () => api.getBlogs(page, limit), + }); +}; + +export const useGetBlogCategories = () => { + return useQuery({ + queryKey: ["blog-categories"], + queryFn: () => api.getBlogCategories(), + }); +}; + +export const useGetBlogDetails = (id: string) => { + return useQuery({ + queryKey: ["blog-details", id], + queryFn: () => api.getBlogDetails(id), + enabled: !!id, + }); +}; + +export const useGetBlogsByCategory = ( + slug: string, + page: number = 1, + limit: number = 10 +) => { + return useQuery({ + queryKey: ["blogs-by-category", slug, page, limit], + queryFn: () => api.getBlogsByCategory(slug, page, limit), + enabled: !!slug, + }); +}; diff --git a/src/app/blogs/page.tsx b/src/app/blogs/page.tsx index 89d1935..4d48808 100644 --- a/src/app/blogs/page.tsx +++ b/src/app/blogs/page.tsx @@ -1,37 +1,91 @@ +'use client' import { NextPage } from 'next' +import { useState } from 'react' import Layout from '@/hoc/Layout' import Categories from './components/Categories' import BlogItem from './components/BlogItem' import LastPost from './components/LastPost' +import Pagination from './components/Pagination' +import { useGetBlogCategories, useGetBlogs, useGetBlogsByCategory } from './hooks/useBlogData' +import PageLoading from '@/components/PageLoading' +import Error from '@/components/Error' const BlogsPage: NextPage = () => { - return ( -
- {/* Categories - Hidden on mobile, shown on desktop */} -
- -
+ const [currentPage, setCurrentPage] = useState(1) + const [selectedCategory, setSelectedCategory] = useState(null) - {/* Main content */} -
- - - - -
+ const { data: blogCategories, isLoading: categoriesLoading, error: categoriesError } = useGetBlogCategories() - {/* Last Post - Full width on mobile, sidebar on desktop */} -
- -
-
- ) + // Always call both hooks, but use the appropriate one + const { data: allBlogs, isLoading: allBlogsLoading, error: allBlogsError } = useGetBlogs(currentPage, 10) + const { data: categoryBlogs, isLoading: categoryBlogsLoading, error: categoryBlogsError } = useGetBlogsByCategory(selectedCategory || '', currentPage, 10) + + // Use the appropriate data based on selected category + const blogs = selectedCategory ? categoryBlogs : allBlogs + const blogsLoading = selectedCategory ? categoryBlogsLoading : allBlogsLoading + const blogsError = selectedCategory ? categoryBlogsError : allBlogsError + + if (blogsLoading || categoriesLoading) { + return + } + + if (blogsError || categoriesError) { + return + } + + const handlePageChange = (page: number) => { + setCurrentPage(page) + window.scrollTo({ top: 0, behavior: 'smooth' }) + } + + const handleCategorySelect = (slug: string | null) => { + setSelectedCategory(slug) + setCurrentPage(1) // Reset to first page when changing category + } + + const pager = blogs?.results?.pager + + return ( +
+ {/* Categories - Hidden on mobile, shown on desktop */} +
+ +
+ + {/* Main content */} +
+ {blogs?.results?.blogs?.map((blog) => ( + + ))} + + {/* Pagination */} + {pager && ( + + )} +
+ + {/* Last Post - Full width on mobile, sidebar on desktop */} +
+ +
+
+ ) } export default function BlogsPageWithLayout() { - return ( - - - - ) -} \ No newline at end of file + return ( + + + + ) +} diff --git a/src/app/blogs/service/Service.ts b/src/app/blogs/service/Service.ts new file mode 100644 index 0000000..4db4ace --- /dev/null +++ b/src/app/blogs/service/Service.ts @@ -0,0 +1,36 @@ +import axios from "@/config/axios"; +import { BlogResponse, BlogCategory } from "../types/Types"; + +export const getBlogs = async ( + page: number, + limit: number +): Promise => { + const { data } = await axios.get(`/blogs?page=${page}&limit=${limit}`); + return data; +}; + +export const getBlogCategories = async (): Promise => { + try { + const { data } = await axios.get("/blogs/categories"); + return data.results?.categories || []; + } catch (error) { + console.error("Error fetching blog categories:", error); + return []; + } +}; + +export const getBlogDetails = async (id: string): Promise => { + const { data } = await axios.get(`/blogs/${id}`); + return data; +}; + +export const getBlogsByCategory = async ( + slug: string, + page: number = 1, + limit: number = 10 +): Promise => { + const { data } = await axios.get( + `/blogs/category/${slug}?page=${page}&limit=${limit}` + ); + return data; +}; diff --git a/src/app/blogs/types/Types.ts b/src/app/blogs/types/Types.ts new file mode 100644 index 0000000..5a7129c --- /dev/null +++ b/src/app/blogs/types/Types.ts @@ -0,0 +1,65 @@ +export interface BlogCategory { + _id: string; + title_fa: string; + title_en: string; + slug: string; + icon: string; + imageUrl: string; + description: string; + variants: number[]; + hierarchy: string[]; + leaf: boolean; + parent: string | null; + deleted: boolean; + createdAt: string; + updatedAt: string; + url: string; + children: BlogCategory[]; + theme?: string; +} + +export interface BlogAuthor { + fullName: string; +} + +export interface Blog { + _id: string; + title_fa: string; + slug: string; + category: BlogCategory; + description: string; + content: string; + author: BlogAuthor; + view: number; + tags: string[]; + imageUrl: string; + createdAt: string; +} + +export interface BlogPager { + page: number; + limit: number; + totalItems: number; + totalPages: number; + prevPage: boolean; + nextPage: boolean; +} + +export interface BlogResults { + pager: BlogPager; + blogs: Blog[]; +} + +export interface BlogCategoryResponse { + status: number; + success: boolean; + results: { + data: BlogCategory[]; + }; +} + +export interface BlogResponse { + status: number; + success: boolean; + results: BlogResults; +} diff --git a/src/app/home/components/BlogCard.tsx b/src/app/home/components/BlogCard.tsx index 4169f2f..f1c7076 100644 --- a/src/app/home/components/BlogCard.tsx +++ b/src/app/home/components/BlogCard.tsx @@ -1,37 +1,67 @@ import { ArrowLeft } from 'iconsax-react' import Image from 'next/image' +import Link from 'next/link' import { FC } from 'react' +import { Blog } from '@/app/blogs/types/Types' +import { PRIMARY_COLOR } from '@/config/const' +import moment from 'moment-jalaali' -const BlogCard: FC = () => { - return ( -
- +interface BlogCardProps { + blog?: Blog; + isLoading?: boolean; +} -
-
تکنولوژی
-
-

- نسل جدید پردازنده اینتل -

- -

- لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است.... -

- -
-
1401/7/21
-
-
ادامه مطلب
- +const BlogCard: FC = ({ blog, isLoading = false }) => { + if (isLoading) { + return ( +
+
+
+
+
+
+
+
-
+ ) + } + + if (!blog) return null; + + return ( + +
+ {blog.title_fa} + +
+
{blog.category?.title_fa || 'عمومی'}
+
+

+ {blog.title_fa} +

+ +

+ {blog.description} +

+ +
+
+ {moment(blog.createdAt, 'jYYYY/jMM/jDD HH:mm:ss').format('jYYYY-jMM-jDD')} +
+
+
ادامه مطلب
+ +
+
+
+ ) } diff --git a/src/app/home/components/Blogs.tsx b/src/app/home/components/Blogs.tsx index ca169e0..c13ac27 100644 --- a/src/app/home/components/Blogs.tsx +++ b/src/app/home/components/Blogs.tsx @@ -1,22 +1,42 @@ +'use client' import { FC } from 'react' +import Link from 'next/link' import BlogCard from './BlogCard' import GridWrapper from '@/components/GridWrapper' +import { useGetBlogs } from '@/app/blogs/hooks/useBlogData'; const Blogs: FC = () => { + + const { data, error } = useGetBlogs(1, 5) + + if (error || !data?.results?.blogs) { + return ( +
+

رسانه و مطالب

+
+ برای مشاهده ی آخرین مطالب رسانه و مطالب اینجا کلیک کنید +
+
+
خطا در بارگذاری مطالب
+
+
+ ) + } + + const blogs = data.results.blogs + return (
-

رسانه و مطالب

+

رسانه و مطالب

- برای مشاهده ی آخرین مطالب رسانه و مطالب اینجا کلیک کنید + برای مشاهده ی آخرین مطالب رسانه و مطالب اینجا کلیک کنید
- - - - - - - + + {blogs.map((blog) => ( + + ))} +
) diff --git a/src/app/home/components/PopularProducts.tsx b/src/app/home/components/PopularProducts.tsx index 5894707..23a3dee 100644 --- a/src/app/home/components/PopularProducts.tsx +++ b/src/app/home/components/PopularProducts.tsx @@ -1,3 +1,4 @@ +'use client' import { FC } from 'react' import { Carousel, @@ -7,37 +8,43 @@ import { CarouselPrevious, } from "@/components/ui/carousel" import ProductCard from '@/components/ProductCard' +import { useGetPopularProducts } from '../hooks/useHomeData' const PopularProducts: FC = () => { - return ( -
-

محبوب ترین محصولات

-
- برای مشاهده ی محبوب ترین محصولات اینجا کلیک کنید -
-
- - - {Array.from({ length: 5 }).map((_, index) => ( - - - - ))} - - - - + const { data } = useGetPopularProducts(); + + if (!!data?.results?.popularProducts?.length) + return ( +
+

محبوب ترین محصولات

+
+ برای مشاهده ی محبوب ترین محصولات اینجا کلیک کنید +
+ +
+ + + {Array.from({ length: 5 }).map((_, index) => ( + + + + ))} + + + + +
-
- ) + ) + return null } export default PopularProducts \ No newline at end of file diff --git a/src/app/home/hooks/useHomeData.ts b/src/app/home/hooks/useHomeData.ts index 5214fef..2280364 100644 --- a/src/app/home/hooks/useHomeData.ts +++ b/src/app/home/hooks/useHomeData.ts @@ -12,3 +12,10 @@ export const getLandingQueryOptions = { queryKey: ["landing"], queryFn: api.getLanding, }; + +export const useGetPopularProducts = () => { + return useQuery({ + queryKey: ["popular-products"], + queryFn: api.getPopularProducts, + }); +}; diff --git a/src/app/home/service/Service.ts b/src/app/home/service/Service.ts index f748b52..c1205aa 100644 --- a/src/app/home/service/Service.ts +++ b/src/app/home/service/Service.ts @@ -5,3 +5,8 @@ export const getLanding = async (): Promise => { const { data } = await axios.get("/landing"); return data; }; + +export const getPopularProducts = async () => { + const { data } = await axios.get("/product/popular/all"); + return data; +}; diff --git a/src/config/const.ts b/src/config/const.ts index e2e602b..576fa4c 100644 --- a/src/config/const.ts +++ b/src/config/const.ts @@ -1,6 +1,6 @@ export const TOKEN_NAME = "sh_token"; export const REFRESH_TOKEN_NAME = "sh_refresh_token"; -// export const BASE_URL = "https://shop-api.dev.danakcorp.com"; -export const BASE_URL = "http://192.168.1.111:4000"; +export const BASE_URL = "https://shop-api.dev.danakcorp.com"; +// export const BASE_URL = "http://192.168.1.111:4000"; export const PRIMARY_COLOR = "#015699";