feat: add more details to product tile

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-06-12 19:14:57 +07:00
parent 4edc2bb580
commit 8f82f6299e
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
10 changed files with 77 additions and 48 deletions

View File

@ -132,11 +132,7 @@ async function RelatedProducts({ id }: { id: string }) {
> >
<GridTileImage <GridTileImage
alt={product.title} alt={product.title}
label={{ product={product}
title: product.title,
amount: product.priceRange.maxVariantPrice.amount,
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url} src={product.featuredImage?.url}
fill fill
sizes="(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw" sizes="(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw"

View File

@ -23,11 +23,7 @@ function ThreeItemGridItem({
} }
priority={priority} priority={priority}
alt={item.title} alt={item.title}
label={{ product={item}
title: item.title as string,
amount: item.priceRange.maxVariantPrice.amount,
currencyCode: item.priceRange.maxVariantPrice.currencyCode
}}
href={`/product/${item.handle}`} href={`/product/${item.handle}`}
/> />
</div> </div>

View File

@ -1,25 +1,23 @@
import { ArrowRightIcon, PhotoIcon } from '@heroicons/react/24/solid'; import { ArrowRightIcon, PhotoIcon } from '@heroicons/react/24/solid';
import clsx from 'clsx'; import clsx from 'clsx';
import Price from 'components/price'; import Price from 'components/price';
import { Product } from 'lib/shopify/types';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
export function GridTileImage({ export function GridTileImage({
active, active,
label, product,
href, href,
place = 'grid',
...props ...props
}: { }: {
active?: boolean; active?: boolean;
label?: { product: Product;
title: string;
amount: string;
currencyCode: string;
};
place?: 'grid' | 'gallery';
href: string; href: string;
} & React.ComponentProps<typeof Image>) { } & React.ComponentProps<typeof Image>) {
const metafieldKeys = ['engineCylinders', 'fuelType'] as Partial<keyof Product>[];
const shouldShowDescription = metafieldKeys.some((key) => product[key]);
return ( return (
<div className="flex h-full flex-col rounded-b border bg-white"> <div className="flex h-full flex-col rounded-b border bg-white">
<div className="grow"> <div className="grow">
@ -43,30 +41,57 @@ export function GridTileImage({
)} )}
</div> </div>
</div> </div>
<div className="flex flex-col gap-2 divide-y px-4"> <h3 className="mt-4 px-4 pb-2 text-sm font-semibold leading-6 text-gray-800">
{label && ( {product.title}
<h3 className="mt-4 text-sm font-semibold leading-6 text-gray-800">{label.title}</h3> </h3>
)} </div>
{label && ( <div className="px-4">
<div className="flex w-full justify-end py-2"> {shouldShowDescription && (
<Price <div className="flex items-center justify-center gap-x-7 border-t py-3">
className="text-lg font-medium text-gray-900" {product.engineCylinders?.length ? (
amount={label.amount} <div className="flex flex-col items-center gap-2">
currencyCode={label.currencyCode} <Image
/> src="/icons/cylinder.png"
</div> alt="Cylinder icon"
)} width={16}
height={16}
className="size-4"
sizes="16px"
/>
<span className="text-xs tracking-wide">{`${product.engineCylinders[0]} Cylinder`}</span>
</div>
) : null}
{product.fuelType ? (
<div className="flex flex-col items-center gap-2">
<Image
src="/icons/fuel.png"
alt="Fuel icon"
width={16}
height={16}
className="size-4"
sizes="16px"
/>
<span className="text-xs tracking-wide">{product.fuelType}</span>
</div>
) : 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> </div>
</div> </div>
{place === 'grid' && (
<Link <Link
href={href} href={href}
className="flex items-center justify-center gap-3 rounded-b bg-dark py-3 text-white" className="flex items-center justify-center gap-3 rounded-b bg-dark py-3 text-white"
> >
<span className="text-sm font-medium tracking-wide">More details</span> <span className="text-sm font-medium tracking-wide">More details</span>
<ArrowRightIcon className="size-4" /> <ArrowRightIcon className="size-4" />
</Link> </Link>
)}
</div> </div>
); );
} }

View File

@ -70,11 +70,7 @@ const ProductsList = ({
> >
<GridTileImage <GridTileImage
alt={product.title} alt={product.title}
label={{ product={product}
title: product.title,
amount: product.priceRange.maxVariantPrice.amount,
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url} src={product.featuredImage?.url}
fill fill
sizes="(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw" sizes="(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"

View File

@ -70,6 +70,12 @@ const productFragment = /* GraphQL */ `
featuredImage { featuredImage {
...image ...image
} }
engineCylinders: metafield(namespace: "custom", key: "engine_cylinders") {
value
}
fuelType: metafield(namespace: "custom", key: "fuel") {
value
}
images(first: 20) { images(first: 20) {
edges { edges {
node { node {

View File

@ -294,6 +294,8 @@ const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean =
const { images, variants, ...rest } = product; const { images, variants, ...rest } = product;
return { return {
...rest, ...rest,
engineCylinders: parseMetaFieldValue<number[]>(product.engineCylinders),
fuelType: product.fuelType?.value || null,
images: reshapeImages(images, product.title), images: reshapeImages(images, product.title),
variants: reshapeVariants(removeEdgesAndNodes(variants)) variants: reshapeVariants(removeEdgesAndNodes(variants))
}; };
@ -305,7 +307,6 @@ const reshapeProducts = (products: ShopifyProduct[]) => {
for (const product of products) { for (const product of products) {
if (product) { if (product) {
const reshapedProduct = reshapeProduct(product); const reshapedProduct = reshapeProduct(product);
if (reshapedProduct) { if (reshapedProduct) {
reshapedProducts.push(reshapedProduct); reshapedProducts.push(reshapedProduct);
} }

View File

@ -100,9 +100,14 @@ export type Metaobject = {
[key: string]: string; [key: string]: string;
}; };
export type Product = Omit<ShopifyProduct, 'variants' | 'images'> & { export type Product = Omit<
ShopifyProduct,
'variants' | 'images' | 'fuelType' | 'engineCylinders'
> & {
variants: ProductVariant[]; variants: ProductVariant[];
images: Image[]; images: Image[];
fuelType: string | null;
engineCylinders: number[] | null;
}; };
export type ProductOption = { export type ProductOption = {
@ -128,6 +133,8 @@ export type ProductVariant = {
mileage: number | null; mileage: number | null;
estimatedDelivery: string | null; estimatedDelivery: string | null;
condition: string | null; condition: string | null;
engineCylinders: string | null;
fuelType: string | null;
}; };
export type ShopifyCartProductVariant = { export type ShopifyCartProductVariant = {
@ -205,6 +212,8 @@ export type ShopifyProduct = {
handle: string; handle: string;
}[]; }[];
}; };
engineCylinders: { value: string } | null;
fuelType: { value: string } | null;
}; };
export type ShopifyCartOperation = { export type ShopifyCartOperation = {

View File

@ -51,7 +51,7 @@ export function normalizeUrl(domain: string, url: string) {
export const parseMetaFieldValue = <T>(field: { value: string } | null): T | null => { export const parseMetaFieldValue = <T>(field: { value: string } | null): T | null => {
try { try {
return JSON.parse(field?.value || '{}'); return field?.value ? JSON.parse(field.value) : null;
} catch (error) { } catch (error) {
return null; return null;
} }

BIN
public/icons/cylinder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

BIN
public/icons/fuel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B