diff --git a/apps/web/src/app/brand/[id]/catalog/BrandCatalogContent.tsx b/apps/web/src/app/brand/[id]/catalog/BrandCatalogContent.tsx new file mode 100644 index 0000000..f244c22 --- /dev/null +++ b/apps/web/src/app/brand/[id]/catalog/BrandCatalogContent.tsx @@ -0,0 +1,41 @@ +import { notFound } from 'next/navigation'; +import { filterProducts, parseSearchParams } from '@/lib/filterProducts'; +import { CatalogView } from '@/components/CatalogView/CatalogView'; +import { getBrands, getProducts } from '@/lib/api'; + +export async function BrandCatalogContent({ + id, + params, +}: { + id: string; + params: Record; +}) { + const [brands, products] = await Promise.all([ + getBrands(), + getProducts(), + ]); + const brand = brands.find((b) => b.id === Number(id)); + if (!brand) notFound(); + + const filters = parseSearchParams(params); + const brandProducts = products.filter((p) => p.brand === brand.name); + const filtered = filterProducts(brandProducts, filters); + + const basePath = `/brand/${id}/catalog`; + + const breadcrumbItems = [ + { label: 'Главная', href: '/' }, + { label: 'Каталог', href: '/catalog' }, + { label: brand.name, href: `/brand/${id}` }, + { label: 'Каталог' }, + ]; + + return ( + + ); +} diff --git a/apps/web/src/app/brand/[id]/catalog/page.tsx b/apps/web/src/app/brand/[id]/catalog/page.tsx index 8e4e2a5..3a31b3a 100644 --- a/apps/web/src/app/brand/[id]/catalog/page.tsx +++ b/apps/web/src/app/brand/[id]/catalog/page.tsx @@ -1,7 +1,7 @@ -import { notFound } from 'next/navigation'; -import { filterProducts, parseSearchParams } from '@/lib/filterProducts'; -import { CatalogView } from '@/components/CatalogView/CatalogView'; -import { getBrands, getProducts, getCategories } from '@/lib/api'; +import { Suspense } from 'react'; +import { getBrands } from '@/lib/api'; +import { CatalogSkeleton } from '@/components/Skeleton/Skeleton'; +import { BrandCatalogContent } from './BrandCatalogContent'; export async function generateMetadata({ params, @@ -26,37 +26,12 @@ export default async function BrandCatalogPage({ searchParams: Promise>; }) { const { id } = await params; - const [brands, products, categories] = await Promise.all([ - getBrands(), - getProducts(), - getCategories(), - ]); - const brand = brands.find((b) => b.id === Number(id)); - if (!brand) notFound(); - const sp = await searchParams; - const filters = parseSearchParams(sp); - const brandProducts = products.filter((p) => p.brand === brand.name); - const filtered = filterProducts(brandProducts, filters); - - const basePath = `/brand/${id}/catalog`; - - const breadcrumbItems = [ - { label: 'Главная', href: '/' }, - { label: 'Каталог', href: '/catalog' }, - { label: brand.name, href: `/brand/${id}` }, - { label: 'Каталог' }, - ]; + const key = JSON.stringify(sp); return ( - + }> + + ); } diff --git a/apps/web/src/app/brand/[id]/loading.tsx b/apps/web/src/app/brand/[id]/loading.tsx new file mode 100644 index 0000000..c83c4a1 --- /dev/null +++ b/apps/web/src/app/brand/[id]/loading.tsx @@ -0,0 +1,5 @@ +import { BrandPageSkeleton } from '@/components/Skeleton/Skeleton'; + +export default function Loading() { + return ; +} diff --git a/apps/web/src/app/catalog/CatalogContent.tsx b/apps/web/src/app/catalog/CatalogContent.tsx new file mode 100644 index 0000000..89b4a25 --- /dev/null +++ b/apps/web/src/app/catalog/CatalogContent.tsx @@ -0,0 +1,25 @@ +import { filterProducts, parseSearchParams } from '@/lib/filterProducts'; +import { CatalogView } from '@/components/CatalogView/CatalogView'; +import { getProducts } from '@/lib/api'; + +export async function CatalogContent({ + params, +}: { + params: Record; +}) { + const products = await getProducts(); + const filters = parseSearchParams(params); + const filtered = filterProducts(products, filters); + + const breadcrumbItems = [ + { label: 'Главная', href: '/' }, + { label: 'Каталог' }, + ]; + + return ( + + ); +} diff --git a/apps/web/src/app/catalog/page.tsx b/apps/web/src/app/catalog/page.tsx index 0559786..4926b35 100644 --- a/apps/web/src/app/catalog/page.tsx +++ b/apps/web/src/app/catalog/page.tsx @@ -1,7 +1,7 @@ +import { Suspense } from 'react'; import { categoryNameMap, CategorySlug } from '@/types'; -import { filterProducts, parseSearchParams } from '@/lib/filterProducts'; -import { CatalogView } from '@/components/CatalogView/CatalogView'; -import { getProducts, getBrands, getCategories } from '@/lib/api'; +import { CatalogSkeleton } from '@/components/Skeleton/Skeleton'; +import { CatalogContent } from './CatalogContent'; export async function generateMetadata({ searchParams, @@ -27,26 +27,11 @@ export default async function CatalogPage({ searchParams: Promise>; }) { const params = await searchParams; - const [products, brands, categories] = await Promise.all([ - getProducts(), - getBrands(), - getCategories(), - ]); - const filters = parseSearchParams(params); - const filtered = filterProducts(products, filters); - - const breadcrumbItems = [ - { label: 'Главная', href: '/' }, - { label: 'Каталог' }, - ]; + const key = JSON.stringify(params); return ( - + }> + + ); } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index c6cd258..ddd592c 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,15 +1,24 @@ +import { Suspense } from 'react'; import { Hero } from '@/components/Hero/Hero'; import { Directions } from '@/components/Directions/Directions'; import { TrustStrip } from '@/components/TrustStrip/TrustStrip'; import { BrandLogos } from '@/components/BrandLogos/BrandLogos'; +import { + DirectionsSkeleton, + BrandLogosSkeleton, +} from '@/components/Skeleton/Skeleton'; export default function HomePage() { return ( <> - + }> + + - + }> + + ); } diff --git a/apps/web/src/app/product/[sku]/loading.tsx b/apps/web/src/app/product/[sku]/loading.tsx new file mode 100644 index 0000000..83d9594 --- /dev/null +++ b/apps/web/src/app/product/[sku]/loading.tsx @@ -0,0 +1,5 @@ +import { ProductPageSkeleton } from '@/components/Skeleton/Skeleton'; + +export default function Loading() { + return ; +} diff --git a/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx b/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx index 221cfc4..99498d5 100644 --- a/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx +++ b/apps/web/src/components/CatalogSidebar/CatalogSidebar.tsx @@ -1,27 +1,41 @@ 'use client'; +import { useEffect, useState } from 'react'; import { Brand, Category, Product } from '@/types'; import { Icon } from '@/components/Icon/Icon'; import { useCatalogFilters } from '@/hooks/useCatalogFilters'; import { getSubcategories } from '@/lib/getSubcategories'; import styles from './CatalogSidebar.module.scss'; +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api'; + interface CatalogSidebarProps { basePath?: string; hiddenFilters?: string[]; - brands: Brand[]; - categories: Category[]; - products: Product[]; } export function CatalogSidebar({ basePath = '/catalog', hiddenFilters = [], - brands, - categories, - products, }: CatalogSidebarProps) { const { filters, setFilter } = useCatalogFilters(basePath); + const [brands, setBrands] = useState([]); + const [categories, setCategories] = useState([]); + const [products, setProducts] = useState([]); + + useEffect(() => { + Promise.all([ + fetch(`${API_URL}/brands`).then((r) => r.json()), + fetch(`${API_URL}/categories`).then((r) => r.json()), + fetch(`${API_URL}/products`).then((r) => r.json()), + ]) + .then(([b, c, p]) => { + setBrands(b); + setCategories(c); + setProducts(p); + }) + .catch(() => {}); + }, []); const toggleFilter = ( key: 'category' | 'brand' | 'subcategory' | 'sort', diff --git a/apps/web/src/components/CatalogView/CatalogView.tsx b/apps/web/src/components/CatalogView/CatalogView.tsx index 49fc00a..18df014 100644 --- a/apps/web/src/components/CatalogView/CatalogView.tsx +++ b/apps/web/src/components/CatalogView/CatalogView.tsx @@ -1,5 +1,4 @@ -import { Suspense } from 'react'; -import { Product, Brand, Category } from '@/types'; +import { Product } from '@/types'; import { Breadcrumb } from '@/components/Breadcrumb/Breadcrumb'; import { CatalogSidebar } from '@/components/CatalogSidebar/CatalogSidebar'; import { ProductCard } from '@/components/ProductCard/ProductCard'; @@ -15,9 +14,6 @@ interface CatalogViewProps { breadcrumbItems: BreadcrumbItem[]; basePath?: string; hiddenFilters?: string[]; - brands: Brand[]; - categories: Category[]; - allProducts?: Product[]; } export function CatalogView({ @@ -25,21 +21,10 @@ export function CatalogView({ breadcrumbItems, basePath = '/catalog', hiddenFilters, - brands, - categories, - allProducts, }: CatalogViewProps) { return (
- - - +
diff --git a/apps/web/src/components/Header/Header.tsx b/apps/web/src/components/Header/Header.tsx index 6bbe8ce..5dc4c78 100644 --- a/apps/web/src/components/Header/Header.tsx +++ b/apps/web/src/components/Header/Header.tsx @@ -24,7 +24,7 @@ export function Header() { fetch(`${API_URL}/products`) .then((res) => res.json()) .then(setProducts) - .catch(() => {}); + .catch((error) => console.error(error)); }, []); const suggestions = diff --git a/apps/web/src/hooks/useCatalogFilters.ts b/apps/web/src/hooks/useCatalogFilters.ts index bb4975f..a68c44a 100644 --- a/apps/web/src/hooks/useCatalogFilters.ts +++ b/apps/web/src/hooks/useCatalogFilters.ts @@ -31,7 +31,7 @@ export function useCatalogFilters(basePath = '/catalog') { const qs = params.toString(); return qs ? `${basePath}?${qs}` : basePath; }, - // eslint-disable-next-line react-hooks/exhaustive-deps + [basePath, searchParams] );