mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 04:37:51 +00:00
feat: implement browse by manufacturers section
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
882d1db67c
commit
9e9573c7be
@ -1,6 +1,7 @@
|
||||
import FAQ from 'components/faq';
|
||||
import Hero from 'components/hero';
|
||||
import About from 'components/home-page/about';
|
||||
import Manufacturers from 'components/home-page/manufacturers';
|
||||
import WhyChoose from 'components/home-page/why-choose';
|
||||
import Footer from 'components/layout/footer';
|
||||
import { Metadata } from 'next';
|
||||
@ -26,13 +27,18 @@ export default async function HomePage() {
|
||||
<Hero />
|
||||
</Suspense>
|
||||
<div className="flex min-h-96 flex-col">
|
||||
<About />
|
||||
<Suspense>
|
||||
<About />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<WhyChoose />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<FAQ />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<Manufacturers />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense>
|
||||
<Footer />
|
||||
|
@ -1,31 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react';
|
||||
import { Metaobject } from 'lib/shopify/types';
|
||||
import RichTextDisplay from './page/rich-text-display';
|
||||
|
||||
type DisplayTabsProps = {
|
||||
items: string[];
|
||||
items: Metaobject[];
|
||||
};
|
||||
const DisplayTabs = ({ items }: DisplayTabsProps) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="flex w-fit items-center rounded bg-gray-100 p-1">
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
key={item}
|
||||
className={clsx(
|
||||
'w-fit cursor-pointer rounded px-6 py-1 text-center text-sm font-medium',
|
||||
{
|
||||
'bg-white text-primary': index === selectedIndex,
|
||||
'bg-transparent text-gray-600': index !== selectedIndex
|
||||
}
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<TabGroup>
|
||||
<TabList className="flex w-fit items-center rounded bg-gray-100 p-1">
|
||||
{items.map((item) => (
|
||||
<Tab
|
||||
key={item.title}
|
||||
className="w-fit cursor-pointer rounded bg-transparent px-6 py-1 text-center text-sm font-medium text-gray-600 focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 data-[selected]:bg-white data-[selected]:text-primary"
|
||||
>
|
||||
{item.title}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanels className="mt-3">
|
||||
{items.map((item) => (
|
||||
<TabPanel key={item.title}>
|
||||
<RichTextDisplay contentBlocks={JSON.parse(item.content || '{}').children} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -16,7 +16,7 @@ const FAQ = async () => {
|
||||
if (!faqs) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 px-6 py-16">
|
||||
<div className="bg-gray-100 px-6 py-20">
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-1 lg:grid-cols-2 lg:gap-20">
|
||||
<div className="col-span-1 flex flex-col gap-3">
|
||||
<Tag text="FAQ" />
|
||||
@ -32,6 +32,7 @@ const FAQ = async () => {
|
||||
alt="FAQs background"
|
||||
fill
|
||||
className="w-full object-cover object-center"
|
||||
sizes="(min-width 1200px) 80vw"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute left-0 top-0 flex min-h-[300px] min-w-[400px] translate-y-1/4 flex-col gap-3 bg-dark px-12 py-14">
|
||||
|
@ -80,7 +80,7 @@ const FilterField = <T extends { [key: string]: unknown }>({
|
||||
</div>
|
||||
<ComboboxOptions
|
||||
anchor="bottom"
|
||||
className="w-[var(--input-width)] rounded-xl border border-gray-200 bg-white p-1 [--anchor-gap:6px] empty:hidden"
|
||||
className="z-10 w-[var(--input-width)] rounded-xl border border-gray-200 bg-white p-1 [--anchor-gap:6px] empty:hidden"
|
||||
>
|
||||
{filteredOptions.map((option) => (
|
||||
<ComboboxOption
|
||||
|
@ -30,12 +30,13 @@ const FiltersList = ({ years, makes, models, menu, autoFocusField }: FiltersList
|
||||
const [partType, setPartType] = useState<{ label: string; value: string } | null>(
|
||||
PART_TYPES.find((type) => type.value === partTypeCollection) || null
|
||||
);
|
||||
|
||||
const [make, setMake] = useState<Metaobject | null>(
|
||||
(partType &&
|
||||
makes.find((make) =>
|
||||
searchParams.get(MAKE_FILTER_ID)
|
||||
? make.id === searchParams.get(MAKE_FILTER_ID)
|
||||
: make.slug === params.collection
|
||||
: params.collection?.includes(make.name!.toLowerCase())
|
||||
)) ||
|
||||
null
|
||||
);
|
||||
@ -100,6 +101,7 @@ const FiltersList = ({ years, makes, models, menu, autoFocusField }: FiltersList
|
||||
getId={(option) => option.id}
|
||||
disabled={!partType}
|
||||
autoFocus={autoFocusField === 'make'}
|
||||
displayKey="display_name"
|
||||
/>
|
||||
<FilterField
|
||||
label="Model"
|
||||
|
@ -4,7 +4,7 @@ import FiltersList from './filters-list';
|
||||
const HomePageFilters = async () => {
|
||||
const yearsData = getMetaobjects('make_model_year_composite');
|
||||
const modelsData = getMetaobjects('make_model_composite');
|
||||
const makesData = getMetaobjects('make_composite');
|
||||
const makesData = getMetaobjects('make');
|
||||
|
||||
const [years, models, makes] = await Promise.all([yearsData, modelsData, makesData]);
|
||||
const menu = await getMenu('main-menu');
|
||||
|
@ -16,7 +16,7 @@ const YMMFiltersContainer = ({ children }: { children: ReactNode }) => {
|
||||
const YMMFilters = async () => {
|
||||
const yearsData = getMetaobjects('make_model_year_composite');
|
||||
const modelsData = getMetaobjects('make_model_composite');
|
||||
const makesData = getMetaobjects('make_composite');
|
||||
const makesData = getMetaobjects('make');
|
||||
|
||||
const [years, models, makes] = await Promise.all([yearsData, modelsData, makesData]);
|
||||
const menu = await getMenu('main-menu');
|
||||
|
@ -1,37 +1,48 @@
|
||||
import DisplayTabs from 'components/display-tabs';
|
||||
import RichTextDisplay from 'components/page/rich-text-display';
|
||||
import { getMetaobject, getMetaobjectsByIds } from 'lib/shopify';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import Image from 'next/image';
|
||||
import Tag from '../tag';
|
||||
import ButtonLink from './button-link';
|
||||
|
||||
const About = () => {
|
||||
const { SITE_NAME } = process.env;
|
||||
const About = async () => {
|
||||
const aboutUs = await getMetaobject({
|
||||
handle: { type: 'about_us', handle: `${kebabCase(SITE_NAME)}-about` }
|
||||
});
|
||||
|
||||
if (!aboutUs) return null;
|
||||
const features = aboutUs.features
|
||||
? await getMetaobjectsByIds(JSON.parse(aboutUs.features) as string[])
|
||||
: [];
|
||||
|
||||
const introduction = aboutUs.introduction ? JSON.parse(aboutUs.introduction) : null;
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-1 items-start gap-x-0 gap-y-10 px-6 py-16 lg:grid-cols-2 lg:gap-x-10">
|
||||
<Image
|
||||
src="/about.png"
|
||||
alt="About Us"
|
||||
sizes="(min-width: 1920px) 588px, (min-width: 770px) 50vw, 100vw"
|
||||
width={588}
|
||||
height={468}
|
||||
className="col-span-1 h-full w-full object-contain"
|
||||
/>
|
||||
<div className="relative">
|
||||
<Image
|
||||
src="/about.png"
|
||||
alt="About Us"
|
||||
sizes="(min-width: 1920px) 588px, (min-width: 770px) 50vw, 100vw"
|
||||
width={588}
|
||||
height={468}
|
||||
className="col-span-1 h-full w-full rounded-sm object-contain"
|
||||
/>
|
||||
<div className="absolute bottom-0 right-0 flex h-auto w-[150px] flex-col items-center gap-2 rounded-sm bg-gray-100 py-5">
|
||||
<span className="text-3xl font-medium text-primary">20+</span>
|
||||
<span className="text-center text-dark">Years Of Experiance</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-col justify-between pb-2">
|
||||
<div className="mb-3 flex flex-col gap-3">
|
||||
<Tag text="About Us" />
|
||||
<h3 className="text-3xl font-semibold lg:text-4xl">Engine & Transmission Experts</h3>
|
||||
<p className="tracking-wide text-blue-200">
|
||||
{`Car Part Planet is your ultimate destination for all your drivetrain replacement needs.
|
||||
Whether you're searching for a used engine, a remanufactured engine, a used
|
||||
transmission, a remanufactured transmission, or seeking expert drivetrain fitment
|
||||
guidance for your DIY replacement project, we've got you covered.`}
|
||||
</p>
|
||||
<DisplayTabs items={['Range', 'Quality', 'Knowledge']} />
|
||||
<p className="tracking-wide text-blue-200">
|
||||
Our dedicated team is committed to sourcing top-quality parts at affordable prices for a
|
||||
wide range of gasoline and diesel vehicles, including those from American, Japanese, and
|
||||
various other manufacturers. Our extensive inventory includes popular engines and
|
||||
transmissions for GM, Chevrolet, Dodge, Ford, Chrysler, Jeep, Nissan, Toyota, Honda, and
|
||||
many other manufacturers.
|
||||
</p>
|
||||
{introduction ? <RichTextDisplay contentBlocks={introduction.children} /> : null}
|
||||
|
||||
<DisplayTabs items={features} />
|
||||
</div>
|
||||
<ButtonLink link={{ text: 'Learn More', url: '/about' }} />
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@ const ButtonLink = ({ link }: { link: { text: string; url: string } }) => {
|
||||
return (
|
||||
<Link href={link.url} className="relative w-fit px-5 py-2 text-white">
|
||||
<Image
|
||||
src="background.svg"
|
||||
src="/background.svg"
|
||||
alt="button background"
|
||||
className="absolute inset-0 -z-10 h-full w-full rounded object-cover"
|
||||
fill
|
||||
|
18
components/home-page/manufacturers.tsx
Normal file
18
components/home-page/manufacturers.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import ManufacturersGrid from 'components/manufacturers-grid';
|
||||
import Tag from 'components/tag';
|
||||
import { getMetaobjects } from 'lib/shopify';
|
||||
|
||||
const Manufacturers = async () => {
|
||||
const manufacturers = await getMetaobjects('make');
|
||||
return (
|
||||
<div className="px-6 py-20">
|
||||
<div className="mx-auto flex max-w-7xl flex-col gap-3">
|
||||
<Tag text="Get Started" />
|
||||
<h3 className="mb-3 text-3xl font-semibold lg:text-4xl">Browse Parts By Manufacturer</h3>
|
||||
<ManufacturersGrid manufacturers={manufacturers} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Manufacturers;
|
@ -6,7 +6,14 @@ export default function LogoSquare({ dark = false }: { dark?: boolean }) {
|
||||
{dark ? (
|
||||
<Image src="/dark-logo.svg" alt="Logo" width={327} height={61} className="h-full w-full" />
|
||||
) : (
|
||||
<Image src="/logo.svg" alt="Logo" width={327} height={61} className="h-full w-full" />
|
||||
<Image
|
||||
src="/logo.svg"
|
||||
alt="Logo"
|
||||
width={327}
|
||||
height={61}
|
||||
className="h-full w-full"
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
37
components/manufacturers-grid/button-group.tsx
Normal file
37
components/manufacturers-grid/button-group.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { ArrowRightIcon } from '@heroicons/react/16/solid';
|
||||
import { MAKE_FILTER_ID } from 'lib/constants';
|
||||
import { Metaobject } from 'lib/shopify/types';
|
||||
import { createUrl } from 'lib/utils';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
const ButtonGroup = ({ manufacturer }: { manufacturer: Metaobject }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const _newSearchParams = new URLSearchParams(searchParams.toString());
|
||||
const router = useRouter();
|
||||
const handleClick = (type: 'engines' | 'transmissions') => {
|
||||
_newSearchParams.set(MAKE_FILTER_ID, manufacturer.id);
|
||||
|
||||
router.push(createUrl(`/search/${type}`, _newSearchParams));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center md:gap-0">
|
||||
<button
|
||||
className="flex items-center gap-1 rounded border border-primary px-1 py-0.5 text-xs text-primary"
|
||||
onClick={() => handleClick('engines')}
|
||||
>
|
||||
Engines <ArrowRightIcon className="size-3" />
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-1 rounded border border-transparent bg-primary/10 px-1 py-0.5 text-xs text-primary"
|
||||
onClick={() => handleClick('transmissions')}
|
||||
>
|
||||
Transmissions <ArrowRightIcon className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonGroup;
|
49
components/manufacturers-grid/index.tsx
Normal file
49
components/manufacturers-grid/index.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { GlobeAltIcon, StarIcon } from '@heroicons/react/24/outline';
|
||||
import { Metaobject } from 'lib/shopify/types';
|
||||
import ButtonGroup from './button-group';
|
||||
import ManufacturerItem from './item';
|
||||
|
||||
type ManufacturersGridProps = {
|
||||
manufacturers: Metaobject[];
|
||||
variant?: 'engine' | 'transmission' | 'home';
|
||||
};
|
||||
|
||||
const ManufacturersGrid = ({ manufacturers, variant = 'home' }: ManufacturersGridProps) => {
|
||||
const popularManufacturers = manufacturers.filter(
|
||||
(manufacturer) => manufacturer.is_popular === 'true'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-auto max-h-[700px] w-full overflow-auto rounded px-10 py-6 shadow">
|
||||
<p className="flex items-center gap-2">
|
||||
<StarIcon className="size-4" />
|
||||
<span className="text-sm font-medium text-blue-800">Popular Manufacturers</span>
|
||||
</p>
|
||||
<div className="mt-6 grid grid-cols-2 gap-x-12 gap-y-7 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{popularManufacturers.map((manufacturer) => (
|
||||
<div key={manufacturer.id} className="flex flex-col gap-3">
|
||||
<ManufacturerItem manufacturer={manufacturer} />
|
||||
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<hr className="my-10 w-full" />
|
||||
<p className="flex items-center gap-2">
|
||||
<GlobeAltIcon className="size-4" />
|
||||
<span className="text-sm font-medium text-blue-800">All Manufacturers</span>
|
||||
</p>
|
||||
<div className="mt-6 grid grid-cols-2 gap-x-12 gap-y-7 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{manufacturers
|
||||
.toSorted((a, b) => a.display_name!.localeCompare(b.display_name!))
|
||||
.map((manufacturer) => (
|
||||
<div key={manufacturer.id} className="flex flex-col gap-3">
|
||||
<ManufacturerItem manufacturer={manufacturer} />
|
||||
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManufacturersGrid;
|
29
components/manufacturers-grid/item.tsx
Normal file
29
components/manufacturers-grid/item.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import ImageDisplay from 'components/page/image-display';
|
||||
import { Metaobject } from 'lib/shopify/types';
|
||||
import { Suspense } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const ManufacturerItem = ({
|
||||
manufacturer,
|
||||
className
|
||||
}: {
|
||||
manufacturer: Metaobject;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={twMerge('flex w-full flex-row items-center justify-between', className)}>
|
||||
<span className="text-sm leading-5">{manufacturer.display_name}</span>
|
||||
<div className="hidden md:block">
|
||||
<Suspense>
|
||||
<ImageDisplay
|
||||
fileId={manufacturer.logo as string}
|
||||
title={manufacturer.display_name || 'Logo'}
|
||||
className="aspect-2 h-auto w-auto"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManufacturerItem;
|
@ -19,7 +19,7 @@ const RichTextBlock = ({ block }: { block: Content }) => {
|
||||
return block.bold ? (
|
||||
<strong className="font-semibold">{block.value}</strong>
|
||||
) : (
|
||||
<span>{block.value}</span>
|
||||
<span className="font-normal">{block.value}</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -589,7 +589,7 @@ export async function getPage(handle: string): Promise<Page> {
|
||||
|
||||
const page = res.body.data.pageByHandle;
|
||||
|
||||
if (page.metafield) {
|
||||
if (page?.metafield) {
|
||||
const metaobjectIds = parseMetaFieldValue<string[]>(page.metafield) || [];
|
||||
|
||||
const metaobjects = await getMetaobjectsByIds(metaobjectIds);
|
||||
|
@ -4,6 +4,7 @@ export const getMetaobjectsQuery = /* GraphQL */ `
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
type
|
||||
fields {
|
||||
reference {
|
||||
... on Metaobject {
|
||||
|
@ -6,6 +6,7 @@ module.exports = {
|
||||
},
|
||||
images: {
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
dangerouslyAllowSVG: true,
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
|
BIN
public/about.png
BIN
public/about.png
Binary file not shown.
Before Width: | Height: | Size: 457 KiB After Width: | Height: | Size: 573 KiB |
Loading…
x
Reference in New Issue
Block a user