feat: modify PLP layout

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-05-06 17:02:10 +07:00
parent e0cd6ac2bd
commit 913e7a1809
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
12 changed files with 333 additions and 54 deletions

View File

@ -2,12 +2,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
@supports (font: -apple-system-body) and (-webkit-appearance: none) { @supports (font: -apple-system-body) and (-webkit-appearance: none) {
img[loading='lazy'] { img[loading='lazy'] {
clip-path: inset(0.6px); clip-path: inset(0.6px);

View File

@ -2,9 +2,14 @@ import { getCollection, getCollectionProducts } from 'lib/shopify';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Breadcrumb from 'components/breadcrumb';
import BreadcrumbHome from 'components/breadcrumb/breadcrumb-home';
import Grid from 'components/grid'; import Grid from 'components/grid';
import ProductGridItems from 'components/layout/product-grid-items'; import ProductGridItems from 'components/layout/product-grid-items';
import Filters from 'components/layout/search/filters';
import SortingMenu from 'components/layout/search/sorting-menu';
import { defaultSort, sorting } from 'lib/constants'; import { defaultSort, sorting } from 'lib/constants';
import { Suspense } from 'react';
export const runtime = 'edge'; export const runtime = 'edge';
@ -33,17 +38,43 @@ export default async function CategoryPage({
}) { }) {
const { sort } = searchParams as { [key: string]: string }; const { sort } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort; const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse }); const productsData = getCollectionProducts({ collection: params.collection, sortKey, reverse });
const collectionData = getCollection(params.collection);
const [products, collection] = await Promise.all([productsData, collectionData]);
return ( return (
<section> <>
{products.length === 0 ? ( <div className="mb-2">
<p className="py-3 text-lg">{`No products found in this collection`}</p> <Suspense fallback={<BreadcrumbHome />}>
) : ( <Breadcrumb type="collection" handle={params.collection} />
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> </Suspense>
<ProductGridItems products={products} /> </div>
</Grid> {collection ? (
)} <div className="mb-1 mt-3 max-w-5xl">
</section> <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 justify-end">
<SortingMenu />
</div>
<section className="mt-3 border-t pt-2">
{products.length === 0 ? (
<p className="py-3 text-lg">{`No products found in this collection`}</p>
) : (
<Grid className="pt-5 lg:grid-cols-3 lg:gap-x-8 xl:grid-cols-4">
<aside className="hidden lg:block">
<Filters collection={params.collection} />
</aside>
<div className="lg:col-span-2 xl:col-span-3">
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<ProductGridItems products={products} />
</Grid>
</div>
</Grid>
)}
</section>
</>
); );
} }

View File

@ -1,21 +1,10 @@
import Footer from 'components/layout/footer'; import Footer from 'components/layout/footer';
import Collections from 'components/layout/search/collections';
import FilterList from 'components/layout/search/filter';
import { sorting } from 'lib/constants';
import { Suspense } from 'react'; import { Suspense } from 'react';
export default function SearchLayout({ children }: { children: React.ReactNode }) { export default function SearchLayout({ children }: { children: React.ReactNode }) {
return ( return (
<Suspense> <Suspense>
<div className="mx-auto flex max-w-screen-2xl flex-col gap-8 px-4 pb-4 text-black md:flex-row dark:text-white"> <div className="mx-auto max-w-screen-2xl px-8 pb-4">{children}</div>
<div className="order-first w-full flex-none md:max-w-[125px]">
<Collections />
</div>
<div className="order-last min-h-screen w-full md:order-none">{children}</div>
<div className="order-none flex-none md:order-last md:w-[125px]">
<FilterList list={sorting} title="Sort by" />
</div>
</div>
<Footer /> <Footer />
</Suspense> </Suspense>
); );

View File

@ -0,0 +1,15 @@
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from './breadcrumb-list';
const BreadcrumbHome = () => {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Home</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
};
export default BreadcrumbHome;

View File

@ -1,4 +1,5 @@
import { getProduct } from 'lib/shopify'; import { getCollection, getMenu, getProduct } from 'lib/shopify';
import { Menu } from 'lib/shopify/types';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { import {
Breadcrumb, Breadcrumb,
@ -14,6 +15,23 @@ type BreadcrumbProps = {
handle: string; handle: string;
}; };
const findParentCollection = (menu: Menu[], collection: string): Menu | null => {
let parentCollection: Menu | null = null;
for (const item of menu) {
if (item.items.length) {
console.log({ collection, item });
const hasParent = item.items.some((subItem) => subItem.path.includes(collection));
if (hasParent) {
parentCollection = item;
} else {
parentCollection = findParentCollection(item.items, collection);
}
}
}
return parentCollection;
};
const BreadcrumbComponent = async ({ type, handle }: BreadcrumbProps) => { const BreadcrumbComponent = async ({ type, handle }: BreadcrumbProps) => {
const items: Array<{ href: string; title: string }> = [{ href: '/', title: 'Home' }]; const items: Array<{ href: string; title: string }> = [{ href: '/', title: 'Home' }];
@ -35,6 +53,25 @@ const BreadcrumbComponent = async ({ type, handle }: BreadcrumbProps) => {
}); });
} }
if (type === 'collection') {
const collectionData = getCollection(handle);
const menuData = getMenu('main-menu');
const [collection, menu] = await Promise.all([collectionData, menuData]);
if (!collection) return null;
const parentCollection = findParentCollection(menu, handle);
if (parentCollection && parentCollection.path !== `/search/${handle}`) {
items.push({
href: `${parentCollection.path}`,
title: parentCollection.title
});
}
items.push({
title: collection.title,
href: `/search/${collection.handle}`
});
}
return ( return (
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList>

View File

@ -4,9 +4,9 @@ import LogoSquare from 'components/logo-square';
import Profile from 'components/profile'; import Profile from 'components/profile';
import OpenProfile from 'components/profile/open-profile'; import OpenProfile from 'components/profile/open-profile';
import { getMenu } from 'lib/shopify'; import { getMenu } from 'lib/shopify';
import { Menu } from 'lib/shopify/types';
import Link from 'next/link'; import Link from 'next/link';
import { Suspense } from 'react'; import { Suspense } from 'react';
import MainMenu from './main-menu';
import MobileMenu from './mobile-menu'; import MobileMenu from './mobile-menu';
import Search, { SearchSkeleton } from './search'; import Search, { SearchSkeleton } from './search';
const { SITE_NAME } = process.env; const { SITE_NAME } = process.env;
@ -15,7 +15,7 @@ export default async function Navbar() {
const menu = await getMenu('main-menu'); const menu = await getMenu('main-menu');
return ( return (
<nav className="relative mb-4 flex items-center justify-between bg-white pb-3 pt-4 md:pb-0 dark:bg-neutral-900"> <nav className="relative mb-4 flex items-center justify-between bg-white pb-3 pt-4 dark:bg-neutral-900 md:pb-0">
<div className="block flex-none pl-4 md:hidden"> <div className="block flex-none pl-4 md:hidden">
<Suspense fallback={null}> <Suspense fallback={null}>
<MobileMenu menu={menu} /> <MobileMenu menu={menu} />
@ -29,7 +29,7 @@ export default async function Navbar() {
className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6" className="mr-2 flex w-full items-center justify-center md:w-auto lg:mr-6"
> >
<LogoSquare /> <LogoSquare />
<div className="flex-none font-league-spartan text-xl font-semibold tracking-tight text-dark md:hidden md:text-2xl lg:block lg:text-3xl lg:leading-tight dark:text-white"> <div className="flex-none font-league-spartan text-xl font-semibold tracking-tight text-dark dark:text-white md:hidden md:text-2xl lg:block lg:text-3xl lg:leading-tight">
{SITE_NAME} {SITE_NAME}
</div> </div>
</Link> </Link>
@ -49,22 +49,7 @@ export default async function Navbar() {
</div> </div>
</div> </div>
{menu.length ? ( <MainMenu menu={menu} />
<div className="hidden w-full items-center justify-center border-b px-4 pb-3 pt-4 md:flex">
<ul className="hidden gap-8 text-sm font-medium md:flex md:items-center lg:gap-16">
{menu.map((item: Menu) => (
<li key={item.title}>
<Link
href={item.path}
className="text-neutral-600 hover:text-black dark:text-neutral-400 dark:hover:text-neutral-300"
>
{item.title}
</Link>
</li>
))}
</ul>
</div>
) : null}
</div> </div>
</nav> </nav>
); );

View File

@ -0,0 +1,95 @@
'use client';
import { Popover, Transition } from '@headlessui/react';
import clsx from 'clsx';
import { Menu } from 'lib/shopify/types';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Fragment, useState } from 'react';
const MainMenu = ({ menu }: { menu: Menu[] }) => {
const pathname = usePathname();
const [open, setOpen] = useState('');
return menu.length ? (
<div className="mt-2 hidden h-11 w-full border-b text-sm font-medium md:flex">
<Popover.Group as={Fragment}>
<div className="z-10 flex h-full w-full items-center justify-center gap-8 px-4 lg:gap-16">
{menu.map((item: Menu) => {
const isActiveItem =
item.path === pathname ||
item.items.some((subItem: Menu) => subItem.path === pathname);
if (!item.items.length) {
return (
<Link
key={item.title}
href={item.path}
className={`flex h-full items-center ${isActiveItem ? 'text-black' : 'text-neutral-600 hover:text-black'}`}
>
{item.title}
</Link>
);
}
const isOpen = open === item.path;
return (
<Popover key={item.title} className="relative flex h-full">
<div
className="relative flex"
onMouseOver={() => setOpen(item.path)}
onMouseLeave={() => setOpen('')}
>
<Link
href={item.path}
className={clsx(
'relative z-10 flex items-center border-b-2 px-2 pt-px transition-colors duration-200 ease-out focus-visible:ring-0 focus-visible:ring-offset-0',
{
'border-gray-500 text-black': isOpen || isActiveItem,
'border-transparent text-neutral-600 hover:text-black':
!isOpen && !isActiveItem
}
)}
>
{item.title}
</Link>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition ease-in duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={isOpen}
>
<Popover.Panel
static
className="absolute inset-x-0 left-1/2 top-full z-10 mt-0.5 min-w-32 max-w-sm -translate-x-1/2 transform text-sm"
>
<div className="overflow-hidden rounded-md shadow-lg ring-1 ring-black/5">
<ul className="flex flex-col space-y-2 bg-white px-4 py-3">
{item.items.map((subItem: Menu) => (
<li key={subItem.title}>
<Link
href={subItem.path}
className={`border-b ${subItem.path === pathname ? 'border-black text-black' : 'border-transparent text-neutral-600 hover:text-black'}`}
>
{subItem.title}
</Link>
</li>
))}
</ul>
</div>
</Popover.Panel>
</Transition>
</div>
</Popover>
);
})}
</div>
</Popover.Group>
</div>
) : null;
};
export default MainMenu;

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Disclosure, Transition } from '@headlessui/react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation'; import { usePathname, useSearchParams } from 'next/navigation';
import { Fragment, Suspense, useEffect, useState } from 'react'; import { Fragment, Suspense, useEffect, useState } from 'react';
@ -35,7 +35,7 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
<button <button
onClick={openMobileMenu} onClick={openMobileMenu}
aria-label="Open mobile menu" aria-label="Open mobile menu"
className="flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors md:hidden dark:border-neutral-700 dark:text-white" className="flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white md:hidden"
> >
<Bars3Icon className="h-4" /> <Bars3Icon className="h-4" />
</button> </button>
@ -80,12 +80,29 @@ export default function MobileMenu({ menu }: { menu: Menu[] }) {
<ul className="flex w-full flex-col"> <ul className="flex w-full flex-col">
{menu.map((item: Menu) => ( {menu.map((item: Menu) => (
<li <li
className="py-2 text-xl text-black transition-colors hover:text-neutral-500 dark:text-white" className="py-2 text-xl text-neutral-600 transition-colors hover:text-black"
key={item.title} key={item.title}
> >
<Link href={item.path} onClick={closeMobileMenu}> {item.items.length ? (
{item.title} <Disclosure>
</Link> <Disclosure.Button>{item.title}</Disclosure.Button>
<Disclosure.Panel className="flex flex-col space-y-2 px-3 py-2 text-lg text-neutral-600 hover:text-black">
{item.items.map((subItem: Menu) => (
<Link
key={subItem.title}
href={subItem.path}
onClick={closeMobileMenu}
>
{subItem.title}
</Link>
))}
</Disclosure.Panel>
</Disclosure>
) : (
<Link href={item.path} onClick={closeMobileMenu}>
{item.title}
</Link>
)}
</li> </li>
))} ))}
</ul> </ul>

View File

@ -0,0 +1,30 @@
import { getMenu } from 'lib/shopify';
import Link from 'next/link';
const Filters = async ({ collection }: { collection: string }) => {
const menu = await getMenu('main-menu');
const subMenu = menu.find((item) => item.path === `/search/${collection}`)?.items || [];
return (
<div>
{subMenu.length ? (
<>
<h3 className="sr-only">Categories</h3>
<ul
role="list"
className="space-y-4 border-b border-gray-200 pb-6 text-sm font-medium text-gray-900"
>
{subMenu.map((subMenuItem) => (
<li key={subMenuItem.title}>
<Link href={subMenuItem.path} className="hover:underline">
{subMenuItem.title}
</Link>
</li>
))}
</ul>
</>
) : null}
</div>
);
};
export default Filters;

View File

@ -0,0 +1,49 @@
'use client';
import { Menu, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/20/solid';
import { sorting } from 'lib/constants';
import { Fragment } from 'react';
import SortingItem from './item';
const SortingMenu = () => {
return (
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="group inline-flex justify-center text-sm font-medium text-gray-700 hover:text-gray-900">
Sort
<ChevronDownIcon
className="-mr-1 ml-1 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2 w-40 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{sorting.map((option) => (
<Menu.Item key={option.title}>
{({ active }) => (
<div>
<SortingItem item={option} hover={active} />
</div>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
);
};
export default SortingMenu;

View File

@ -0,0 +1,36 @@
import clsx from 'clsx';
import { SortFilterItem } from 'lib/constants';
import { createUrl } from 'lib/utils';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
const SortingItem = ({ item, hover }: { item: SortFilterItem; hover: boolean }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const active = searchParams.get('sort') === item.slug;
const q = searchParams.get('q');
const href = createUrl(
pathname,
new URLSearchParams({
...(q && { q }),
...(item.slug && item.slug.length && { sort: item.slug })
})
);
const DynamicTag = active ? 'p' : Link;
return (
<DynamicTag
prefetch={!active ? false : undefined}
href={href}
className={clsx('block px-4 py-2 text-sm', {
'font-medium text-gray-900': active,
'text-gray-500': !active,
'bg-gray-100': hover,
'bg-transparent': !hover
})}
>
{item.title}
</DynamicTag>
);
};
export default SortingItem;

View File

@ -2,6 +2,7 @@ const plugin = require('tailwindcss/plugin');
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: 'class',
content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
extend: { extend: {