mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 20:57:51 +00:00
Merge remote-tracking branch 'origin' into CPP-153
This commit is contained in:
commit
f90da11e31
@ -13,3 +13,16 @@ input,
|
||||
button {
|
||||
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-neutral-600 dark:focus-visible:ring-offset-neutral-900;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Hide scrollbar for WebKit browsers */
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Firefox */
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ export const metadata = {
|
||||
export default async function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className={GeistSans.variable}>
|
||||
<body className="bg-neutral-50 text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
|
||||
<body className="bg-white text-black selection:bg-teal-300 dark:bg-neutral-900 dark:text-white dark:selection:bg-pink-500 dark:selection:text-white">
|
||||
<header>
|
||||
<Banner />
|
||||
<Navbar />
|
||||
|
@ -34,7 +34,7 @@ export default async function HomePage() {
|
||||
<WhyChoose />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<FAQ />
|
||||
<FAQ handle="home-page-faqs" />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<Manufacturers />
|
||||
|
@ -81,12 +81,16 @@ export default async function ProductPage({ params }: { params: { handle: string
|
||||
__html: JSON.stringify(productJsonLd)
|
||||
}}
|
||||
/>
|
||||
<div className="mx-auto mt-4 max-w-screen-2xl px-4">
|
||||
<div className="mx-auto mt-4 max-w-screen-2xl px-8 xl:px-4">
|
||||
<div className="hidden lg:block">
|
||||
<BreadcrumbComponent type="product" handle={product.handle} />
|
||||
</div>
|
||||
<div className="my-3 flex flex-col space-x-0 rounded-lg border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-black md:p-10 lg:flex-row lg:gap-8 lg:space-x-3">
|
||||
<div className="my-3 flex flex-col space-x-0 lg:flex-row lg:gap-8 lg:space-x-3">
|
||||
<div className="h-full w-full basis-full lg:basis-7/12">
|
||||
<ProductDescription product={product} />
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block lg:basis-5/12">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="aspect-square relative h-full max-h-[550px] w-full overflow-hidden" />
|
||||
@ -100,18 +104,12 @@ export default async function ProductPage({ params }: { params: { handle: string
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className="basis-full lg:basis-5/12">
|
||||
<ProductDescription product={product} />
|
||||
</div>
|
||||
</div>
|
||||
<Suspense>
|
||||
<RelatedProducts id={product.id} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense>
|
||||
<Footer />
|
||||
</Suspense>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -4,8 +4,10 @@ import { notFound } from 'next/navigation';
|
||||
|
||||
import Breadcrumb from 'components/breadcrumb';
|
||||
import BreadcrumbHome from 'components/breadcrumb/breadcrumb-home';
|
||||
import FAQ from 'components/faq';
|
||||
import YMMFilters, { YMMFiltersPlaceholder } from 'components/filters';
|
||||
import Grid from 'components/grid';
|
||||
import Manufacturers from 'components/home-page/manufacturers';
|
||||
import ProductsList from 'components/layout/products-list';
|
||||
import { getProductsInCollection } from 'components/layout/products-list/actions';
|
||||
import FiltersContainer, {
|
||||
@ -14,6 +16,7 @@ import FiltersContainer, {
|
||||
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 HelpfulLinks from 'components/layout/search/helpful-links';
|
||||
import ProductsGridPlaceholder from 'components/layout/search/placeholder';
|
||||
import SortingMenu from 'components/layout/search/sorting-menu';
|
||||
import { Suspense } from 'react';
|
||||
@ -58,7 +61,7 @@ async function CategoryPage({
|
||||
<MobileFilters filters={filters} menu={<SubMenu collection={params.collection} />} />
|
||||
<SortingMenu />
|
||||
</div>
|
||||
<Grid className="grid-cols-1 sm:grid-cols-2 sm:gap-x-8 lg:grid-cols-3">
|
||||
<Grid className="hide-scrollbar max-h-[1000px] grid-cols-1 overflow-y-auto 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>
|
||||
) : (
|
||||
@ -80,37 +83,53 @@ export default async function CategorySearchPage(props: {
|
||||
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>
|
||||
<>
|
||||
<div className="mx-auto mt-6 max-w-screen-2xl px-8 pb-10">
|
||||
<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} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense fallback={<HeaderPlaceholder />} key={`header-${props.params.collection}`}>
|
||||
<Header collection={props.params.collection} />
|
||||
</Suspense>
|
||||
<SubMenu collection={props.params.collection} />
|
||||
<h3 className="sr-only">Filters</h3>
|
||||
<Suspense
|
||||
fallback={<FiltersListPlaceholder />}
|
||||
key={`filters-${props.params.collection}`}
|
||||
>
|
||||
<FiltersContainer searchParams={props.searchParams} />
|
||||
<HelpfulLinks collection={props.params.collection} />
|
||||
</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} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense fallback={<HeaderPlaceholder />} key={`header-${props.params.collection}`}>
|
||||
<Header collection={props.params.collection} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense
|
||||
fallback={<ProductsGridPlaceholder />}
|
||||
key={`products-${props.params.collection}`}
|
||||
>
|
||||
<CategoryPage {...props} />
|
||||
</Suspense>
|
||||
<Suspense
|
||||
fallback={<ProductsGridPlaceholder />}
|
||||
key={`products-${props.params.collection}`}
|
||||
>
|
||||
<CategoryPage {...props} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FAQ handle="plp-faqs" />
|
||||
<Suspense>
|
||||
<Manufacturers
|
||||
variant={
|
||||
(props.params.collection as string).includes('engines') ? 'engines' : 'transmissions'
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
import Footer from 'components/layout/footer';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export default function SearchLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<div className="min-h-[500px] lg:min-h-[800px]">{children}</div>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
|
@ -12,7 +12,10 @@ import {
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function addItem(prevState: any, selectedVariantIds: Array<string>) {
|
||||
export async function addItem(
|
||||
prevState: any,
|
||||
selectedVariantIds: Array<{ merchandiseId: string; quantity: number }>
|
||||
) {
|
||||
let cartId = cookies().get('cartId')?.value;
|
||||
let cart;
|
||||
|
||||
@ -31,10 +34,8 @@ export async function addItem(prevState: any, selectedVariantIds: Array<string>)
|
||||
}
|
||||
|
||||
try {
|
||||
await addToCart(
|
||||
cartId,
|
||||
selectedVariantIds.map((variantId) => ({ merchandiseId: variantId, quantity: 1 }))
|
||||
);
|
||||
const cart = await addToCart(cartId, selectedVariantIds);
|
||||
console.log({ cartLines: cart.lines });
|
||||
revalidateTag(TAGS.cart);
|
||||
} catch (e) {
|
||||
return 'Error adding item to cart';
|
||||
@ -65,7 +66,6 @@ export async function setMetafields(
|
||||
|
||||
revalidateTag(TAGS.cart);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return 'Error set cart attributes';
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ function SubmitButton({
|
||||
}) {
|
||||
const { pending } = useFormStatus();
|
||||
const buttonClasses =
|
||||
'relative flex w-full items-center justify-center rounded bg-secondary p-4 tracking-wide text-white gap-3';
|
||||
'relative flex w-full items-center justify-center rounded bg-secondary p-3 tracking-wide text-white gap-3';
|
||||
const disabledClasses = 'cursor-not-allowed opacity-60 hover:opacity-60';
|
||||
|
||||
if (!availableForSale) {
|
||||
@ -80,11 +80,20 @@ export function AddToCart({
|
||||
const coreVariantId = searchParams.get(CORE_VARIANT_ID_KEY);
|
||||
|
||||
// remove special core-waiver value as it is not a valid variant
|
||||
const selectedVariantIds = [coreVariantId, selectedVariantId]
|
||||
.filter(Boolean)
|
||||
.filter((value) => value !== CORE_WAIVER) as string[];
|
||||
const addingVariants = (
|
||||
[coreVariantId, selectedVariantId]
|
||||
.filter(Boolean)
|
||||
.filter((value) => value !== CORE_WAIVER) as string[]
|
||||
).map((id) => ({ merchandiseId: id, quantity: 1 }));
|
||||
|
||||
const actionWithVariant = formAction.bind(null, selectedVariantIds);
|
||||
if (variant?.addOnProduct) {
|
||||
addingVariants.push({
|
||||
merchandiseId: variant.addOnProduct.id,
|
||||
quantity: variant.addOnProduct.quantity
|
||||
});
|
||||
}
|
||||
|
||||
const actionWithVariant = formAction.bind(null, addingVariants);
|
||||
|
||||
return (
|
||||
<form action={actionWithVariant}>
|
||||
|
@ -36,10 +36,11 @@ function SubmitButton() {
|
||||
|
||||
export function DeleteItemButton({ item }: { item: CartItem }) {
|
||||
const [message, formAction] = useFormState(removeItem, null);
|
||||
const { id: itemId, coreCharge } = item;
|
||||
const { id: itemId, coreCharge, addOnProduct } = item;
|
||||
const actionWithVariant = formAction.bind(null, [
|
||||
itemId,
|
||||
...(coreCharge?.id ? [coreCharge.id] : [])
|
||||
...(coreCharge?.id ? [coreCharge.id] : []),
|
||||
...(addOnProduct?.id ? [addOnProduct.id] : [])
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -51,11 +51,19 @@ export function EditItemQuantityButton({ item, type }: { item: CartItem; type: '
|
||||
if (item.coreCharge) {
|
||||
payload.push({
|
||||
lineId: item.coreCharge.id,
|
||||
variantId: item.coreCharge.id,
|
||||
variantId: item.coreCharge.merchandise.id,
|
||||
quantity
|
||||
});
|
||||
}
|
||||
|
||||
if (item.addOnProduct) {
|
||||
payload.push({
|
||||
lineId: item.addOnProduct.id,
|
||||
variantId: item.addOnProduct.merchandise.id,
|
||||
quantity: quantity * item.addOnProduct.quantity
|
||||
});
|
||||
}
|
||||
|
||||
const actionWithVariant = formAction.bind(null, payload);
|
||||
|
||||
return (
|
||||
|
@ -21,7 +21,11 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
const openCart = () => setIsOpen(true);
|
||||
const closeCart = () => setIsOpen(false);
|
||||
const { control, handleSubmit } = useForm<VehicleFormSchema>({
|
||||
resolver: zodResolver(vehicleFormSchema)
|
||||
resolver: zodResolver(vehicleFormSchema),
|
||||
defaultValues: {
|
||||
customer_vin: cart?.attributes.find((a) => a.key === 'customer_vin')?.value || '',
|
||||
customer_mileage: cart?.attributes.find((a) => a.key === 'customer_mileage')?.value || ''
|
||||
}
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { PhoneIcon } from '@heroicons/react/24/outline';
|
||||
import { getMetaobject } from 'lib/shopify';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import Image from 'next/image';
|
||||
import { Suspense } from 'react';
|
||||
import AccordionBlock from './page/accordion-block';
|
||||
import Tag from './tag';
|
||||
|
||||
const { SITE_NAME } = process.env;
|
||||
|
||||
const FAQ = async () => {
|
||||
const FAQ = async ({ handle }: { handle: string }) => {
|
||||
const faqs = await getMetaobject({
|
||||
handle: { handle: `${kebabCase(SITE_NAME)}-faqs`, type: 'accordion' }
|
||||
handle: { handle, type: 'accordion' }
|
||||
});
|
||||
|
||||
if (!faqs) return null;
|
||||
|
@ -6,7 +6,7 @@ import { Menu, Metaobject } from 'lib/shopify/types';
|
||||
import { createUrl, findParentCollection } from 'lib/shopify/utils';
|
||||
import get from 'lodash.get';
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import FilterField from './field';
|
||||
|
||||
type FiltersListProps = {
|
||||
@ -31,11 +31,13 @@ const FiltersList = ({ years, makes, models, menu, autoFocusField }: FiltersList
|
||||
PART_TYPES.find((type) => type.value === partTypeCollection) || null
|
||||
);
|
||||
|
||||
const makeIdFromSearchParams = searchParams.get(MAKE_FILTER_ID);
|
||||
|
||||
const [make, setMake] = useState<Metaobject | null>(
|
||||
(partType &&
|
||||
makes.find((make) =>
|
||||
searchParams.get(MAKE_FILTER_ID)
|
||||
? make.id === searchParams.get(MAKE_FILTER_ID)
|
||||
makeIdFromSearchParams
|
||||
? make.id === makeIdFromSearchParams
|
||||
: params.collection?.includes(make.name!.toLowerCase())
|
||||
)) ||
|
||||
null
|
||||
@ -52,6 +54,22 @@ const FiltersList = ({ years, makes, models, menu, autoFocusField }: FiltersList
|
||||
|
||||
const disabled = !partType || !make || !model || !year;
|
||||
|
||||
useEffect(() => {
|
||||
if (partType) {
|
||||
const _make = makes.find((make) =>
|
||||
makeIdFromSearchParams
|
||||
? make.id === makeIdFromSearchParams
|
||||
: params.collection?.includes(make.name!.toLowerCase())
|
||||
);
|
||||
|
||||
if (_make) {
|
||||
setMake(_make);
|
||||
setModel(null);
|
||||
setYear(null);
|
||||
}
|
||||
}
|
||||
}, [makeIdFromSearchParams, makes, params.collection, partType]);
|
||||
|
||||
const onChangeMake = (value: Metaobject | null) => {
|
||||
setMake(value);
|
||||
setModel(null);
|
||||
|
@ -1,10 +1,52 @@
|
||||
import { ArrowRightIcon, PhotoIcon } from '@heroicons/react/24/solid';
|
||||
import clsx from 'clsx';
|
||||
import Price from 'components/price';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import { CONDITIONS } from 'lib/constants';
|
||||
import { Product, ProductVariant } from 'lib/shopify/types';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
const PriceSection = ({ variants }: { variants: ProductVariant[] }) => {
|
||||
const usedVariants = variants.filter((variant) => variant.condition === CONDITIONS.Used);
|
||||
|
||||
const minUsedVariantPrice = usedVariants.length
|
||||
? usedVariants.reduce(
|
||||
(min, variant) => Math.min(min, Number(variant.price.amount)),
|
||||
Number(usedVariants[0]?.price.amount)
|
||||
)
|
||||
: null;
|
||||
|
||||
const remanVariants = variants.filter(
|
||||
(variant) => variant.condition === CONDITIONS.Remanufactured
|
||||
);
|
||||
|
||||
const minRemanufacturedPrice = remanVariants.length
|
||||
? remanVariants.reduce(
|
||||
(min, variant) => Math.min(min, Number(variant.price.amount)),
|
||||
Number(remanVariants[0]?.price.amount)
|
||||
)
|
||||
: null;
|
||||
|
||||
const currencyCode = variants[0]?.price.currencyCode || 'USD';
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
{typeof minUsedVariantPrice === 'number' && (
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-sm">{CONDITIONS.Used}</span>
|
||||
<Price amount={String(minUsedVariantPrice)} currencyCode={currencyCode} />
|
||||
</div>
|
||||
)}
|
||||
{typeof minRemanufacturedPrice === 'number' && (
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-sm">{CONDITIONS.Remanufactured}</span>
|
||||
<Price amount={String(minRemanufacturedPrice)} currencyCode={currencyCode} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function GridTileImage({
|
||||
active,
|
||||
product,
|
||||
@ -17,7 +59,7 @@ export function GridTileImage({
|
||||
} & React.ComponentProps<typeof Image>) {
|
||||
const metafieldKeys = ['engineCylinders', 'fuelType'] as Partial<keyof Product>[];
|
||||
const shouldShowDescription = metafieldKeys.some((key) => product[key]);
|
||||
|
||||
const variantsWithCondition = product.variants.filter((variant) => variant.condition !== null);
|
||||
return (
|
||||
<div className="flex h-full flex-col rounded-b border bg-white">
|
||||
<div className="grow">
|
||||
@ -76,12 +118,16 @@ export function GridTileImage({
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end border-t py-2">
|
||||
<Price
|
||||
className="text-lg font-medium text-gray-900"
|
||||
amount={product.priceRange.minVariantPrice.amount}
|
||||
currencyCode={product.priceRange.minVariantPrice.currencyCode}
|
||||
/>
|
||||
<div className="flex justify-end border-t py-3">
|
||||
{variantsWithCondition.length ? (
|
||||
<PriceSection variants={variantsWithCondition} />
|
||||
) : (
|
||||
<Price
|
||||
className="text-lg font-medium text-gray-900"
|
||||
amount={product.priceRange.minVariantPrice.amount}
|
||||
currencyCode={product.priceRange.minVariantPrice.currencyCode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,15 +1,13 @@
|
||||
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 { SITE_NAME } = process.env;
|
||||
const About = async () => {
|
||||
const aboutUs = await getMetaobject({
|
||||
handle: { type: 'about_us', handle: `${kebabCase(SITE_NAME)}-about` }
|
||||
handle: { type: 'about_us', handle: 'about-us' }
|
||||
});
|
||||
|
||||
if (!aboutUs) return null;
|
||||
|
@ -2,14 +2,24 @@ import ManufacturersGrid from 'components/manufacturers-grid';
|
||||
import Tag from 'components/tag';
|
||||
import { getMetaobjects } from 'lib/shopify';
|
||||
|
||||
const Manufacturers = async () => {
|
||||
const Manufacturers = async ({
|
||||
variant = 'home'
|
||||
}: {
|
||||
variant?: 'engines' | 'transmissions' | 'home';
|
||||
}) => {
|
||||
const manufacturers = await getMetaobjects('make');
|
||||
const title: Record<typeof variant, string> = {
|
||||
engines: 'Engines',
|
||||
home: 'Parts',
|
||||
transmissions: 'Transmissions'
|
||||
};
|
||||
|
||||
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} />
|
||||
<h3 className="mb-3 text-3xl font-semibold lg:text-4xl">{`Browse ${title[variant]} By Manufacturer`}</h3>
|
||||
<ManufacturersGrid manufacturers={manufacturers} variant={variant} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,14 +1,13 @@
|
||||
import ImageDisplay from 'components/page/image-display';
|
||||
import RichTextDisplay from 'components/page/rich-text-display';
|
||||
import { getMetaobject, getMetaobjectsByIds } from 'lib/shopify';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import { Suspense } from 'react';
|
||||
import Tag from '../tag';
|
||||
|
||||
const { SITE_NAME } = process.env;
|
||||
const WhyChoose = async () => {
|
||||
const whyChooseContent = await getMetaobject({
|
||||
handle: { type: 'why_choose', handle: `${kebabCase(SITE_NAME)}-why-choose` }
|
||||
handle: { type: 'why_choose', handle: 'why-choose' }
|
||||
});
|
||||
|
||||
if (!whyChooseContent || !whyChooseContent.items) return null;
|
||||
|
44
components/layout/search/helpful-links.tsx
Normal file
44
components/layout/search/helpful-links.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { getCollection, getMetaobjectsByIds } from 'lib/shopify';
|
||||
import Link from 'next/link';
|
||||
|
||||
const LinkItem = async ({
|
||||
collectionLinkId,
|
||||
anchorText
|
||||
}: {
|
||||
collectionLinkId: string;
|
||||
anchorText: string;
|
||||
}) => {
|
||||
const collection = await getCollection({ id: collectionLinkId });
|
||||
|
||||
if (!collection) return null;
|
||||
|
||||
return (
|
||||
<Link href={collection.path} className="border p-2 text-sm text-gray-600">
|
||||
{anchorText}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
const HelpfulLinks = async ({ collection }: { collection: string }) => {
|
||||
const collectionData = await getCollection({ handle: collection });
|
||||
if (!collectionData || !collectionData.helpfulLinks) return null;
|
||||
|
||||
const helpfulLinks = await getMetaobjectsByIds(collectionData.helpfulLinks);
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<div className="mb-4 text-sm font-medium text-gray-900">Helpful links</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{helpfulLinks.map((link) => (
|
||||
<LinkItem
|
||||
key={link.id}
|
||||
collectionLinkId={link.collection_link!}
|
||||
anchorText={link.anchor_text!}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpfulLinks;
|
@ -1,11 +1,12 @@
|
||||
import { GlobeAltIcon, StarIcon } from '@heroicons/react/24/outline';
|
||||
import { MAKE_FILTER_ID } from 'lib/constants';
|
||||
import { Metaobject } from 'lib/shopify/types';
|
||||
import ButtonGroup from './button-group';
|
||||
import ManufacturerItem from './item';
|
||||
|
||||
type ManufacturersGridProps = {
|
||||
manufacturers: Metaobject[];
|
||||
variant?: 'engine' | 'transmission' | 'home';
|
||||
variant?: 'engines' | 'transmissions' | 'home';
|
||||
};
|
||||
|
||||
const ManufacturersGrid = ({ manufacturers, variant = 'home' }: ManufacturersGridProps) => {
|
||||
@ -22,7 +23,15 @@ const ManufacturersGrid = ({ manufacturers, variant = 'home' }: ManufacturersGri
|
||||
<div className="mt-6 grid grid-cols-2 gap-x-12 gap-y-5 md:grid-cols-3 md:gap-y-8 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{popularManufacturers.map((manufacturer) => (
|
||||
<div key={manufacturer.id} className="flex flex-col gap-2">
|
||||
<ManufacturerItem manufacturer={manufacturer} />
|
||||
{variant === 'home' ? (
|
||||
<ManufacturerItem manufacturer={manufacturer} />
|
||||
) : (
|
||||
<ManufacturerItem
|
||||
manufacturer={manufacturer}
|
||||
className={'rounded border border-primary px-2 py-1'}
|
||||
href={`/search/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
||||
/>
|
||||
)}
|
||||
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
||||
</div>
|
||||
))}
|
||||
@ -37,7 +46,15 @@ const ManufacturersGrid = ({ manufacturers, variant = 'home' }: ManufacturersGri
|
||||
.toSorted((a, b) => a.display_name!.localeCompare(b.display_name!))
|
||||
.map((manufacturer) => (
|
||||
<div key={manufacturer.id} className="flex flex-col gap-2">
|
||||
<ManufacturerItem manufacturer={manufacturer} />
|
||||
{variant === 'home' ? (
|
||||
<ManufacturerItem manufacturer={manufacturer} />
|
||||
) : (
|
||||
<ManufacturerItem
|
||||
manufacturer={manufacturer}
|
||||
className={'rounded border border-primary px-2 py-1'}
|
||||
href={`/search/${variant}?${MAKE_FILTER_ID}=${manufacturer.id}`}
|
||||
/>
|
||||
)}
|
||||
{variant === 'home' && <ButtonGroup manufacturer={manufacturer} />}
|
||||
</div>
|
||||
))}
|
||||
|
@ -1,16 +1,19 @@
|
||||
import ImageDisplay from 'components/page/image-display';
|
||||
import { Metaobject } from 'lib/shopify/types';
|
||||
import Link from 'next/link';
|
||||
import { Suspense } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const ManufacturerItem = ({
|
||||
manufacturer,
|
||||
className
|
||||
className,
|
||||
href
|
||||
}: {
|
||||
manufacturer: Metaobject;
|
||||
className?: string;
|
||||
href?: string;
|
||||
}) => {
|
||||
return (
|
||||
const children = (
|
||||
<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">
|
||||
@ -25,6 +28,11 @@ const ManufacturerItem = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return <Link href={href}>{children}</Link>;
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
||||
export default ManufacturerItem;
|
||||
|
@ -6,7 +6,8 @@ const Price = ({
|
||||
as,
|
||||
currencyCode = 'USD',
|
||||
currencyCodeClassName,
|
||||
showCurrency = false
|
||||
showCurrency = false,
|
||||
prefix
|
||||
}: {
|
||||
amount: string;
|
||||
as?: 'p' | 'span';
|
||||
@ -14,6 +15,7 @@ const Price = ({
|
||||
currencyCode: string;
|
||||
currencyCodeClassName?: string;
|
||||
showCurrency?: boolean;
|
||||
prefix?: string;
|
||||
} & React.ComponentProps<'p'>) => {
|
||||
// Convert string to float and check if it is zero
|
||||
const price = parseFloat(amount);
|
||||
@ -27,6 +29,7 @@ const Price = ({
|
||||
// Otherwise, format and display the price
|
||||
return (
|
||||
<Component suppressHydrationWarning={true} className={className}>
|
||||
{prefix}
|
||||
{new Intl.NumberFormat(undefined, {
|
||||
style: 'currency',
|
||||
currency: currencyCode,
|
||||
|
16
components/product/additional-information.tsx
Normal file
16
components/product/additional-information.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import Details from './details';
|
||||
import ShippingPolicy from './shipping-policy';
|
||||
import WarrantyPolicy from './warranty-policy';
|
||||
|
||||
const AdditionalInformation = ({ product }: { product: Product }) => {
|
||||
return (
|
||||
<div className="my-5 w-full divide-y">
|
||||
<Details product={product} />
|
||||
<WarrantyPolicy />
|
||||
<ShippingPolicy />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdditionalInformation;
|
@ -93,8 +93,8 @@ const CoreCharge = ({ variants }: CoreChargeProps) => {
|
||||
period, you will never need to pay the core charge.
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
If you don't manage to return the old part within the 30-day period, we will then
|
||||
charge you the core charge. This keeps more money in your pocket upfront.
|
||||
If you don't manage to return the old part within the 30-day period, we will
|
||||
then charge you the core charge. This keeps more money in your pocket upfront.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
|
114
components/product/delivery.tsx
Normal file
114
components/product/delivery.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { TruckIcon } from '@heroicons/react/24/outline';
|
||||
import Price from 'components/price';
|
||||
import SideDialog from 'components/side-dialog';
|
||||
import { DELIVERY_OPTION_KEY } from 'lib/constants';
|
||||
import { cn, createUrl } from 'lib/utils';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { ReactNode, useState } from 'react';
|
||||
|
||||
const options = ['Commercial', 'Residential'] as const;
|
||||
type Option = (typeof options)[number];
|
||||
|
||||
export const deliveryOptions: Array<{
|
||||
key: Option;
|
||||
template: ReactNode;
|
||||
price: number;
|
||||
}> = [
|
||||
{
|
||||
template: <span className="font-bold">Commercial</span>,
|
||||
price: 299,
|
||||
key: 'Commercial'
|
||||
},
|
||||
{
|
||||
template: <span className="font-bold">Residential</span>,
|
||||
price: 398,
|
||||
key: 'Residential'
|
||||
}
|
||||
];
|
||||
|
||||
const Delivery = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const [openingDialog, setOpeningDialog] = useState<'information' | 'terms-conditions' | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const newSearchParams = new URLSearchParams(searchParams.toString());
|
||||
const selectedDeliveryOption = newSearchParams.get(DELIVERY_OPTION_KEY);
|
||||
|
||||
const handleSelectDelivery = (option: Option) => {
|
||||
newSearchParams.set(DELIVERY_OPTION_KEY, option);
|
||||
|
||||
const newUrl = createUrl(pathname, newSearchParams);
|
||||
router.replace(newUrl, { scroll: false });
|
||||
};
|
||||
|
||||
if (!selectedDeliveryOption) {
|
||||
handleSelectDelivery(options[0]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col text-xs lg:text-sm">
|
||||
<div className="mb-3 flex flex-row items-center space-x-1 divide-x divide-gray-400 leading-none lg:space-x-3">
|
||||
<div className="flex flex-row items-center space-x-2 text-base font-medium">
|
||||
<TruckIcon className="h-5 w-5" />
|
||||
<span>Delivery</span>
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
<button
|
||||
onClick={() => setOpeningDialog('information')}
|
||||
className="text-xs text-blue-800 hover:underline lg:text-sm"
|
||||
>
|
||||
Information
|
||||
</button>
|
||||
<SideDialog
|
||||
title="Information"
|
||||
onClose={() => setOpeningDialog(null)}
|
||||
open={openingDialog === 'information'}
|
||||
>
|
||||
<p>Information</p>
|
||||
</SideDialog>
|
||||
</div>
|
||||
<div className="pl-2">
|
||||
<button
|
||||
onClick={() => setOpeningDialog('terms-conditions')}
|
||||
className="text-xs text-blue-800 hover:underline lg:text-sm"
|
||||
>
|
||||
Terms & Conditions
|
||||
</button>
|
||||
<SideDialog
|
||||
title="Terms & Conditions"
|
||||
onClose={() => setOpeningDialog(null)}
|
||||
open={openingDialog === 'terms-conditions'}
|
||||
>
|
||||
<p>Terms & Conditions</p>
|
||||
</SideDialog>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="flex min-h-16 flex-row space-x-4 pt-2">
|
||||
{deliveryOptions.map((option) => (
|
||||
<li className="flex w-32" key={option.key}>
|
||||
<button
|
||||
onClick={() => handleSelectDelivery(option.key)}
|
||||
className={cn(
|
||||
'font-base flex w-full flex-col flex-wrap items-center justify-center space-y-0.5 rounded border text-center text-xs',
|
||||
{
|
||||
'border-0 ring-2 ring-secondary': selectedDeliveryOption === option.key
|
||||
}
|
||||
)}
|
||||
>
|
||||
{option.template}
|
||||
<Price amount={String(option.price)} currencyCode="USD" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Delivery;
|
82
components/product/details.tsx
Normal file
82
components/product/details.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import Price from 'components/price';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import DisclosureSection from './disclosure-section';
|
||||
|
||||
const Details = ({ product }: { product: Product }) => {
|
||||
const searchParams = useSearchParams();
|
||||
const variants = product.variants;
|
||||
|
||||
const variant = variants.find((variant) =>
|
||||
variant.selectedOptions.every(
|
||||
(option) => option.value === searchParams.get(option.name.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const details = [
|
||||
...(product.transmissionTag
|
||||
? [
|
||||
{
|
||||
title: 'Transmission Tag',
|
||||
value: product.transmissionTag.join()
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(product.transmissionCode
|
||||
? [
|
||||
{
|
||||
title: 'Transmission Code',
|
||||
value: product.transmissionCode.join()
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(product.transmissionSpeeds
|
||||
? [
|
||||
{
|
||||
title: 'Transmission Speeds',
|
||||
value: product.transmissionSpeeds.map((speed) => `${speed}-Speed`).join()
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
return (
|
||||
<DisclosureSection title="Product Details" defaultOpen>
|
||||
<div className="flex w-full items-center p-1">
|
||||
<span className="basis-2/5">Condition</span>
|
||||
<span>{variant?.condition || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex w-full items-center bg-gray-100 p-1">
|
||||
<span className="basis-2/5">Price</span>
|
||||
<Price
|
||||
amount={variant?.price.amount || product.priceRange.minVariantPrice.amount}
|
||||
currencyCode={
|
||||
variant?.price.currencyCode || product.priceRange.minVariantPrice.currencyCode
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full items-center p-1">
|
||||
<span className="basis-2/5">Warranty</span>
|
||||
<span />
|
||||
</div>
|
||||
<div className="flex w-full items-center bg-gray-100 p-1">
|
||||
<span className="basis-2/5">Cylinders</span>
|
||||
<span>{product.engineCylinders?.map((cylinder) => `${cylinder} Cylinders`).join()}</span>
|
||||
</div>
|
||||
|
||||
{details.map(({ title, value }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx('flex w-full items-center p-1', { 'bg-gray-100': index % 2 !== 0 })}
|
||||
>
|
||||
<span className="basis-2/5">{title}</span>
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</DisclosureSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default Details;
|
25
components/product/disclosure-section.tsx
Normal file
25
components/product/disclosure-section.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type DisclosureProps = {
|
||||
children: ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const DisclosureSection = ({ children, title, defaultOpen }: DisclosureProps) => {
|
||||
return (
|
||||
<Disclosure as="div" className="p-3" defaultOpen={defaultOpen}>
|
||||
<DisclosureButton className="group flex w-full items-center justify-between">
|
||||
<span className="font-medium">{title}</span>
|
||||
<ChevronDownIcon className="size-4 group-data-[open]:rotate-180" />
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel className="mt-2 py-2 text-sm">{children}</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisclosureSection;
|
73
components/product/price-summary.tsx
Normal file
73
components/product/price-summary.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import Price from 'components/price';
|
||||
import { CORE_VARIANT_ID_KEY, CORE_WAIVER, DELIVERY_OPTION_KEY } from 'lib/constants';
|
||||
import { Money, ProductVariant } from 'lib/shopify/types';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { deliveryOptions } from './delivery';
|
||||
|
||||
type PriceSummaryProps = {
|
||||
variants: ProductVariant[];
|
||||
defaultPrice: Money;
|
||||
};
|
||||
|
||||
const PriceSummary = ({ variants, defaultPrice }: PriceSummaryProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const variant = variants.find((variant) =>
|
||||
variant.selectedOptions.every(
|
||||
(option) => option.value === searchParams.get(option.name.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
const price = variant?.price.amount || defaultPrice.amount;
|
||||
const selectedCoreChargeOption = searchParams.get(CORE_VARIANT_ID_KEY);
|
||||
const selectedDeliveryOption = searchParams.get(DELIVERY_OPTION_KEY);
|
||||
const deliveryPrice =
|
||||
deliveryOptions.find((option) => option.key === selectedDeliveryOption)?.price ?? 0;
|
||||
const currencyCode = variant?.price.currencyCode || defaultPrice.currencyCode;
|
||||
const corePrice = selectedCoreChargeOption === CORE_WAIVER ? 0 : variant?.coreCharge?.amount ?? 0;
|
||||
|
||||
const totalPrice = Number(price) + deliveryPrice + Number(corePrice);
|
||||
return (
|
||||
<div className="mb-3 flex flex-col gap-2">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-xl font-semibold">Our Price</span>
|
||||
<Price amount={price} currencyCode={currencyCode} className="text-2xl font-semibold" />
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-sm text-gray-400">{`Core Charge ${selectedCoreChargeOption === CORE_WAIVER ? '(Waived for 30 days)' : ''}`}</span>
|
||||
{selectedCoreChargeOption === CORE_WAIVER ? (
|
||||
<span className="text-sm text-gray-400">{`+$0.00`}</span>
|
||||
) : (
|
||||
<Price
|
||||
amount={variant?.coreCharge?.amount ?? '0'}
|
||||
currencyCode={currencyCode}
|
||||
className="text-sm text-gray-400"
|
||||
prefix="+"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-sm text-gray-400">{`Flat Rate Shipping (${selectedDeliveryOption} address)`}</span>
|
||||
<Price
|
||||
amount={String(deliveryPrice)}
|
||||
currencyCode={currencyCode}
|
||||
className="text-sm text-gray-400"
|
||||
prefix="+"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-sm text-gray-400">To Pay Today</span>
|
||||
<Price
|
||||
amount={String(totalPrice)}
|
||||
currencyCode={currencyCode}
|
||||
className="text-sm text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PriceSummary;
|
@ -2,7 +2,11 @@ import { AddToCart } from 'components/cart/add-to-cart';
|
||||
import Prose from 'components/prose';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
import { Suspense } from 'react';
|
||||
import AdditionalInformation from './additional-information';
|
||||
import CoreCharge from './core-charge';
|
||||
import Delivery from './delivery';
|
||||
import PriceSummary from './price-summary';
|
||||
import ProductDetails from './product-details';
|
||||
import SpecialOffer from './special-offer';
|
||||
import VariantDetails from './vairant-details';
|
||||
import { VariantSelector } from './variant-selector';
|
||||
@ -11,7 +15,7 @@ import Warranty from './warranty';
|
||||
export function ProductDescription({ product }: { product: Product }) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 flex flex-col dark:border-neutral-700">
|
||||
<div className="mb-4 flex flex-col">
|
||||
<h1 className="text-xl font-bold md:text-2xl">{product.title}</h1>
|
||||
|
||||
<VariantDetails
|
||||
@ -34,6 +38,7 @@ export function ProductDescription({ product }: { product: Product }) {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<ProductDetails product={product} />
|
||||
<div className="mb-2 border-t py-4 dark:border-neutral-700">
|
||||
<CoreCharge variants={product.variants} />
|
||||
</div>
|
||||
@ -42,12 +47,16 @@ export function ProductDescription({ product }: { product: Product }) {
|
||||
<Warranty />
|
||||
</div>
|
||||
|
||||
<div className="mb-2 border-t py-4 dark:border-neutral-700">
|
||||
<Delivery />
|
||||
</div>
|
||||
|
||||
<PriceSummary variants={product.variants} defaultPrice={product.priceRange.minVariantPrice} />
|
||||
<Suspense fallback={null}>
|
||||
<AddToCart variants={product.variants} availableForSale={product.availableForSale} />
|
||||
</Suspense>
|
||||
<div className="mt-4 border-t pt-4">
|
||||
<SpecialOffer />
|
||||
</div>
|
||||
<SpecialOffer />
|
||||
<AdditionalInformation product={product} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
51
components/product/product-details.tsx
Normal file
51
components/product/product-details.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import {
|
||||
BeakerIcon,
|
||||
BoltIcon,
|
||||
CogIcon,
|
||||
CpuChipIcon,
|
||||
CubeTransparentIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { Product } from 'lib/shopify/types';
|
||||
|
||||
const ProductDetails = ({ product }: { product: Product }) => {
|
||||
return (
|
||||
<div className="mb-3 flex flex-col gap-3">
|
||||
<span className="font-medium">Details</span>
|
||||
<div className="grid grid-cols-4 gap-y-3 text-sm">
|
||||
{product.transmissionType && (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CubeTransparentIcon className="size-4 text-primary" />
|
||||
{product.transmissionType}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{product.transmissionSpeeds && product.transmissionSpeeds.length && (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<BoltIcon className="size-4 text-primary" />
|
||||
{`${product.transmissionSpeeds[0]}-Speed`}
|
||||
</div>
|
||||
)}
|
||||
{product.driveType && (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CogIcon className="size-4 text-primary" />
|
||||
{product.driveType}
|
||||
</div>
|
||||
)}
|
||||
{product.engineCylinders?.length && (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<BeakerIcon className="size-4 text-primary" />
|
||||
{`${product.engineCylinders[0]} Cylinders`}
|
||||
</div>
|
||||
)}
|
||||
{product.transmissionCode?.length && (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CpuChipIcon className="size-4 text-primary" />
|
||||
{product.transmissionCode[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetails;
|
44
components/product/shipping-policy.tsx
Normal file
44
components/product/shipping-policy.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import DisclosureSection from './disclosure-section';
|
||||
|
||||
const { SITE_NAME } = process.env;
|
||||
const ShippingPolicy = () => {
|
||||
return (
|
||||
<DisclosureSection title="Shipping & returns">
|
||||
<p>
|
||||
At {SITE_NAME}, we offer a Flat Rate Shipping (Commercial address) service as long as the
|
||||
delivery address is in a commercially zoned location. Unfortunately, residential and home
|
||||
businesses are not considered commercial addresses. A business or commercial address
|
||||
location must be able to receive freight without the requirement of prior appointment setup
|
||||
or notification. This location should also have the capability of unloading the
|
||||
remanufactured transmission with a forklift from the delivery truck. If you don't have
|
||||
a commercial or business address that meets these specifications, you should ship it
|
||||
directly to the dealership or repair shop that is performing the repairs to ensure you enjoy
|
||||
Flat Rate Shipping (Commercial address). Residential delivery or Liftgate service will
|
||||
result in additional $99 fee.
|
||||
</p>
|
||||
<p className="my-3">
|
||||
After placing the order for a remanufactured transmission, most customers will receive it
|
||||
within 7-14 business days — not including holidays or weekends. Please keep in mind that
|
||||
certain locations (remote areas) and locations in Colorado, Utah, New York, Oregon, and
|
||||
California may require an additional delivery fee. In either case, we will always ship your
|
||||
remanufactured transmission out as soon as possible. Because of weather conditions,
|
||||
increasing order volumes, and conditions outside of our control, all shipping times are
|
||||
estimates, not guarantees. It's important to note that {SITE_NAME} will not be liable
|
||||
for any extra fees the carrier may levy due to storage or redelivery. While every
|
||||
transmission from {SITE_NAME} has been rigorously inspected and tested prior to being
|
||||
shipped, damage may occur during transportation.
|
||||
</p>
|
||||
<p>
|
||||
As such, we strongly suggest you carefully inspect your transmission upon receipt. If you
|
||||
notice any missing parts, wrong parts, or damage, you should report it prior to signing any
|
||||
delivery documentation. It"s imperative to report missing parts, damage, or wrong parts
|
||||
at the time of delivery. If you fail to do so prior to signing your shipping documents,
|
||||
responsibility will be placed on the purchaser or receiver. For clarity,
|
||||
"purchaser" refers to any representative of the company designated to sign for the
|
||||
delivery of the remanufactured transmission.
|
||||
</p>
|
||||
</DisclosureSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShippingPolicy;
|
@ -1,28 +1,71 @@
|
||||
import { CurrencyDollarIcon, ShieldCheckIcon, UsersIcon } from '@heroicons/react/24/outline';
|
||||
import { TruckIcon } from '@heroicons/react/24/solid';
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
CurrencyDollarIcon,
|
||||
ShieldCheckIcon,
|
||||
StarIcon,
|
||||
TruckIcon,
|
||||
UsersIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const SpecialOffer = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3 text-base font-medium tracking-tight">Special Offers</div>
|
||||
<div className="flex flex-col space-y-2 pl-2 text-sm tracking-normal text-neutral-800 lg:text-base dark:text-white">
|
||||
<p className="flex items-center gap-3">
|
||||
<TruckIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> Flat Rate Shipping
|
||||
(Commercial Address)
|
||||
</p>
|
||||
<p className="flex items-center gap-3">
|
||||
<ShieldCheckIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> Up to 5 Years
|
||||
Unlimited Miles Warranty
|
||||
</p>
|
||||
<p className="flex items-center gap-3">
|
||||
<UsersIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> Excellent Customer Support
|
||||
</p>
|
||||
<p className="flex items-center gap-3">
|
||||
<CurrencyDollarIcon className="h-4 w-4 text-secondary lg:h-5 lg:w-5" /> No Core Charge for
|
||||
30 days
|
||||
</p>
|
||||
<div className="mt-10 grid grid-cols-2 gap-y-5 xl:grid-cols-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<TruckIcon className="size-12 text-primary" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium uppercase">Flat Rate Shipping</span>
|
||||
<span className="text-sm font-light">
|
||||
We offer a flat $299 shipping fee to commercial addresses
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<div className="flex items-start gap-3">
|
||||
<CurrencyDollarIcon className="size-10 text-primary" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium uppercase">Best Price Guarantee</span>
|
||||
<span className="text-sm font-light">
|
||||
We will match or beat any competitor's pricing
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<ShieldCheckIcon className="size-8 text-primary" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium uppercase">Unbeatable Warranty</span>
|
||||
<span className="text-sm font-light">Up to 5 years with unlimited miles</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<UsersIcon className="size-10 text-primary" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium uppercase">Excellent Support</span>
|
||||
<span className="text-sm font-light">
|
||||
End-to-end, expert care from our customer service team
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<ArrowPathIcon className="size-10 text-primary" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium uppercase">Core Charge Waiver</span>
|
||||
<span className="text-sm font-light">
|
||||
Avoid the core charge by returning within 30 days
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<StarIcon className="size-10 text-primary" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium uppercase">Free Core Return</span>
|
||||
<span className="text-sm font-light">
|
||||
Unlike competitors, we pay for the return of your core
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { CheckCircleIcon } from '@heroicons/react/24/outline';
|
||||
import Price from 'components/price';
|
||||
import { Money, ProductVariant } from 'lib/shopify/types';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
@ -20,17 +21,23 @@ const VariantDetails = ({ variants, defaultPrice }: VariantDetailsProps) => {
|
||||
const price = variant?.price.amount || defaultPrice.amount;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-5 flex items-center justify-start gap-x-2">
|
||||
<p className="text-sm">SKU: {variant?.sku || 'N/A'}</p>
|
||||
<p className="text-sm">Condition: {variant?.condition || 'N/A'}</p>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<Price
|
||||
amount={price}
|
||||
currencyCode={variant?.price.currencyCode || defaultPrice.currencyCode}
|
||||
className="text-2xl font-semibold"
|
||||
/>
|
||||
</>
|
||||
<div className="mt-2 flex items-center justify-start gap-x-2">
|
||||
{variant?.availableForSale ? (
|
||||
<div className="flex items-center gap-1 text-sm text-green-500">
|
||||
<CheckCircleIcon className="size-5" /> In Stock
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm text-red-600">Out of Stock</span>
|
||||
)}
|
||||
<p className="text-sm">Condition: {variant?.condition || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
102
components/product/warranty-policy.tsx
Normal file
102
components/product/warranty-policy.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
ArrowsRightLeftIcon,
|
||||
CurrencyDollarIcon,
|
||||
FlagIcon
|
||||
} from '@heroicons/react/24/outline';
|
||||
import DisclosureSection from './disclosure-section';
|
||||
|
||||
const { SITE_NAME } = process.env;
|
||||
|
||||
const WarrantyPolicy = () => {
|
||||
return (
|
||||
<DisclosureSection title="Warranty">
|
||||
<div className="mb-3 font-medium">Year 2001 and Newer</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Personal/Individual Transmission Warranty</span>
|
||||
<span>60 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-100 p-1">
|
||||
<span className="basis-1/2">Commercial Transmissions Warranty</span>
|
||||
<span>Prior to 03/01/2020 18 Months/ 100,000 Miles</span>
|
||||
</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Commercial Transmissions Warranty</span>
|
||||
<span>Effective 03/01/2020 36 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-100 p-1">
|
||||
<span className="basis-1/2">Continuously Variable Transmission (CVT) Warranty</span>
|
||||
<span>36 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Manual Transmission Warranty</span>
|
||||
<span>36 Months/ Unlimited Miles</span>
|
||||
</div>
|
||||
<div className="my-3 font-medium">Year 2000 and Older</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Personal/Individual Transmission Warranty</span>
|
||||
<span>36 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-100 p-1">
|
||||
<span className="basis-1/2">Commercial Transmissions Warranty</span>
|
||||
<span>18 Months/ 100,000 Miles</span>
|
||||
</div>
|
||||
<div className="flex items-center p-1">
|
||||
<span className="basis-1/2">Commercial Transmissions Warranty</span>
|
||||
<span>36 Months/ Unlimited Mileage</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-100 p-1">
|
||||
<span className="basis-1/2">Continuously Variable Transmission (CVT) Warranty</span>
|
||||
<span>36 Months/ Unlimited Miles</span>
|
||||
</div>
|
||||
<div className="my-5">
|
||||
<div className="mb-1 flex items-center gap-2 font-medium">
|
||||
<ArrowsRightLeftIcon className="size-4 text-primary" />
|
||||
Easy, Hassle-Free, Transferable Warranty
|
||||
</div>
|
||||
<p>
|
||||
At {SITE_NAME}, we offer an easy, transferable, hassle-free warranty. Instead of being
|
||||
associated only with you, the warranty is attached to your Vehicle Identification Number.
|
||||
As such, the warranty is transferable with vehicle ownership, which means you never have
|
||||
to worry about any paperwork or fees involved. Please note, that the used parts warranty
|
||||
is not transferable.
|
||||
</p>
|
||||
</div>
|
||||
<div className="my-5">
|
||||
<div className="mb-1 flex items-center gap-2 font-medium">
|
||||
<FlagIcon className="size-4 text-primary" />
|
||||
Nationwide Coverage
|
||||
</div>
|
||||
<p>
|
||||
Whether you're in California, Chicago, New York, Florida, or anywhere in between, you
|
||||
are covered with a nationwide warranty. This warranty covers you anywhere in the
|
||||
continental U.S.
|
||||
</p>
|
||||
</div>
|
||||
<div className="my-5">
|
||||
<div className="mb-1 flex items-center gap-2 font-medium">
|
||||
<ArrowPathIcon className="size-4 text-primary" />
|
||||
Instant Replacement
|
||||
</div>
|
||||
<p>
|
||||
With instant replacement, your replacement transmission will be sent out as soon as you
|
||||
submit your claim. This way you can spend less time waiting and more time doing whatever
|
||||
needs to be done.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="my-5">
|
||||
<div className="mb-1 flex items-center gap-2 font-medium">
|
||||
<CurrencyDollarIcon className="size-4 text-primary" />
|
||||
Paid Parts & Labor
|
||||
</div>
|
||||
<p>
|
||||
When you have your work performed in a certified shop, your {SITE_NAME} warranty will pay
|
||||
for parts and labor at $50 an hour, which is the Mitchell labor reimbursement rate.
|
||||
</p>
|
||||
</div>
|
||||
</DisclosureSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarrantyPolicy;
|
@ -46,3 +46,15 @@ export const MODEL_FILTER_ID = 'filter.p.m.custom.make_model_composite';
|
||||
export const YEAR_FILTER_ID = 'filter.p.m.custom.make_model_year_composite';
|
||||
export const PRODUCT_METAFIELD_PREFIX = 'filter.p.m';
|
||||
export const VARIANT_METAFIELD_PREFIX = 'filter.v.m';
|
||||
|
||||
export const CONDITIONS = {
|
||||
Used: 'Used',
|
||||
Remanufactured: 'Remanufactured'
|
||||
};
|
||||
|
||||
export const DELIVERY_OPTION_KEY = 'delivery';
|
||||
|
||||
export const ADD_ON_PRODUCT_TYPES = {
|
||||
addOn: 'Add On',
|
||||
coreCharge: 'Core Charge'
|
||||
};
|
||||
|
@ -1,9 +1,13 @@
|
||||
import productFragment from './product';
|
||||
import imageFragment from './image';
|
||||
|
||||
const cartFragment = /* GraphQL */ `
|
||||
fragment cart on Cart {
|
||||
id
|
||||
checkoutUrl
|
||||
attributes {
|
||||
key
|
||||
value
|
||||
}
|
||||
cost {
|
||||
subtotalAmount {
|
||||
amount
|
||||
@ -38,11 +42,22 @@ const cartFragment = /* GraphQL */ `
|
||||
value
|
||||
}
|
||||
product {
|
||||
...product
|
||||
featuredImage {
|
||||
...image
|
||||
}
|
||||
handle
|
||||
title
|
||||
productType
|
||||
}
|
||||
coreVariantId: metafield(key: "coreVariant", namespace: "custom") {
|
||||
value
|
||||
}
|
||||
addOnQuantity: metafield(namespace: "custom", key: "add_on_quantity") {
|
||||
value
|
||||
}
|
||||
addOnProductId: metafield(namespace: "custom", key: "add_on") {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,7 +65,7 @@ const cartFragment = /* GraphQL */ `
|
||||
}
|
||||
totalQuantity
|
||||
}
|
||||
${productFragment}
|
||||
${imageFragment}
|
||||
`;
|
||||
|
||||
export default cartFragment;
|
||||
|
@ -64,6 +64,12 @@ const productFragment = /* GraphQL */ `
|
||||
condition: metafield(namespace: "custom", key: "condition") {
|
||||
value
|
||||
}
|
||||
addOnQuantity: metafield(namespace: "custom", key: "add_on_quantity") {
|
||||
value
|
||||
}
|
||||
addOnProductId: metafield(namespace: "custom", key: "add_on") {
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,6 +82,21 @@ const productFragment = /* GraphQL */ `
|
||||
fuelType: metafield(namespace: "custom", key: "fuel") {
|
||||
value
|
||||
}
|
||||
transmissionType: metafield(namespace: "custom", key: "transmission_type") {
|
||||
value
|
||||
}
|
||||
transmissionSpeeds: metafield(namespace: "custom", key: "transmission_speeds") {
|
||||
value
|
||||
}
|
||||
driveType: metafield(namespace: "custom", key: "drive_type") {
|
||||
value
|
||||
}
|
||||
transmissionCode: metafield(namespace: "custom", key: "transmission_code") {
|
||||
value
|
||||
}
|
||||
transmissionTag: metafield(namespace: "custom", key: "transmission_tag") {
|
||||
value
|
||||
}
|
||||
images(first: 20) {
|
||||
edges {
|
||||
node {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
ADD_ON_PRODUCT_TYPES,
|
||||
AVAILABILITY_FILTER_ID,
|
||||
HIDDEN_PRODUCT_TAG,
|
||||
MAKE_FILTER_ID,
|
||||
@ -41,7 +42,6 @@ import {
|
||||
Address,
|
||||
Cart,
|
||||
CartAttributeInput,
|
||||
CartItem,
|
||||
Collection,
|
||||
Connection,
|
||||
Customer,
|
||||
@ -84,6 +84,7 @@ import {
|
||||
ShopifyRemoveFromCartOperation,
|
||||
ShopifySetCartAttributesOperation,
|
||||
ShopifyUpdateCartOperation,
|
||||
TransmissionType,
|
||||
ShopifyCustomer,
|
||||
ShopifyOrder,
|
||||
ShopifyAddress,
|
||||
@ -260,7 +261,7 @@ const reshapeCart = (cart: ShopifyCart): Cart => {
|
||||
...lineItem,
|
||||
merchandise: {
|
||||
...lineItem.merchandise,
|
||||
product: reshapeProduct(lineItem.merchandise.product)
|
||||
product: lineItem.merchandise.product
|
||||
}
|
||||
}))
|
||||
};
|
||||
@ -273,6 +274,7 @@ const reshapeCollection = (collection: ShopifyCollection): Collection | undefine
|
||||
|
||||
return {
|
||||
...collection,
|
||||
helpfulLinks: parseMetaFieldValue<string[]>(collection.helpfulLinks),
|
||||
path: `/search/${collection.handle}`
|
||||
};
|
||||
};
|
||||
@ -374,18 +376,29 @@ const reshapeImages = (images: Connection<Image>, productTitle: string) => {
|
||||
};
|
||||
|
||||
const reshapeVariants = (variants: ShopifyProductVariant[]): ProductVariant[] => {
|
||||
return variants.map((variant) => ({
|
||||
return variants.map(({ addOnProductId, addOnQuantity, ...variant }) => ({
|
||||
...variant,
|
||||
waiverAvailable: parseMetaFieldValue<boolean>(variant.waiverAvailable),
|
||||
coreVariantId: variant.coreVariantId?.value || null,
|
||||
coreCharge: parseMetaFieldValue<Money>(variant.coreCharge),
|
||||
mileage: variant.mileage?.value ?? null,
|
||||
estimatedDelivery: variant.estimatedDelivery?.value || null,
|
||||
condition: variant.condition?.value || null
|
||||
condition: variant.condition?.value || null,
|
||||
...(addOnProductId
|
||||
? {
|
||||
addOnProduct: {
|
||||
id: addOnProductId.value,
|
||||
quantity: addOnQuantity?.value ? Number(addOnQuantity.value) : 1
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}));
|
||||
};
|
||||
|
||||
const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => {
|
||||
const reshapeProduct = (
|
||||
product: ShopifyProduct,
|
||||
filterHiddenProducts: boolean = true
|
||||
): Product | undefined => {
|
||||
if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) {
|
||||
return undefined;
|
||||
}
|
||||
@ -393,6 +406,13 @@ const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean =
|
||||
const { images, variants, ...rest } = product;
|
||||
return {
|
||||
...rest,
|
||||
transmissionCode: parseMetaFieldValue<string[]>(product.transmissionCode),
|
||||
transmissionSpeeds: parseMetaFieldValue<number[]>(product.transmissionSpeeds),
|
||||
transmissionTag: parseMetaFieldValue<string[]>(product.transmissionTag),
|
||||
driveType: parseMetaFieldValue<string[]>(product.driveType),
|
||||
transmissionType: product.transmissionType
|
||||
? (product.transmissionType.value as TransmissionType)
|
||||
: null,
|
||||
engineCylinders: parseMetaFieldValue<number[]>(product.engineCylinders),
|
||||
fuelType: product.fuelType?.value || null,
|
||||
images: reshapeImages(images, product.title),
|
||||
@ -633,20 +653,34 @@ export async function getCart(cartId: string): Promise<Cart | undefined> {
|
||||
const cart = reshapeCart(res.body.data.cart);
|
||||
|
||||
// attach core charge as an additional attribute of a cart line, and remove the core charge line from cart
|
||||
const extendedCartLines = cart?.lines.reduce((lines, item) => {
|
||||
const coreVariantId = item.merchandise.coreVariantId?.value;
|
||||
if (coreVariantId) {
|
||||
const relatedCoreCharge = cart.lines.find((line) => line.merchandise.id === coreVariantId);
|
||||
return lines.concat([
|
||||
{
|
||||
...item,
|
||||
coreCharge: relatedCoreCharge
|
||||
}
|
||||
]);
|
||||
}
|
||||
const extendedCartLines = cart?.lines
|
||||
.map((item) => {
|
||||
const coreVariantId = item.merchandise.coreVariantId?.value;
|
||||
const addOnProductId = item.merchandise.addOnProductId;
|
||||
const _item = { ...item };
|
||||
|
||||
return lines;
|
||||
}, [] as CartItem[]);
|
||||
if (coreVariantId) {
|
||||
const relatedCoreCharge = cart.lines.find((line) => line.merchandise.id === coreVariantId);
|
||||
_item.coreCharge = relatedCoreCharge;
|
||||
}
|
||||
|
||||
if (addOnProductId) {
|
||||
const relatedAddOnProduct = cart.lines.find(
|
||||
(line) => line.merchandise.id === addOnProductId.value
|
||||
);
|
||||
_item.addOnProduct = relatedAddOnProduct
|
||||
? {
|
||||
...relatedAddOnProduct,
|
||||
quantity: item.merchandise.addOnQuantity
|
||||
? Number(item.merchandise.addOnQuantity.value)
|
||||
: 1
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
return _item;
|
||||
})
|
||||
// core charge shouldn't present as a dedicated product as it's tightly coupled with the product
|
||||
.filter((item) => item.merchandise.product.productType !== ADD_ON_PRODUCT_TYPES.coreCharge);
|
||||
|
||||
const totalQuantity = extendedCartLines.reduce((sum, line) => sum + line.quantity, 0);
|
||||
|
||||
@ -730,7 +764,8 @@ export async function getCollections(): Promise<Collection[]> {
|
||||
description: 'All products'
|
||||
},
|
||||
path: '/search',
|
||||
updatedAt: new Date().toISOString()
|
||||
updatedAt: new Date().toISOString(),
|
||||
helpfulLinks: null
|
||||
},
|
||||
// Filter out the `hidden` collections.
|
||||
// Collections that start with `hidden-*` need to be hidden on the search page.
|
||||
|
@ -9,6 +9,9 @@ const collectionFragment = /* GraphQL */ `
|
||||
seo {
|
||||
...seo
|
||||
}
|
||||
helpfulLinks: metafield(namespace: "custom", key: "helpful_links") {
|
||||
value
|
||||
}
|
||||
updatedAt
|
||||
}
|
||||
${seoFragment}
|
||||
|
@ -25,14 +25,24 @@ export type CartItem = {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
product: Product;
|
||||
product: {
|
||||
id: string;
|
||||
handle: string;
|
||||
title: string;
|
||||
featuredImage: Image;
|
||||
productType: string;
|
||||
};
|
||||
coreVariantId: { value: string } | null;
|
||||
addOnQuantity: { value: string } | null;
|
||||
addOnProductId: { value: string } | null;
|
||||
};
|
||||
coreCharge?: CartItem;
|
||||
addOnProduct?: CartItem & { quantity: number };
|
||||
};
|
||||
|
||||
export type Collection = ShopifyCollection & {
|
||||
export type Collection = Omit<ShopifyCollection, 'helpfulLinks'> & {
|
||||
path: string;
|
||||
helpfulLinks: string[] | null;
|
||||
};
|
||||
|
||||
export type Customer = {
|
||||
@ -379,14 +389,29 @@ export type Metaobject = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export type TransmissionType = 'Automatic' | 'Manual';
|
||||
|
||||
export type Product = Omit<
|
||||
ShopifyProduct,
|
||||
'variants' | 'images' | 'fuelType' | 'engineCylinders'
|
||||
| 'variants'
|
||||
| 'images'
|
||||
| 'fuelType'
|
||||
| 'engineCylinders'
|
||||
| 'driveType'
|
||||
| 'transmissionType'
|
||||
| 'transmissionSpeeds'
|
||||
| 'transmissionCode'
|
||||
| 'transmissionTag'
|
||||
> & {
|
||||
variants: ProductVariant[];
|
||||
images: Image[];
|
||||
fuelType: string | null;
|
||||
engineCylinders: number[] | null;
|
||||
driveType: string[] | null;
|
||||
transmissionType: TransmissionType | null;
|
||||
transmissionSpeeds: number[] | null;
|
||||
transmissionCode: string[] | null;
|
||||
transmissionTag: string[] | null;
|
||||
};
|
||||
|
||||
export type ProductOption = {
|
||||
@ -414,6 +439,10 @@ export type ProductVariant = {
|
||||
condition: string | null;
|
||||
engineCylinders: string | null;
|
||||
fuelType: string | null;
|
||||
addOnProduct?: {
|
||||
quantity: number;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyCartProductVariant = {
|
||||
@ -432,7 +461,13 @@ export type CartProductVariant = Omit<ShopifyCartProductVariant, 'coreVariantId'
|
||||
|
||||
export type ShopifyProductVariant = Omit<
|
||||
ProductVariant,
|
||||
'coreCharge' | 'waiverAvailable' | 'coreVariantId' | 'mileage' | 'estimatedDelivery' | 'condition'
|
||||
| 'coreCharge'
|
||||
| 'waiverAvailable'
|
||||
| 'coreVariantId'
|
||||
| 'mileage'
|
||||
| 'estimatedDelivery'
|
||||
| 'condition'
|
||||
| 'addOnProduct'
|
||||
> & {
|
||||
waiverAvailable: { value: string };
|
||||
coreVariantId: { value: string } | null;
|
||||
@ -440,6 +475,8 @@ export type ShopifyProductVariant = Omit<
|
||||
mileage: { value: number } | null;
|
||||
estimatedDelivery: { value: string } | null;
|
||||
condition: { value: string } | null;
|
||||
addOnProductId: { value: string } | null;
|
||||
addOnQuantity: { value: string } | null;
|
||||
};
|
||||
|
||||
export type SEO = {
|
||||
@ -450,6 +487,7 @@ export type SEO = {
|
||||
export type ShopifyCart = {
|
||||
id: string;
|
||||
checkoutUrl: string;
|
||||
attributes: { key: string; value: string }[];
|
||||
cost: {
|
||||
subtotalAmount: Money;
|
||||
totalAmount: Money;
|
||||
@ -465,6 +503,7 @@ export type ShopifyCollection = {
|
||||
description: string;
|
||||
seo: SEO;
|
||||
updatedAt: string;
|
||||
helpfulLinks: { value: string } | null;
|
||||
};
|
||||
|
||||
export type ShopifyProduct = {
|
||||
@ -493,6 +532,11 @@ export type ShopifyProduct = {
|
||||
};
|
||||
engineCylinders: { value: string } | null;
|
||||
fuelType: { value: string } | null;
|
||||
transmissionType: { value: string } | null;
|
||||
transmissionTag: { value: string } | null;
|
||||
transmissionCode: { value: string } | null;
|
||||
driveType: { value: string } | null;
|
||||
transmissionSpeeds: { value: string } | null;
|
||||
};
|
||||
|
||||
export type ShopifyCartOperation = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user