feat: implement browse by manufacturers section

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-06-09 12:30:18 +07:00
parent 882d1db67c
commit 9e9573c7be
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
19 changed files with 220 additions and 54 deletions

View File

@ -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">
<Suspense>
<About />
</Suspense>
<Suspense>
<WhyChoose />
</Suspense>
<Suspense>
<FAQ />
</Suspense>
<Suspense>
<Manufacturers />
</Suspense>
</div>
<Suspense>
<Footer />

View File

@ -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
}
)}
<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}
</button>
{item.title}
</Tab>
))}
</div>
</TabList>
<TabPanels className="mt-3">
{items.map((item) => (
<TabPanel key={item.title}>
<RichTextDisplay contentBlocks={JSON.parse(item.content || '{}').children} />
</TabPanel>
))}
</TabPanels>
</TabGroup>
);
};

View File

@ -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">

View File

@ -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

View File

@ -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"

View File

@ -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');

View File

@ -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');

View File

@ -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">
<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 object-contain"
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>

View File

@ -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

View 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;

View File

@ -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>
);

View 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;

View 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;

View 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;

View File

@ -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>
);
}

View File

@ -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);

View File

@ -4,6 +4,7 @@ export const getMetaobjectsQuery = /* GraphQL */ `
edges {
node {
id
type
fields {
reference {
... on Metaobject {

View File

@ -6,6 +6,7 @@ module.exports = {
},
images: {
formats: ['image/avif', 'image/webp'],
dangerouslyAllowSVG: true,
remotePatterns: [
{
protocol: 'https',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

After

Width:  |  Height:  |  Size: 573 KiB