blogs
This commit is contained in:
@@ -1,34 +1,42 @@
|
|||||||
import { PRIMARY_COLOR } from '@/config/const'
|
import { PRIMARY_COLOR } from '@/config/const'
|
||||||
import { ArrowLeft } from 'iconsax-react'
|
import { ArrowLeft } from 'iconsax-react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Blog } from '../types/Types'
|
||||||
|
|
||||||
const BlogItem = () => {
|
interface BlogItemProps {
|
||||||
|
blog: Blog
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlogItem: React.FC<BlogItemProps> = ({ blog }) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex-1 flex flex-col sm:flex-row gap-4 sm:gap-6'>
|
<div className=' flex flex-col sm:flex-row gap-4 sm:gap-6'>
|
||||||
<Image
|
<Image
|
||||||
width={200}
|
width={200}
|
||||||
height={160}
|
height={160}
|
||||||
src={'https://picsum.photos/200/160'}
|
src={blog.imageUrl || 'https://picsum.photos/200/160'}
|
||||||
className='w-full sm:max-w-[200px] sm:max-h-[160px] h-48 sm:h-auto object-cover sm:object-contain'
|
className='w-full sm:max-w-[200px] sm:max-h-[160px] h-48 sm:h-auto object-cover sm:object-contain'
|
||||||
alt='blog'
|
alt={blog.title_fa}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<h6 className='font-bold text-base sm:text-lg'>
|
<h6 className='font-bold text-base sm:text-lg'>
|
||||||
سامسونگ اولین لپتاپ OLED یکپارچه را معرفی میکند
|
{blog.title_fa}
|
||||||
</h6>
|
</h6>
|
||||||
|
|
||||||
<p className='mt-3 sm:mt-4 text-sm line-clamp-2 sm:line-clamp-3 text-[#7F7F7F] leading-6 sm:leading-7'>
|
<p className='mt-3 sm:mt-4 text-sm line-clamp-2 sm:line-clamp-3 text-[#7F7F7F] leading-6 sm:leading-7'>
|
||||||
لپ تاپ گلکسی بوک 3 سامسونگ دارای یک پنل OLED خواهد بود که در آن از فناوری سلولی استفاده میشود و حسگرها مستقیماً در نمایشگر قرار میگیرند تا صفحه لمسی نازکتر و سبکتری داشته باشد.
|
{blog.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className='flex mt-3 sm:mt-4 text-primary gap-1.5 items-center text-sm cursor-pointer'>
|
<Link href={`/blogs/${blog._id}`}>
|
||||||
<div className='text-sm'>
|
<div className='flex mt-3 sm:mt-4 text-primary gap-1.5 items-center text-sm cursor-pointer'>
|
||||||
ادامه مطلب
|
<div className='text-sm'>
|
||||||
|
ادامه مطلب
|
||||||
|
</div>
|
||||||
|
<ArrowLeft size={18} color={PRIMARY_COLOR} />
|
||||||
</div>
|
</div>
|
||||||
<ArrowLeft size={18} color={PRIMARY_COLOR} />
|
</Link>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
import { BlogCategory } from '../types/Types'
|
||||||
|
|
||||||
|
interface CategoriesProps {
|
||||||
|
categories: BlogCategory[]
|
||||||
|
selectedCategory?: string | null
|
||||||
|
onCategorySelect?: (slug: string | null) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Categories: FC<CategoriesProps> = ({
|
||||||
|
categories,
|
||||||
|
selectedCategory,
|
||||||
|
onCategorySelect
|
||||||
|
}) => {
|
||||||
|
if (!categories || categories.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='max-w-[292px] w-full'>
|
||||||
|
<div className='h-20 bg-primary rounded-[10px] flex items-center px-4 text-white text-lg'>
|
||||||
|
دسته بندی مطالب
|
||||||
|
</div>
|
||||||
|
<div className='text-center text-sm text-[#8C8C8C] mt-4'>
|
||||||
|
هیچ دستهبندی یافت نشد
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Categories: FC = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className='max-w-[292px] w-full'>
|
<div className='max-w-[292px] w-full'>
|
||||||
<div className='h-20 bg-primary rounded-[10px] flex items-center px-4 text-white text-lg'>
|
<div className='h-20 bg-primary rounded-[10px] flex items-center px-4 text-white text-lg'>
|
||||||
@@ -8,18 +32,34 @@ const Categories: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex flex-col gap-3 mt-4'>
|
<div className='flex flex-col gap-3 mt-4'>
|
||||||
<div className='h-14 bg-[#F8F8F8] rounded-[10px] px-4 flex items-center justify-between'>
|
{/* All Categories */}
|
||||||
<div className='font-medium text-[#8C8C8C]'>موبایل</div>
|
<div
|
||||||
<div className='size-3 rounded-full bg-[#D9D9D9]'></div>
|
onClick={() => onCategorySelect?.(null)}
|
||||||
</div>
|
className={`h-14 rounded-[10px] px-4 flex items-center justify-between transition-colors cursor-pointer ${selectedCategory === null
|
||||||
<div className='h-14 bg-[#F8F8F8] rounded-[10px] px-4 flex items-center justify-between'>
|
? 'bg-primary text-white'
|
||||||
<div className='font-medium text-[#8C8C8C]'>موبایل</div>
|
: 'bg-[#F8F8F8] text-[#8C8C8C] hover:bg-[#E8E8E8]'
|
||||||
<div className='size-3 rounded-full bg-[#D9D9D9]'></div>
|
}`}
|
||||||
</div>
|
>
|
||||||
<div className='h-14 bg-[#F8F8F8] rounded-[10px] px-4 flex items-center justify-between'>
|
<div className='font-medium'>همه مطالب</div>
|
||||||
<div className='font-medium text-[#8C8C8C]'>موبایل</div>
|
<div className={`size-3 rounded-full ${selectedCategory === null ? 'bg-white' : 'bg-[#D9D9D9]'
|
||||||
<div className='size-3 rounded-full bg-[#D9D9D9]'></div>
|
}`}></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Category Items */}
|
||||||
|
{categories.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category._id}
|
||||||
|
onClick={() => 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]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className='font-medium'>{category.title_fa}</div>
|
||||||
|
<div className={`size-3 rounded-full ${selectedCategory === category.slug ? 'bg-white' : 'bg-[#D9D9D9]'
|
||||||
|
}`}></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import MiniBlogItem from './MiniBlogItem'
|
import MiniBlogItem from './MiniBlogItem'
|
||||||
|
import { Blog } from '../types/Types'
|
||||||
|
|
||||||
const LastPost: FC = () => {
|
interface LastPostProps {
|
||||||
|
blogs: Blog[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const LastPost: FC<LastPostProps> = ({ blogs }) => {
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<div className='font-bold text-base sm:text-lg mb-4'>
|
<div className='font-bold text-base sm:text-lg mb-4'>
|
||||||
@@ -9,8 +14,9 @@ const LastPost: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
<MiniBlogItem />
|
{blogs.map((blog) => (
|
||||||
<MiniBlogItem />
|
<MiniBlogItem key={blog._id} blog={blog} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,27 +1,35 @@
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { Blog } from '../types/Types'
|
||||||
|
|
||||||
const MiniBlogItem = () => {
|
interface MiniBlogItemProps {
|
||||||
|
blog: Blog
|
||||||
|
}
|
||||||
|
|
||||||
|
const MiniBlogItem: React.FC<MiniBlogItemProps> = ({ blog }) => {
|
||||||
return (
|
return (
|
||||||
<div className='flex gap-3 sm:gap-4 mt-4 sm:mt-6'>
|
<Link href={`/blogs/${blog._id}`} className='block'>
|
||||||
<Image
|
<div className='flex gap-3 sm:gap-4 mt-4 sm:mt-6'>
|
||||||
width={123}
|
<Image
|
||||||
height={123}
|
width={123}
|
||||||
src={'https://picsum.photos/123/123'}
|
height={123}
|
||||||
alt='last post'
|
src={blog.imageUrl || 'https://picsum.photos/123/123'}
|
||||||
className='w-20 h-20 sm:max-w-[100px] sm:max-h-[100px] object-cover sm:object-contain flex-shrink-0'
|
alt={blog.title_fa}
|
||||||
/>
|
className='w-20 h-20 sm:max-w-[100px] sm:max-h-[100px] object-cover sm:object-contain flex-shrink-0'
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='flex-1 min-w-0'>
|
<div className='flex-1 min-w-0'>
|
||||||
<h6 className='text-sm font-bold leading-5'>
|
<h6 className='text-sm font-bold leading-5'>
|
||||||
رندرهای گلکسی M54 5G
|
{blog.title_fa}
|
||||||
</h6>
|
</h6>
|
||||||
|
|
||||||
<p className='mt-2 sm:mt-3 text-xs text-[#7F7F7F] line-clamp-2 leading-5'>
|
<p className='mt-2 sm:mt-3 text-xs text-[#7F7F7F] line-clamp-2 leading-5'>
|
||||||
طبق رندرها، گلکسی M54 5G فاقد ماژول دوربین است و سنسورهای دوربین هر....
|
{blog.description}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<PaginationProps> = ({
|
||||||
|
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 (
|
||||||
|
<div className='mt-8'>
|
||||||
|
{/* Pagination Info */}
|
||||||
|
<div className='text-center text-sm text-muted-foreground mb-4'>
|
||||||
|
نمایش {startItem} تا {endItem} از {totalItems} مطلب
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
<div className='flex items-center justify-center gap-2'>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className='flex items-center gap-1'
|
||||||
|
>
|
||||||
|
<ChevronRight className='h-4 w-4' />
|
||||||
|
قبلی
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className='flex items-center gap-1'>
|
||||||
|
{/* First page */}
|
||||||
|
{currentPage > 3 && totalPages > 5 && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(1)}
|
||||||
|
className='w-10 h-10'
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</Button>
|
||||||
|
{currentPage > 4 && (
|
||||||
|
<span className='px-2 text-muted-foreground'>...</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Page numbers */}
|
||||||
|
{pageNumbers.map((pageNumber) => (
|
||||||
|
<Button
|
||||||
|
key={pageNumber}
|
||||||
|
variant={currentPage === pageNumber ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(pageNumber)}
|
||||||
|
className='w-10 h-10'
|
||||||
|
>
|
||||||
|
{pageNumber}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Last page */}
|
||||||
|
{currentPage < totalPages - 2 && totalPages > 5 && (
|
||||||
|
<>
|
||||||
|
{currentPage < totalPages - 3 && (
|
||||||
|
<span className='px-2 text-muted-foreground'>...</span>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(totalPages)}
|
||||||
|
className='w-10 h-10'
|
||||||
|
>
|
||||||
|
{totalPages}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className='flex items-center gap-1'
|
||||||
|
>
|
||||||
|
بعدی
|
||||||
|
<ChevronLeft className='h-4 w-4' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Pagination
|
||||||
@@ -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<BlogResponse>({
|
||||||
|
queryKey: ["blogs", page, limit],
|
||||||
|
queryFn: () => api.getBlogs(page, limit),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetBlogCategories = () => {
|
||||||
|
return useQuery<BlogCategory[]>({
|
||||||
|
queryKey: ["blog-categories"],
|
||||||
|
queryFn: () => api.getBlogCategories(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetBlogDetails = (id: string) => {
|
||||||
|
return useQuery<BlogResponse>({
|
||||||
|
queryKey: ["blog-details", id],
|
||||||
|
queryFn: () => api.getBlogDetails(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetBlogsByCategory = (
|
||||||
|
slug: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10
|
||||||
|
) => {
|
||||||
|
return useQuery<BlogResponse>({
|
||||||
|
queryKey: ["blogs-by-category", slug, page, limit],
|
||||||
|
queryFn: () => api.getBlogsByCategory(slug, page, limit),
|
||||||
|
enabled: !!slug,
|
||||||
|
});
|
||||||
|
};
|
||||||
+79
-25
@@ -1,37 +1,91 @@
|
|||||||
|
'use client'
|
||||||
import { NextPage } from 'next'
|
import { NextPage } from 'next'
|
||||||
|
import { useState } from 'react'
|
||||||
import Layout from '@/hoc/Layout'
|
import Layout from '@/hoc/Layout'
|
||||||
import Categories from './components/Categories'
|
import Categories from './components/Categories'
|
||||||
import BlogItem from './components/BlogItem'
|
import BlogItem from './components/BlogItem'
|
||||||
import LastPost from './components/LastPost'
|
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 = () => {
|
const BlogsPage: NextPage = () => {
|
||||||
return (
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
<div className='flex flex-col lg:flex-row gap-6 lg:gap-24 p-4 sm:p-6 lg:p-10'>
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
||||||
{/* Categories - Hidden on mobile, shown on desktop */}
|
|
||||||
<div className='hidden lg:block'>
|
|
||||||
<Categories />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main content */}
|
const { data: blogCategories, isLoading: categoriesLoading, error: categoriesError } = useGetBlogCategories()
|
||||||
<div className='flex-1 flex flex-col gap-6'>
|
|
||||||
<BlogItem />
|
|
||||||
<BlogItem />
|
|
||||||
<BlogItem />
|
|
||||||
<BlogItem />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Last Post - Full width on mobile, sidebar on desktop */}
|
// Always call both hooks, but use the appropriate one
|
||||||
<div className='lg:max-w-[340px] w-full'>
|
const { data: allBlogs, isLoading: allBlogsLoading, error: allBlogsError } = useGetBlogs(currentPage, 10)
|
||||||
<LastPost />
|
const { data: categoryBlogs, isLoading: categoryBlogsLoading, error: categoryBlogsError } = useGetBlogsByCategory(selectedCategory || '', currentPage, 10)
|
||||||
</div>
|
|
||||||
</div>
|
// 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 <PageLoading />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blogsError || categoriesError) {
|
||||||
|
return <Error errorText="خطا در بارگذاری مطالب" />
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className='flex flex-col lg:flex-row gap-6 lg:gap-24 p-4 sm:p-6 lg:p-10'>
|
||||||
|
{/* Categories - Hidden on mobile, shown on desktop */}
|
||||||
|
<div className='hidden lg:block'>
|
||||||
|
<Categories
|
||||||
|
categories={blogCategories || []}
|
||||||
|
selectedCategory={selectedCategory}
|
||||||
|
onCategorySelect={handleCategorySelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className='flex-1 flex flex-col gap-6'>
|
||||||
|
{blogs?.results?.blogs?.map((blog) => (
|
||||||
|
<BlogItem key={blog._id} blog={blog} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{pager && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={pager.totalPages}
|
||||||
|
totalItems={pager.totalItems}
|
||||||
|
itemsPerPage={10}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Last Post - Full width on mobile, sidebar on desktop */}
|
||||||
|
<div className='lg:max-w-[340px] w-full'>
|
||||||
|
<LastPost blogs={blogs?.results?.blogs?.slice(0, 2) || []} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BlogsPageWithLayout() {
|
export default function BlogsPageWithLayout() {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<BlogsPage />
|
<BlogsPage />
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import axios from "@/config/axios";
|
||||||
|
import { BlogResponse, BlogCategory } from "../types/Types";
|
||||||
|
|
||||||
|
export const getBlogs = async (
|
||||||
|
page: number,
|
||||||
|
limit: number
|
||||||
|
): Promise<BlogResponse> => {
|
||||||
|
const { data } = await axios.get(`/blogs?page=${page}&limit=${limit}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBlogCategories = async (): Promise<BlogCategory[]> => {
|
||||||
|
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<BlogResponse> => {
|
||||||
|
const { data } = await axios.get(`/blogs/${id}`);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBlogsByCategory = async (
|
||||||
|
slug: string,
|
||||||
|
page: number = 1,
|
||||||
|
limit: number = 10
|
||||||
|
): Promise<BlogResponse> => {
|
||||||
|
const { data } = await axios.get(
|
||||||
|
`/blogs/category/${slug}?page=${page}&limit=${limit}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,37 +1,67 @@
|
|||||||
import { ArrowLeft } from 'iconsax-react'
|
import { ArrowLeft } from 'iconsax-react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
import { FC } from 'react'
|
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 = () => {
|
interface BlogCardProps {
|
||||||
return (
|
blog?: Blog;
|
||||||
<div className='bg-white p-3 rounded-2xl shadow w-full overflow-hidden'>
|
isLoading?: boolean;
|
||||||
<Image
|
}
|
||||||
src='https://picsum.photos/200/300?grayscale'
|
|
||||||
width={357}
|
|
||||||
height={201}
|
|
||||||
alt=''
|
|
||||||
className='w-full h-[120px] sm:h-[181px] object-cover rounded-xl sm:rounded-2xl'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='mt-3'>
|
const BlogCard: FC<BlogCardProps> = ({ blog, isLoading = false }) => {
|
||||||
<h6 className='text-primary text-sm'>تکنولوژی</h6>
|
if (isLoading) {
|
||||||
</div>
|
return (
|
||||||
<h4 className='mt-2 text-sm sm:text-base'>
|
<div className='bg-white p-3 rounded-2xl shadow w-full overflow-hidden animate-pulse h-full flex flex-col'>
|
||||||
نسل جدید پردازنده اینتل
|
<div className='w-full h-[120px] sm:h-[181px] bg-gray-200 rounded-xl sm:rounded-2xl'></div>
|
||||||
</h4>
|
<div className='mt-3 h-4 w-16 bg-gray-200 rounded'></div>
|
||||||
|
<div className='mt-2 h-5 w-full bg-gray-200 rounded'></div>
|
||||||
<p className='mt-2 text-xs sm:text-sm text-[#7F7F7F] font-light leading-5 sm:leading-6'>
|
<div className='mt-2 h-4 w-3/4 bg-gray-200 rounded flex-1'></div>
|
||||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است....
|
<div className='mt-4 sm:mt-6 flex justify-between items-center'>
|
||||||
</p>
|
<div className='h-3 w-16 bg-gray-200 rounded'></div>
|
||||||
|
<div className='h-3 w-20 bg-gray-200 rounded'></div>
|
||||||
<div className='mt-4 sm:mt-6 flex justify-between items-center'>
|
|
||||||
<div className='text-xs sm:text-sm font-light text-[#7F7F7F]'>1401/7/21</div>
|
|
||||||
<div className='flex gap-1 items-center text-[#7F7F7F]'>
|
|
||||||
<div className='text-xs sm:text-sm font-light'>ادامه مطلب</div>
|
|
||||||
<ArrowLeft size={14} color='#7F7F7F' className='sm:w-4 sm:h-4' />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blog) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={`/blogs/${blog._id}`}>
|
||||||
|
<div className='bg-white p-3 rounded-2xl shadow w-full overflow-hidden hover:shadow-lg transition-shadow cursor-pointer h-full flex flex-col'>
|
||||||
|
<Image
|
||||||
|
src={blog.imageUrl || 'https://picsum.photos/200/300?grayscale'}
|
||||||
|
width={357}
|
||||||
|
height={201}
|
||||||
|
alt={blog.title_fa}
|
||||||
|
className='w-full h-[120px] sm:h-[181px] object-cover rounded-xl sm:rounded-2xl'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='mt-3'>
|
||||||
|
<h6 className='text-primary text-sm'>{blog.category?.title_fa || 'عمومی'}</h6>
|
||||||
|
</div>
|
||||||
|
<h4 className='mt-2 text-sm sm:text-base line-clamp-2'>
|
||||||
|
{blog.title_fa}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p className='mt-2 text-xs sm:text-sm text-[#7F7F7F] font-light leading-5 sm:leading-6 line-clamp-2 flex-1'>
|
||||||
|
{blog.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className='mt-4 sm:mt-6 flex justify-between items-center'>
|
||||||
|
<div className='text-xs sm:text-sm font-light text-[#7F7F7F]'>
|
||||||
|
{moment(blog.createdAt, 'jYYYY/jMM/jDD HH:mm:ss').format('jYYYY-jMM-jDD')}
|
||||||
|
</div>
|
||||||
|
<div className='flex gap-1 items-center text-primary hover:text-primary transition-colors'>
|
||||||
|
<div className='text-xs sm:text-sm font-light'>ادامه مطلب</div>
|
||||||
|
<ArrowLeft size={14} color={PRIMARY_COLOR} className='sm:w-4 sm:h-4' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,42 @@
|
|||||||
|
'use client'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
import BlogCard from './BlogCard'
|
import BlogCard from './BlogCard'
|
||||||
import GridWrapper from '@/components/GridWrapper'
|
import GridWrapper from '@/components/GridWrapper'
|
||||||
|
import { useGetBlogs } from '@/app/blogs/hooks/useBlogData';
|
||||||
|
|
||||||
const Blogs: FC = () => {
|
const Blogs: FC = () => {
|
||||||
|
|
||||||
|
const { data, error } = useGetBlogs(1, 5)
|
||||||
|
|
||||||
|
if (error || !data?.results?.blogs) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4 className='text-[#012B4D] text-lg'>رسانه و مطالب</h4>
|
||||||
|
<div className='mt-2 flex gap-1 text-sm text-[#8C8C8C]'>
|
||||||
|
برای مشاهده ی آخرین مطالب رسانه و مطالب <Link href="/blogs" className='text-primary hover:underline'>اینجا کلیک کنید</Link>
|
||||||
|
</div>
|
||||||
|
<div className='mt-10'>
|
||||||
|
<div className='text-center text-gray-500'>خطا در بارگذاری مطالب</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const blogs = data.results.blogs
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h4 className='text-[#012B4D] text-lg'>رسانه و مطالب </h4>
|
<h4 className='text-[#012B4D] text-lg'>رسانه و مطالب</h4>
|
||||||
<div className='mt-2 flex gap-1 text-sm text-[#8C8C8C]'>
|
<div className='mt-2 flex gap-1 text-sm text-[#8C8C8C]'>
|
||||||
برای مشاهده ی آخرین مطالب رسانه و مطالب <span className='text-primary'>اینجا کلیک کنید</span>
|
برای مشاهده ی آخرین مطالب رسانه و مطالب <Link href="/blogs" className='text-primary hover:underline'>اینجا کلیک کنید</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-10'>
|
<div className='mt-10'>
|
||||||
<GridWrapper desktop={5} mobile={1} gapDesktop={24} gapMobile={16} >
|
<GridWrapper className='items-stretch' desktop={5} mobile={1} gapDesktop={24} gapMobile={16}>
|
||||||
<BlogCard />
|
{blogs.map((blog) => (
|
||||||
<BlogCard />
|
<BlogCard key={blog._id} blog={blog} />
|
||||||
<BlogCard />
|
))}
|
||||||
<BlogCard />
|
</GridWrapper>
|
||||||
<BlogCard />
|
|
||||||
</GridWrapper >
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import {
|
import {
|
||||||
Carousel,
|
Carousel,
|
||||||
@@ -7,37 +8,43 @@ import {
|
|||||||
CarouselPrevious,
|
CarouselPrevious,
|
||||||
} from "@/components/ui/carousel"
|
} from "@/components/ui/carousel"
|
||||||
import ProductCard from '@/components/ProductCard'
|
import ProductCard from '@/components/ProductCard'
|
||||||
|
import { useGetPopularProducts } from '../hooks/useHomeData'
|
||||||
|
|
||||||
|
|
||||||
const PopularProducts: FC = () => {
|
const PopularProducts: FC = () => {
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h4 className='text-[#012B4D] text-lg'>محبوب ترین محصولات</h4>
|
|
||||||
<div className='mt-2 flex gap-1 text-sm text-[#8C8C8C]'>
|
|
||||||
برای مشاهده ی محبوب ترین محصولات <span className='text-primary'>اینجا کلیک کنید</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-10'>
|
const { data } = useGetPopularProducts();
|
||||||
<Carousel
|
|
||||||
opts={{
|
if (!!data?.results?.popularProducts?.length)
|
||||||
align: "start",
|
return (
|
||||||
direction: "rtl",
|
<div>
|
||||||
}}
|
<h4 className='text-[#012B4D] text-lg'>محبوب ترین محصولات</h4>
|
||||||
className="w-full"
|
<div className='mt-2 flex gap-1 text-sm text-[#8C8C8C]'>
|
||||||
>
|
برای مشاهده ی محبوب ترین محصولات <span className='text-primary'>اینجا کلیک کنید</span>
|
||||||
<CarouselContent className='gap-1 sm:gap-2'>
|
</div>
|
||||||
{Array.from({ length: 5 }).map((_, index) => (
|
|
||||||
<CarouselItem key={index} className="basis-[240px] sm:basis-[292px]">
|
<div className='mt-10'>
|
||||||
<ProductCard />
|
<Carousel
|
||||||
</CarouselItem>
|
opts={{
|
||||||
))}
|
align: "start",
|
||||||
</CarouselContent>
|
direction: "rtl",
|
||||||
<CarouselPrevious className="left-auto -right-12 [&_svg]:-scale-x-100" />
|
}}
|
||||||
<CarouselNext className="right-auto -left-12 [&_svg]:-scale-x-100" />
|
className="w-full"
|
||||||
</Carousel>
|
>
|
||||||
|
<CarouselContent className='gap-1 sm:gap-2'>
|
||||||
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<CarouselItem key={index} className="basis-[240px] sm:basis-[292px]">
|
||||||
|
<ProductCard />
|
||||||
|
</CarouselItem>
|
||||||
|
))}
|
||||||
|
</CarouselContent>
|
||||||
|
<CarouselPrevious className="left-auto -right-12 [&_svg]:-scale-x-100" />
|
||||||
|
<CarouselNext className="right-auto -left-12 [&_svg]:-scale-x-100" />
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PopularProducts
|
export default PopularProducts
|
||||||
@@ -12,3 +12,10 @@ export const getLandingQueryOptions = {
|
|||||||
queryKey: ["landing"],
|
queryKey: ["landing"],
|
||||||
queryFn: api.getLanding,
|
queryFn: api.getLanding,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetPopularProducts = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["popular-products"],
|
||||||
|
queryFn: api.getPopularProducts,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,3 +5,8 @@ export const getLanding = async (): Promise<ILandingResponse> => {
|
|||||||
const { data } = await axios.get<ILandingResponse>("/landing");
|
const { data } = await axios.get<ILandingResponse>("/landing");
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPopularProducts = async () => {
|
||||||
|
const { data } = await axios.get("/product/popular/all");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
export const TOKEN_NAME = "sh_token";
|
export const TOKEN_NAME = "sh_token";
|
||||||
export const REFRESH_TOKEN_NAME = "sh_refresh_token";
|
export const REFRESH_TOKEN_NAME = "sh_refresh_token";
|
||||||
// export const BASE_URL = "https://shop-api.dev.danakcorp.com";
|
export const BASE_URL = "https://shop-api.dev.danakcorp.com";
|
||||||
export const BASE_URL = "http://192.168.1.111:4000";
|
// export const BASE_URL = "http://192.168.1.111:4000";
|
||||||
|
|
||||||
export const PRIMARY_COLOR = "#015699";
|
export const PRIMARY_COLOR = "#015699";
|
||||||
|
|||||||
Reference in New Issue
Block a user