blogs
This commit is contained in:
@@ -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<BlogItemProps> = ({ blog }) => {
|
||||
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
|
||||
width={200}
|
||||
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'
|
||||
alt='blog'
|
||||
alt={blog.title_fa}
|
||||
/>
|
||||
|
||||
<div className='flex-1'>
|
||||
<h6 className='font-bold text-base sm:text-lg'>
|
||||
سامسونگ اولین لپتاپ OLED یکپارچه را معرفی میکند
|
||||
{blog.title_fa}
|
||||
</h6>
|
||||
|
||||
<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>
|
||||
|
||||
<Link href={`/blogs/${blog._id}`}>
|
||||
<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>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<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 (
|
||||
<div className='max-w-[292px] w-full'>
|
||||
<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 className='flex flex-col gap-3 mt-4'>
|
||||
<div className='h-14 bg-[#F8F8F8] rounded-[10px] px-4 flex items-center justify-between'>
|
||||
<div className='font-medium text-[#8C8C8C]'>موبایل</div>
|
||||
<div className='size-3 rounded-full bg-[#D9D9D9]'></div>
|
||||
{/* All Categories */}
|
||||
<div
|
||||
onClick={() => 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]'
|
||||
}`}
|
||||
>
|
||||
<div className='font-medium'>همه مطالب</div>
|
||||
<div className={`size-3 rounded-full ${selectedCategory === null ? 'bg-white' : 'bg-[#D9D9D9]'
|
||||
}`}></div>
|
||||
</div>
|
||||
<div className='h-14 bg-[#F8F8F8] rounded-[10px] px-4 flex items-center justify-between'>
|
||||
<div className='font-medium text-[#8C8C8C]'>موبایل</div>
|
||||
<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 text-[#8C8C8C]'>موبایل</div>
|
||||
<div className='size-3 rounded-full bg-[#D9D9D9]'></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>
|
||||
)
|
||||
|
||||
@@ -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<LastPostProps> = ({ blogs }) => {
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<div className='font-bold text-base sm:text-lg mb-4'>
|
||||
@@ -9,8 +14,9 @@ const LastPost: FC = () => {
|
||||
</div>
|
||||
|
||||
<div className='space-y-4'>
|
||||
<MiniBlogItem />
|
||||
<MiniBlogItem />
|
||||
{blogs.map((blog) => (
|
||||
<MiniBlogItem key={blog._id} blog={blog} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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<MiniBlogItemProps> = ({ blog }) => {
|
||||
return (
|
||||
<Link href={`/blogs/${blog._id}`} className='block'>
|
||||
<div className='flex gap-3 sm:gap-4 mt-4 sm:mt-6'>
|
||||
<Image
|
||||
width={123}
|
||||
height={123}
|
||||
src={'https://picsum.photos/123/123'}
|
||||
alt='last post'
|
||||
src={blog.imageUrl || 'https://picsum.photos/123/123'}
|
||||
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'>
|
||||
<h6 className='text-sm font-bold leading-5'>
|
||||
رندرهای گلکسی M54 5G
|
||||
{blog.title_fa}
|
||||
</h6>
|
||||
|
||||
<p className='mt-2 sm:mt-3 text-xs text-[#7F7F7F] line-clamp-2 leading-5'>
|
||||
طبق رندرها، گلکسی M54 5G فاقد ماژول دوربین است و سنسورهای دوربین هر....
|
||||
{blog.description}
|
||||
</p>
|
||||
</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,
|
||||
});
|
||||
};
|
||||
+60
-6
@@ -1,28 +1,82 @@
|
||||
'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 = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
||||
|
||||
const { data: blogCategories, isLoading: categoriesLoading, error: categoriesError } = useGetBlogCategories()
|
||||
|
||||
// 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 <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
|
||||
categories={blogCategories || []}
|
||||
selectedCategory={selectedCategory}
|
||||
onCategorySelect={handleCategorySelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className='flex-1 flex flex-col gap-6'>
|
||||
<BlogItem />
|
||||
<BlogItem />
|
||||
<BlogItem />
|
||||
<BlogItem />
|
||||
{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 />
|
||||
<LastPost blogs={blogs?.results?.blogs?.slice(0, 2) || []} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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,38 +1,68 @@
|
||||
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 = () => {
|
||||
interface BlogCardProps {
|
||||
blog?: Blog;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const BlogCard: FC<BlogCardProps> = ({ blog, isLoading = false }) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='bg-white p-3 rounded-2xl shadow w-full overflow-hidden'>
|
||||
<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'>
|
||||
<h6 className='text-primary text-sm'>تکنولوژی</h6>
|
||||
</div>
|
||||
<h4 className='mt-2 text-sm sm:text-base'>
|
||||
نسل جدید پردازنده اینتل
|
||||
</h4>
|
||||
|
||||
<p className='mt-2 text-xs sm:text-sm text-[#7F7F7F] font-light leading-5 sm:leading-6'>
|
||||
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است....
|
||||
</p>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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'>
|
||||
<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 className='h-3 w-16 bg-gray-200 rounded'></div>
|
||||
<div className='h-3 w-20 bg-gray-200 rounded'></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>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogCard
|
||||
@@ -1,21 +1,41 @@
|
||||
'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 (
|
||||
<div>
|
||||
<h4 className='text-[#012B4D] text-lg'>رسانه و مطالب</h4>
|
||||
<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 className='mt-10'>
|
||||
<GridWrapper desktop={5} mobile={1} gapDesktop={24} gapMobile={16} >
|
||||
<BlogCard />
|
||||
<BlogCard />
|
||||
<BlogCard />
|
||||
<BlogCard />
|
||||
<BlogCard />
|
||||
<div className='text-center text-gray-500'>خطا در بارگذاری مطالب</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const blogs = 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'>
|
||||
<GridWrapper className='items-stretch' desktop={5} mobile={1} gapDesktop={24} gapMobile={16}>
|
||||
{blogs.map((blog) => (
|
||||
<BlogCard key={blog._id} blog={blog} />
|
||||
))}
|
||||
</GridWrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use client'
|
||||
import { FC } from 'react'
|
||||
import {
|
||||
Carousel,
|
||||
@@ -7,9 +8,14 @@ import {
|
||||
CarouselPrevious,
|
||||
} from "@/components/ui/carousel"
|
||||
import ProductCard from '@/components/ProductCard'
|
||||
import { useGetPopularProducts } from '../hooks/useHomeData'
|
||||
|
||||
|
||||
const PopularProducts: FC = () => {
|
||||
|
||||
const { data } = useGetPopularProducts();
|
||||
|
||||
if (!!data?.results?.popularProducts?.length)
|
||||
return (
|
||||
<div>
|
||||
<h4 className='text-[#012B4D] text-lg'>محبوب ترین محصولات</h4>
|
||||
@@ -38,6 +44,7 @@ const PopularProducts: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
export default PopularProducts
|
||||
@@ -12,3 +12,10 @@ export const getLandingQueryOptions = {
|
||||
queryKey: ["landing"],
|
||||
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");
|
||||
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 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";
|
||||
|
||||
Reference in New Issue
Block a user