all -product
This commit is contained in:
64
src/app/[locale]/all-product/page.tsx
Normal file
64
src/app/[locale]/all-product/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import AllProducts from '@/features/category/ui/AllProduct';
|
||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
params: { locale: 'uz' | 'ru' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
|
const { locale } = params;
|
||||||
|
|
||||||
|
const titles = {
|
||||||
|
uz: 'Barcha mahsulotlar | GASTRO',
|
||||||
|
ru: 'Все товары | GASTRO',
|
||||||
|
};
|
||||||
|
|
||||||
|
const descriptions = {
|
||||||
|
uz: 'GASTRO onlayn do‘konidagi barcha mahsulotlar ro‘yxati. Sifatli mahsulotlar, qulay narxlar va tez yetkazib berish.',
|
||||||
|
ru: 'Полный каталог товаров интернет-магазина GASTRO. Качественные товары, выгодные цены и быстрая доставка.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const keywords = {
|
||||||
|
uz: 'barcha mahsulotlar, onlayn do‘kon, gastro, xarid, yetkazib berish',
|
||||||
|
ru: 'все товары, онлайн-магазин, gastro, покупка, доставка',
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: titles[locale],
|
||||||
|
description: descriptions[locale],
|
||||||
|
keywords: keywords[locale],
|
||||||
|
openGraph: {
|
||||||
|
title: titles[locale],
|
||||||
|
description: descriptions[locale],
|
||||||
|
siteName: 'GASTRO',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/logos/logo.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 1200,
|
||||||
|
alt: titles[locale],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: locale === 'uz' ? 'uz_UZ' : 'ru_RU',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: titles[locale],
|
||||||
|
description: descriptions[locale],
|
||||||
|
images: ['/logos/logo.png'],
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AllProductPage() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<AllProducts />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/features/category/ui/AllProduct.tsx
Normal file
113
src/features/category/ui/AllProduct.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { product_api } from '@/shared/config/api/product/api';
|
||||||
|
import { usePathname, useRouter } from '@/shared/config/i18n/navigation';
|
||||||
|
import { Card } from '@/shared/ui/card';
|
||||||
|
import { GlobalPagination } from '@/shared/ui/global-pagination';
|
||||||
|
import { Skeleton } from '@/shared/ui/skeleton';
|
||||||
|
import { ProductCard } from '@/widgets/categories/ui/product-card';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const PAGE_SIZE = 36;
|
||||||
|
|
||||||
|
const AllProducts = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlPage = Number(searchParams.get('page')) || 1;
|
||||||
|
setPage(urlPage);
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: product,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ['all_product_list', page],
|
||||||
|
queryFn: () => {
|
||||||
|
return product_api.list({
|
||||||
|
page,
|
||||||
|
page_size: PAGE_SIZE,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
select(data) {
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
params.set('page', newPage.toString());
|
||||||
|
|
||||||
|
router.push(`${pathname}?${params.toString()}`, {
|
||||||
|
scroll: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="custom-container p-4 mb-5 flex flex-col min-h-[calc(85vh)]">
|
||||||
|
<div className="flex-1">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5" />
|
||||||
|
<span>{t('Orqaga')}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p className="text-gray-600 text-sm mt-1">
|
||||||
|
{product?.total} {t('ta mahsulot')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Products grid */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 gap-3">
|
||||||
|
{isLoading &&
|
||||||
|
Array.from({ length: 6 }).map((_, index) => (
|
||||||
|
<Card className="p-3 space-y-3 rounded-xl" key={index}>
|
||||||
|
<Skeleton className="h-40 sm:h-48 md:h-56 w-full rounded-lg" />
|
||||||
|
<Skeleton className="h-4 w-3/4" />
|
||||||
|
<Skeleton className="h-4 w-1/2" />
|
||||||
|
<Skeleton className="h-10 w-full rounded-lg" />
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
{product &&
|
||||||
|
!isLoading &&
|
||||||
|
product.results
|
||||||
|
.filter((product) => product.state === 'A')
|
||||||
|
.map((item) => (
|
||||||
|
<ProductCard key={item.id} product={item} error={isError} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination at the bottom */}
|
||||||
|
{product && (
|
||||||
|
<div className="w-full mt-5 flex justify-end">
|
||||||
|
<GlobalPagination
|
||||||
|
page={page}
|
||||||
|
total={product.total ?? 0}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AllProducts;
|
||||||
@@ -202,5 +202,7 @@
|
|||||||
"So'rov yuborildi!": "Запрос успешно отправлен!",
|
"So'rov yuborildi!": "Запрос успешно отправлен!",
|
||||||
|
|
||||||
"Tez-tez So'raladigan Savollar": "Часто задаваемые вопросы",
|
"Tez-tez So'raladigan Savollar": "Часто задаваемые вопросы",
|
||||||
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar": "Ответы на самые часто задаваемые вопросы о Gastro Market"
|
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar": "Ответы на самые часто задаваемые вопросы о Gastro Market",
|
||||||
|
|
||||||
|
"Barcha mahsulotlar": "Все товары"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,5 +202,7 @@ declare const messages: {
|
|||||||
|
|
||||||
"Tez-tez So'raladigan Savollar": "Tez-tez So'raladigan Savollar";
|
"Tez-tez So'raladigan Savollar": "Tez-tez So'raladigan Savollar";
|
||||||
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar": "Gastro Market haqida eng ko'p so'raladigan savollarga javoblar";
|
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar": "Gastro Market haqida eng ko'p so'raladigan savollarga javoblar";
|
||||||
|
|
||||||
|
'Barcha mahsulotlar': 'Barcha mahsulotlar';
|
||||||
};
|
};
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -198,5 +198,7 @@
|
|||||||
"So'rov yuborildi!": "So'rov yuborildi!",
|
"So'rov yuborildi!": "So'rov yuborildi!",
|
||||||
|
|
||||||
"Tez-tez So'raladigan Savollar": "Tez-tez So'raladigan Savollar",
|
"Tez-tez So'raladigan Savollar": "Tez-tez So'raladigan Savollar",
|
||||||
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar": "Gastro Market haqida eng ko'p so'raladigan savollarga javoblar"
|
"Gastro Market haqida eng ko'p so'raladigan savollarga javoblar": "Gastro Market haqida eng ko'p so'raladigan savollarga javoblar",
|
||||||
|
|
||||||
|
"Barcha mahsulotlar": "Barcha mahsulotlar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import CategoryImage from '@/assets/water-bottle.png';
|
|||||||
import { category_api } from '@/shared/config/api/category/api';
|
import { category_api } from '@/shared/config/api/category/api';
|
||||||
import { product_api } from '@/shared/config/api/product/api';
|
import { product_api } from '@/shared/config/api/product/api';
|
||||||
import { BASE_URL } from '@/shared/config/api/URLs';
|
import { BASE_URL } from '@/shared/config/api/URLs';
|
||||||
import { Link } from '@/shared/config/i18n/navigation';
|
import { Link, useRouter } from '@/shared/config/i18n/navigation';
|
||||||
import { cn } from '@/shared/lib/utils';
|
import { cn } from '@/shared/lib/utils';
|
||||||
import { AspectRatio } from '@/shared/ui/aspect-ratio';
|
import { AspectRatio } from '@/shared/ui/aspect-ratio';
|
||||||
import { Button } from '@/shared/ui/button';
|
import { Button } from '@/shared/ui/button';
|
||||||
@@ -31,6 +31,7 @@ const Welcome = () => {
|
|||||||
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
||||||
const [canScrollNext, setCanScrollNext] = useState(false);
|
const [canScrollNext, setCanScrollNext] = useState(false);
|
||||||
const [apiCat, setApiCat] = useState<CarouselApi>();
|
const [apiCat, setApiCat] = useState<CarouselApi>();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ['banner_list'],
|
queryKey: ['banner_list'],
|
||||||
@@ -218,12 +219,25 @@ const Welcome = () => {
|
|||||||
</Carousel>
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
<section className="relative custom-container mt-5 justify-center items-center border-b border-slate-200">
|
<section className="relative custom-container mt-5 justify-center items-center border-b border-slate-200">
|
||||||
|
<div className="flex items-center justify-between pb-3">
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 group cursor-pointer"
|
||||||
|
onClick={() => router.push(`/all-product/`)}
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl font-bold text-slate-800 group-hover:text-blue-600 transition-colors">
|
||||||
|
Barcha mahsulotlar
|
||||||
|
</h2>
|
||||||
|
<div className="p-1.5 bg-slate-100 rounded-full group-hover:bg-blue-100 transition-all">
|
||||||
|
<ChevronRight className="text-slate-600 group-hover:text-blue-600 group-hover:translate-x-0.5 transition-all" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex items-center justify-between pb-3">
|
<div className="flex items-center justify-between pb-3">
|
||||||
<div className="flex items-center gap-2 group cursor-pointer">
|
<div className="flex items-center gap-2 group cursor-pointer">
|
||||||
<div className="p-1.5 bg-slate-100 rounded-full group-hover:bg-blue-100 transition-all"></div>
|
<div className="p-1.5 bg-slate-100 rounded-full group-hover:bg-blue-100 transition-all"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Carousel className="w-full mt-5" setApi={setApiPro}>
|
<Carousel className="w-full" setApi={setApiPro}>
|
||||||
<CarouselContent className="pr-[12%] sm:pr-0">
|
<CarouselContent className="pr-[12%] sm:pr-0">
|
||||||
{productLoading &&
|
{productLoading &&
|
||||||
Array.from({ length: 6 }).map((__, index) => (
|
Array.from({ length: 6 }).map((__, index) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user