mirror of
https://github.com/vercel/commerce.git
synced 2025-05-13 05:07:51 +00:00
feat: add more details to product tile
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
4edc2bb580
commit
8f82f6299e
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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 {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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 = {
|
||||||
|
@ -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
BIN
public/icons/cylinder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 509 B |
BIN
public/icons/fuel.png
Normal file
BIN
public/icons/fuel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 584 B |
Loading…
x
Reference in New Issue
Block a user