fix: update layout for PLP

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-06-12 17:10:58 +07:00
parent 39b1235c82
commit ca37984cff
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
15 changed files with 226 additions and 218 deletions

View File

@ -10,7 +10,6 @@ import { ProductDescription } from 'components/product/product-description';
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
import { getProduct, getProductRecommendations } from 'lib/shopify';
import { Image } from 'lib/shopify/types';
import Link from 'next/link';
export const runtime = 'edge';
@ -131,7 +130,6 @@ async function RelatedProducts({ id }: { id: string }) {
key={product.handle}
className="aspect-square w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5"
>
<Link className="relative h-full w-full" href={`/product/${product.handle}`}>
<GridTileImage
alt={product.title}
label={{
@ -142,8 +140,8 @@ async function RelatedProducts({ id }: { id: string }) {
src={product.featuredImage?.url}
fill
sizes="(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw"
href={`/product/${product.handle}`}
/>
</Link>
</li>
))}
</ul>

View File

@ -8,7 +8,9 @@ import YMMFilters, { YMMFiltersPlaceholder } from 'components/filters';
import Grid from 'components/grid';
import ProductsList from 'components/layout/products-list';
import { getProductsInCollection } from 'components/layout/products-list/actions';
import FiltersList from 'components/layout/search/filters/filters-list';
import FiltersContainer, {
FiltersListPlaceholder
} from 'components/layout/search/filters/filters-container';
import MobileFilters from 'components/layout/search/filters/mobile-filters';
import SubMenu from 'components/layout/search/filters/sub-menu';
import Header, { HeaderPlaceholder } from 'components/layout/search/header';
@ -47,19 +49,16 @@ async function CategoryPage({
return (
<>
<div className="flex w-full items-center justify-between gap-2 lg:justify-end">
<div className="my-3 block lg:hidden">
<Suspense fallback={<YMMFiltersPlaceholder />}>
<YMMFilters />
</Suspense>
</div>
<div className="mb-5 flex w-full items-center justify-between gap-2 lg:justify-end">
<MobileFilters filters={filters} menu={<SubMenu collection={params.collection} />} />
<SortingMenu />
</div>
<section>
<Grid className="pt-5 lg:grid-cols-3 lg:gap-x-8 xl:grid-cols-4">
<aside className="hidden lg:block">
<SubMenu collection={params.collection} />
<h3 className="sr-only">Filters</h3>
<FiltersList filters={filters} />
</aside>
<div className="lg:col-span-2 xl:col-span-3">
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<Grid className="grid-cols-1 sm:grid-cols-2 sm:gap-x-8 lg:grid-cols-3">
{products.length === 0 ? (
<p className="py-3 text-lg">{`No products found in this collection`}</p>
) : (
@ -72,19 +71,30 @@ async function CategoryPage({
/>
)}
</Grid>
</div>
</Grid>
</section>
</>
);
}
export default function CategorySearchPage(props: {
export default async function CategorySearchPage(props: {
params: { collection: string };
searchParams?: { [key: string]: string | string[] | undefined };
}) {
return (
<>
<div className="grid lg:grid-cols-3 lg:gap-x-10 xl:grid-cols-4">
<aside className="hidden lg:block">
<div className="mb-5">
<Suspense fallback={<YMMFiltersPlaceholder />}>
<YMMFilters />
</Suspense>
</div>
<SubMenu collection={props.params.collection} />
<h3 className="sr-only">Filters</h3>
<Suspense fallback={<FiltersListPlaceholder />} key={`filters-${props.params.collection}`}>
<FiltersContainer searchParams={props.searchParams} />
</Suspense>
</aside>
<div className="lg:col-span-2 xl:col-span-3">
<div className="mb-2">
<Suspense fallback={<BreadcrumbHome />} key={`breadcrumb-${props.params.collection}`}>
<Breadcrumb type="collection" handle={props.params.collection} />
@ -93,14 +103,14 @@ export default function CategorySearchPage(props: {
<Suspense fallback={<HeaderPlaceholder />} key={`header-${props.params.collection}`}>
<Header collection={props.params.collection} />
</Suspense>
<div className="my-3">
<Suspense fallback={<YMMFiltersPlaceholder />}>
<YMMFilters />
</Suspense>
</div>
<Suspense fallback={<ProductsGridPlaceholder />} key={`products-${props.params.collection}`}>
<Suspense
fallback={<ProductsGridPlaceholder />}
key={`products-${props.params.collection}`}
>
<CategoryPage {...props} />
</Suspense>
</>
</div>
</div>
);
}

View File

@ -1,10 +1,11 @@
import Footer from 'components/layout/footer';
import { Suspense } from 'react';
export default function SearchLayout({ children }: { children: React.ReactNode }) {
return (
<>
<div className="mx-auto mt-4 min-h-[500px] max-w-screen-2xl px-8 pb-4 lg:min-h-[800px]">
{children}
<div className="mx-auto mt-6 min-h-[500px] max-w-screen-2xl px-8 pb-4 lg:min-h-[800px]">
<Suspense>{children}</Suspense>
</div>
<Footer />
</>

View File

@ -1,40 +0,0 @@
import { getCollectionProducts } from 'lib/shopify';
import Link from 'next/link';
import { GridTileImage } from './grid/tile';
export async function Carousel() {
// Collections that start with `hidden-*` are hidden from the search page.
const { products } = await getCollectionProducts({ collection: 'hidden-homepage-carousel' });
if (!products?.length) return null;
// Purposefully duplicating products to make the carousel loop and not run out of products on wide screens.
const carouselProducts = [...products, ...products, ...products];
return (
<div className=" w-full overflow-x-auto pb-6 pt-1">
<ul className="flex animate-carousel gap-4">
{carouselProducts.map((product, i) => (
<li
key={`${product.handle}${i}`}
className="relative aspect-square h-[30vh] max-h-[275px] w-2/3 max-w-[475px] flex-none md:w-1/3"
>
<Link href={`/product/${product.handle}`} className="relative h-full w-full">
<GridTileImage
alt={product.title}
label={{
title: product.title,
amount: product.priceRange.maxVariantPrice.amount,
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url}
fill
sizes="(min-width: 1024px) 25vw, (min-width: 768px) 33vw, 50vw"
/>
</Link>
</li>
))}
</ul>
</div>
);
}

View File

@ -83,7 +83,7 @@ const FiltersList = ({ years, makes, models, menu, autoFocusField }: FiltersList
};
return (
<div className="flex grow flex-col items-center gap-3 md:flex-row">
<>
<FilterField
label="Part Type"
onChange={onChangePartType}
@ -124,11 +124,11 @@ const FiltersList = ({ years, makes, models, menu, autoFocusField }: FiltersList
<Button
onClick={onSearch}
disabled={disabled}
className="w-full rounded bg-secondary px-4 py-1.5 text-sm font-medium text-white data-[disabled]:cursor-not-allowed data-[hover]:bg-secondary/85 data-[disabled]:opacity-50 md:w-auto"
className="w-full rounded bg-secondary px-4 py-1.5 text-sm font-medium text-white data-[disabled]:cursor-not-allowed data-[hover]:bg-secondary/85 data-[disabled]:opacity-50 @md:w-auto"
>
Search
</Button>
</div>
</>
);
};

View File

@ -4,7 +4,7 @@ import FiltersList from './filters-list';
const YMMFiltersContainer = ({ children }: { children: ReactNode }) => {
return (
<div className="rounded border bg-white px-6 pb-5 pt-4">
<div className="rounded border bg-white px-6 pb-5 pt-4 @container">
<p className="mb-3 text-xl font-semibold leading-tight tracking-tight text-neutral-700">
Find Your Car Part
</p>
@ -23,7 +23,9 @@ const YMMFilters = async () => {
return (
<YMMFiltersContainer>
<div className="flex grow flex-col items-center gap-3 @md:flex-row">
<FiltersList years={years} makes={makes} models={models} menu={menu} />
</div>
</YMMFiltersContainer>
);
};

View File

@ -1,7 +1,6 @@
import { GridTileImage } from 'components/grid/tile';
import { getCollectionProducts } from 'lib/shopify';
import type { Product } from 'lib/shopify/types';
import Link from 'next/link';
function ThreeItemGridItem({
item,
@ -16,7 +15,6 @@ function ThreeItemGridItem({
<div
className={size === 'full' ? 'md:col-span-4 md:row-span-2' : 'md:col-span-2 md:row-span-1'}
>
<Link className="aspect-square relative block h-full w-full" href={`/product/${item.handle}`}>
<GridTileImage
src={item.featuredImage.url}
fill
@ -30,8 +28,8 @@ function ThreeItemGridItem({
amount: item.priceRange.maxVariantPrice.amount,
currencyCode: item.priceRange.maxVariantPrice.currencyCode
}}
href={`/product/${item.handle}`}
/>
</Link>
</div>
);
}

View File

@ -1,11 +1,14 @@
import { PhotoIcon } from '@heroicons/react/24/outline';
import { ArrowRightIcon, PhotoIcon } from '@heroicons/react/24/solid';
import clsx from 'clsx';
import Price from 'components/price';
import Image from 'next/image';
import Label from '../label';
import Link from 'next/link';
export function GridTileImage({
active,
label,
href,
place = 'grid',
...props
}: {
active?: boolean;
@ -14,17 +17,18 @@ export function GridTileImage({
amount: string;
currencyCode: string;
};
place?: 'grid' | 'gallery';
href: string;
} & React.ComponentProps<typeof Image>) {
return (
<div className="group">
<div className="flex h-full flex-col rounded-b border bg-white">
<div className="grow">
<div className="px-4">
<div
className={clsx(
'aspect-h-1 aspect-w-1 relative overflow-hidden rounded-lg bg-gray-200 group-hover:opacity-75',
{
className={clsx('aspect-h-1 aspect-w-1 relative overflow-hidden', {
'border-2 border-secondary': active,
'border-neutral-200': !active
}
)}
})}
>
{props.src ? (
// eslint-disable-next-line jsx-a11y/alt-text -- `alt` is inherited from `props`, which is being enforced with TypeScript
@ -38,9 +42,59 @@ export function GridTileImage({
</div>
)}
</div>
{label ? (
<Label title={label.title} amount={label.amount} currencyCode={label.currencyCode} />
) : null}
</div>
<div className="flex flex-col gap-2 divide-y px-4">
{label && (
<h3 className="mt-4 text-sm font-semibold leading-6 text-gray-800">{label.title}</h3>
)}
{label && (
<div className="flex w-full justify-end py-2">
<Price
className="text-lg font-medium text-gray-900"
amount={label.amount}
currencyCode={label.currencyCode}
/>
</div>
)}
</div>
</div>
{place === 'grid' && (
<Link
href={href}
className="flex items-center justify-center gap-3 rounded-b bg-dark py-3 text-white"
>
<span className="text-sm font-medium tracking-wide">More details</span>
<ArrowRightIcon className="size-4" />
</Link>
)}
</div>
);
}
export const TileImage = ({
active,
...props
}: {
active?: boolean;
} & React.ComponentProps<typeof Image>) => {
return (
<div
className={clsx('aspect-h-1 aspect-w-1 relative overflow-hidden rounded border bg-white', {
'border-2 border-secondary': active,
'border-neutral-200': !active
})}
>
{props.src ? (
// eslint-disable-next-line jsx-a11y/alt-text -- `alt` is inherited from `props`, which is being enforced with TypeScript
<Image className={clsx('h-full w-full object-cover object-center')} {...props} />
) : (
<div
className="flex h-full w-full items-center justify-center text-gray-400"
title="Missing product image"
>
<PhotoIcon className="size-7" />
</div>
)}
</div>
);
};

View File

@ -1,25 +0,0 @@
import Price from './price';
const Label = ({
title,
amount,
currencyCode
}: {
title: string;
amount: string;
currencyCode: string;
}) => {
return (
<div className="flex flex-col">
<h3 className="mt-4 text-sm text-gray-700">{title}</h3>
<Price
className="text-lg font-medium text-gray-900"
amount={amount}
currencyCode={currencyCode}
currencyCodeClassName="hidden @[275px]/label:inline"
/>
</div>
);
};
export default Label;

View File

@ -3,7 +3,6 @@
import Grid from 'components/grid';
import { GridTileImage } from 'components/grid/tile';
import { Product } from 'lib/shopify/types';
import Link from 'next/link';
import { useEffect, useRef, useState } from 'react';
import { getProductsInCollection, searchProducts } from './actions';
@ -66,10 +65,9 @@ const ProductsList = ({
{products.map((product, index) => (
<Grid.Item
key={product.handle}
className="animate-fadeIn rounded-lg"
className="animate-fadeIn"
ref={index === products.length - 1 && _pageInfo.hasNextPage ? lastElement : undefined}
>
<Link className="relative inline-block h-full w-full" href={`/product/${product.handle}`}>
<GridTileImage
alt={product.title}
label={{
@ -80,8 +78,8 @@ const ProductsList = ({
src={product.featuredImage?.url}
fill
sizes="(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
href={`/product/${product.handle}`}
/>
</Link>
</Grid.Item>
))}
</>

View File

@ -0,0 +1,26 @@
import { getProductsInCollection } from 'components/layout/products-list/actions';
import FiltersList from './filters-list';
const FiltersContainer = async ({
searchParams
}: {
searchParams?: { [key: string]: string | string[] | undefined };
}) => {
const { filters } = await getProductsInCollection({
searchParams
});
return <FiltersList filters={filters} defaultOpen={false} />;
};
export const FiltersListPlaceholder = () => {
return (
<div className="hidden lg:flex lg:flex-col lg:gap-4">
<div className="h-32 w-full rounded bg-gray-200" />
<div className="h-32 w-full rounded bg-gray-200" />
<div className="h-32 w-full rounded bg-gray-200" />
<div className="h-32 w-full rounded bg-gray-200" />
</div>
);
};
export default FiltersContainer;

View File

@ -42,7 +42,7 @@ const Filters = ({ filters, defaultOpen = true }: { filters: Filter[]; defaultOp
<Disclosure
key={id}
as="div"
className="flex h-auto max-h-[550px] flex-col gap-y-3 overflow-hidden pt-5"
className="flex h-auto max-h-[300px] flex-col gap-y-3 overflow-hidden pt-5"
defaultOpen={defaultOpen}
>
<DisclosureButton className="group flex items-center justify-between">

View File

@ -7,11 +7,11 @@ const SubMenu = async ({ collection }: { collection: string }) => {
const subMenu = menu.find((item) => item.path === `/search/${collection}`)?.items || [];
return subMenu.length ? (
<>
<h3 className="sr-only">Categories</h3>
<div className="border-t pt-4">
<div className="text-sm font-medium text-gray-900">Manufacturers</div>
<ul
role="list"
className="space-y-4 border-b border-gray-200 pb-6 text-sm font-medium text-gray-900"
className="ml-1 mt-2 max-h-[300px] space-y-3 overflow-y-auto border-b border-gray-200 pb-6 text-sm text-gray-600"
>
{subMenu.map((subMenuItem) => (
<li key={subMenuItem.title}>
@ -21,7 +21,7 @@ const SubMenu = async ({ collection }: { collection: string }) => {
</li>
))}
</ul>
</>
</div>
) : null;
};

View File

@ -1,23 +1,9 @@
import Grid from 'components/grid';
const ProductsGridPlaceholder = () => {
return (
<section>
<Grid className="animate-pulse pt-5 lg:grid-cols-3 lg:gap-x-8 xl:grid-cols-4">
<aside className="hidden lg:flex lg:flex-col lg:gap-4">
<div className="h-32 w-full rounded bg-gray-200" />
<div className="h-32 w-full rounded bg-gray-200" />
<div className="h-32 w-full rounded bg-gray-200" />
<div className="h-32 w-full rounded bg-gray-200" />
</aside>
<div className="lg:col-span-2 xl:col-span-3">
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<section className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 9 }).map((_, index) => (
<div key={index} className="h-96 w-full rounded-lg bg-gray-200" />
))}
</Grid>
</div>
</Grid>
</section>
);
};

View File

@ -1,7 +1,7 @@
'use client';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import { GridTileImage } from 'components/grid/tile';
import { TileImage } from 'components/grid/tile';
import { createUrl } from 'lib/utils';
import Image from 'next/image';
import Link from 'next/link';
@ -81,11 +81,11 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
<li key={image.src} className="h-16 w-16 md:h-20 md:w-20">
<Link
aria-label="Enlarge product image"
href={createUrl(pathname, imageSearchParams)}
scroll={false}
className="h-full w-full"
href={createUrl(pathname, imageSearchParams)}
>
<GridTileImage
<TileImage
alt={image.altText}
src={image.src}
width={80}