mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 20:57:51 +00:00
fix: update PLP display
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
f5a2237d43
commit
78a79a44b7
@ -1,4 +1,4 @@
|
||||
import { getCollection, getCollectionProducts, getMenu } from 'lib/shopify';
|
||||
import { getCollection, getCollectionProducts } from 'lib/shopify';
|
||||
import { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
@ -9,6 +9,8 @@ import ProductGridItems from 'components/layout/product-grid-items';
|
||||
import FiltersList from 'components/layout/search/filters/filters-list';
|
||||
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';
|
||||
import ProductsGridPlaceholder from 'components/layout/search/placeholder';
|
||||
import SortingMenu from 'components/layout/search/sorting-menu';
|
||||
import {
|
||||
AVAILABILITY_FILTER_ID,
|
||||
@ -90,7 +92,7 @@ const constructFilterInput = (filters: {
|
||||
return results;
|
||||
};
|
||||
|
||||
export default async function CategoryPage({
|
||||
async function CategoryPage({
|
||||
params,
|
||||
searchParams
|
||||
}: {
|
||||
@ -101,42 +103,23 @@ export default async function CategoryPage({
|
||||
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
|
||||
|
||||
const filtersInput = constructFilterInput(rest);
|
||||
const productsData = getCollectionProducts({
|
||||
const { products, filters } = await getCollectionProducts({
|
||||
collection: params.collection,
|
||||
sortKey,
|
||||
reverse,
|
||||
...(filtersInput.length ? { filters: filtersInput } : {})
|
||||
});
|
||||
const collectionData = getCollection(params.collection);
|
||||
const menuData = getMenu('main-menu');
|
||||
|
||||
const [{ products, filters }, collection, menu] = await Promise.all([
|
||||
productsData,
|
||||
collectionData,
|
||||
menuData
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<Suspense fallback={<BreadcrumbHome />}>
|
||||
<Breadcrumb type="collection" handle={params.collection} />
|
||||
</Suspense>
|
||||
</div>
|
||||
{collection ? (
|
||||
<div className="mb-3 mt-3 max-w-5xl lg:mb-1">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-gray-900">{collection.title}</h1>
|
||||
<p className="mt-2 text-base text-gray-500">{collection.description}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex w-full items-center justify-between gap-2 lg:justify-end">
|
||||
<MobileFilters collection={params.collection} filters={filters} menu={menu} />
|
||||
<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 menu={menu} collection={params.collection} />
|
||||
<SubMenu collection={params.collection} />
|
||||
<h3 className="sr-only">Filters</h3>
|
||||
<FiltersList filters={filters} />
|
||||
</aside>
|
||||
@ -154,3 +137,24 @@ export default async function CategoryPage({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CategorySearchPage(props: {
|
||||
params: { collection: string };
|
||||
searchParams?: { [key: string]: string | string[] | undefined };
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<Suspense fallback={<BreadcrumbHome />}>
|
||||
<Breadcrumb type="collection" handle={props.params.collection} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense fallback={<HeaderPlaceholder />}>
|
||||
<Header collection={props.params.collection} />
|
||||
</Suspense>
|
||||
<Suspense fallback={<ProductsGridPlaceholder />}>
|
||||
<CategoryPage {...props} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import Footer from 'components/layout/footer';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export default function SearchLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="mx-auto max-w-screen-2xl px-8 pb-4">{children}</div>
|
||||
<>
|
||||
<div className="mx-auto min-h-[500px] max-w-screen-2xl px-8 pb-4 lg:min-h-[800px]">
|
||||
{children}
|
||||
</div>
|
||||
<Footer />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -17,12 +17,11 @@ type BreadcrumbProps = {
|
||||
|
||||
const findParentCollection = (menu: Menu[], collection: string): Menu | null => {
|
||||
let parentCollection: Menu | null = null;
|
||||
|
||||
for (const item of menu) {
|
||||
if (item.items.length) {
|
||||
const hasParent = item.items.some((subItem) => subItem.path.includes(collection));
|
||||
if (hasParent) {
|
||||
parentCollection = item;
|
||||
return item;
|
||||
} else {
|
||||
parentCollection = findParentCollection(item.items, collection);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ function ThreeItemGridItem({
|
||||
<div
|
||||
className={size === 'full' ? 'md:col-span-4 md:row-span-2' : 'md:col-span-2 md:row-span-1'}
|
||||
>
|
||||
<Link className="relative block aspect-square h-full w-full" href={`/product/${item.handle}`}>
|
||||
<Link className="aspect-square relative block h-full w-full" href={`/product/${item.handle}`}>
|
||||
<GridTileImage
|
||||
src={item.featuredImage.url}
|
||||
fill
|
||||
@ -26,7 +26,6 @@ function ThreeItemGridItem({
|
||||
priority={priority}
|
||||
alt={item.title}
|
||||
label={{
|
||||
position: size === 'full' ? 'center' : 'bottom',
|
||||
title: item.title as string,
|
||||
amount: item.priceRange.maxVariantPrice.amount,
|
||||
currencyCode: item.priceRange.maxVariantPrice.currencyCode
|
||||
|
@ -1,49 +1,45 @@
|
||||
import { PhotoIcon } from '@heroicons/react/24/outline';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import Label from '../label';
|
||||
|
||||
export function GridTileImage({
|
||||
isInteractive = true,
|
||||
active,
|
||||
label,
|
||||
...props
|
||||
}: {
|
||||
isInteractive?: boolean;
|
||||
active?: boolean;
|
||||
label?: {
|
||||
title: string;
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
position?: 'bottom' | 'center';
|
||||
};
|
||||
} & React.ComponentProps<typeof Image>) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'group flex h-full w-full items-center justify-center overflow-hidden rounded-lg border bg-white hover:border-secondary dark:bg-black',
|
||||
{
|
||||
relative: label,
|
||||
'border-2 border-secondary': active,
|
||||
'border-neutral-200 dark:border-neutral-800': !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('relative h-full w-full object-contain', {
|
||||
'transition duration-300 ease-in-out group-hover:scale-105': isInteractive
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
) : null}
|
||||
<div className="group">
|
||||
<div
|
||||
className={clsx(
|
||||
'aspect-h-1 aspect-w-1 relative overflow-hidden rounded-lg bg-gray-200 group-hover:opacity-75',
|
||||
{
|
||||
'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>
|
||||
{label ? (
|
||||
<Label
|
||||
title={label.title}
|
||||
amount={label.amount}
|
||||
currencyCode={label.currencyCode}
|
||||
position={label.position}
|
||||
/>
|
||||
<Label title={label.title} amount={label.amount} currencyCode={label.currencyCode} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,32 +1,23 @@
|
||||
import clsx from 'clsx';
|
||||
import Price from './price';
|
||||
|
||||
const Label = ({
|
||||
title,
|
||||
amount,
|
||||
currencyCode,
|
||||
position = 'bottom'
|
||||
currencyCode
|
||||
}: {
|
||||
title: string;
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
position?: 'bottom' | 'center';
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx('absolute bottom-0 left-0 flex w-full px-4 pb-4 @container/label', {
|
||||
'lg:px-20 lg:pb-[35%]': position === 'center'
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center rounded-full border bg-white/70 p-1 text-xs font-semibold text-black backdrop-blur-md dark:border-neutral-800 dark:bg-black/70 dark:text-white">
|
||||
<h3 className="mr-4 line-clamp-2 flex-grow pl-2 leading-none tracking-tight">{title}</h3>
|
||||
<Price
|
||||
className="flex-none rounded-full bg-blue-600 p-2 text-white"
|
||||
amount={amount}
|
||||
currencyCode={currencyCode}
|
||||
currencyCodeClassName="hidden @[275px]/label:inline"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ export default function ProductGridItems({ products }: { products: Product[] })
|
||||
return (
|
||||
<>
|
||||
{products.map((product) => (
|
||||
<Grid.Item key={product.handle} className="animate-fadeIn">
|
||||
<Grid.Item key={product.handle} className="animate-fadeIn rounded-lg">
|
||||
<Link className="relative inline-block h-full w-full" href={`/product/${product.handle}`}>
|
||||
<GridTileImage
|
||||
alt={product.title}
|
||||
|
@ -3,20 +3,11 @@
|
||||
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react';
|
||||
import { FunnelIcon } from '@heroicons/react/24/outline';
|
||||
import { XMarkIcon } from '@heroicons/react/24/solid';
|
||||
import { Filter, Menu } from 'lib/shopify/types';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Filter } from 'lib/shopify/types';
|
||||
import { Fragment, ReactNode, useState } from 'react';
|
||||
import Filters from './filters-list';
|
||||
import SubMenu from './sub-menu';
|
||||
|
||||
const MobileFilters = ({
|
||||
collection,
|
||||
filters,
|
||||
menu
|
||||
}: {
|
||||
collection: string;
|
||||
filters: Filter[];
|
||||
menu: Menu[];
|
||||
}) => {
|
||||
const MobileFilters = ({ filters, menu }: { filters: Filter[]; menu: ReactNode }) => {
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
|
||||
return (
|
||||
@ -64,7 +55,7 @@ const MobileFilters = ({
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-gray-200 px-4 pt-4">
|
||||
<SubMenu collection={collection} menu={menu} />
|
||||
{menu}
|
||||
<Filters filters={filters} defaultOpen={false} />
|
||||
</div>
|
||||
</DialogPanel>
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Menu } from 'lib/shopify/types';
|
||||
import { getMenu } from 'lib/shopify';
|
||||
import Link from 'next/link';
|
||||
|
||||
const SubMenu = ({ menu, collection }: { menu: Menu[]; collection: string }) => {
|
||||
const SubMenu = async ({ collection }: { collection: string }) => {
|
||||
const menu = await getMenu('main-menu');
|
||||
|
||||
const subMenu = menu.find((item) => item.path === `/search/${collection}`)?.items || [];
|
||||
|
||||
return subMenu.length ? (
|
||||
|
21
components/layout/search/header.tsx
Normal file
21
components/layout/search/header.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { getCollection } from 'lib/shopify';
|
||||
|
||||
const Header = async ({ collection }: { collection: string }) => {
|
||||
const collectionData = await getCollection(collection);
|
||||
|
||||
return collectionData ? (
|
||||
<div className="mb-3 mt-3 max-w-5xl lg:mb-1">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-gray-900">{collectionData.title}</h1>
|
||||
<p className="mt-2 text-base text-gray-500">{collectionData.description}</p>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const HeaderPlaceholder = () => {
|
||||
return (
|
||||
<div className="mb-3 mt-3 max-w-5xl lg:mb-1">
|
||||
<div className="h-10 w-1/2 rounded bg-gray-200" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Header;
|
25
components/layout/search/placeholder.tsx
Normal file
25
components/layout/search/placeholder.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
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">
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<div key={index} className="h-96 w-full rounded-lg bg-gray-200" />
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
</Grid>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductsGridPlaceholder;
|
@ -28,7 +28,7 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden">
|
||||
<div className="relative aspect-1 h-full max-h-[550px] w-full overflow-hidden">
|
||||
{images[imageIndex] && (
|
||||
<Image
|
||||
className="h-full w-full object-contain"
|
||||
|
@ -35,6 +35,7 @@
|
||||
"tailwind-merge": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.11",
|
||||
|
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@ -40,6 +40,9 @@ dependencies:
|
||||
version: 2.2.2
|
||||
|
||||
devDependencies:
|
||||
'@tailwindcss/aspect-ratio':
|
||||
specifier: ^0.4.2
|
||||
version: 0.4.2(tailwindcss@3.4.1)
|
||||
'@tailwindcss/container-queries':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(tailwindcss@3.4.1)
|
||||
@ -688,6 +691,14 @@ packages:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/@tailwindcss/aspect-ratio@0.4.2(tailwindcss@3.4.1):
|
||||
resolution: {integrity: sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==}
|
||||
peerDependencies:
|
||||
tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1'
|
||||
dependencies:
|
||||
tailwindcss: 3.4.1
|
||||
dev: true
|
||||
|
||||
/@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.1):
|
||||
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
|
||||
peerDependencies:
|
||||
|
@ -57,6 +57,7 @@ module.exports = {
|
||||
}
|
||||
);
|
||||
}),
|
||||
require('@tailwindcss/forms')
|
||||
require('@tailwindcss/forms'),
|
||||
require('@tailwindcss/aspect-ratio')
|
||||
]
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user