mirror of
https://github.com/vercel/commerce.git
synced 2025-05-19 07:56:59 +00:00
wip: Product detail view
This commit is contained in:
parent
523db4e1d4
commit
097cb83568
@ -39,7 +39,7 @@ export default async function HomePage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-screen overflow-scroll">
|
||||
<div>
|
||||
<Navbar cart={cart} locale={locale} />
|
||||
<div className="pt-48">
|
||||
<ThreeItemGrid lang={locale} />
|
||||
|
43
app/[locale]/product/[handle]/layout.tsx
Normal file
43
app/[locale]/product/[handle]/layout.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import Footer from 'components/layout/footer';
|
||||
import { SupportedLocale } from 'components/layout/navbar/language-control';
|
||||
|
||||
import Navbar from 'components/layout/navbar';
|
||||
import { getCart } from 'lib/shopify';
|
||||
import { cookies } from 'next/headers';
|
||||
import { ReactNode, Suspense } from 'react';
|
||||
|
||||
export const runtime = 'edge';
|
||||
const { SITE_NAME } = process.env;
|
||||
|
||||
export const metadata = {
|
||||
title: SITE_NAME,
|
||||
description: SITE_NAME,
|
||||
openGraph: {
|
||||
type: 'website'
|
||||
}
|
||||
};
|
||||
|
||||
export default async function ProductLayout({
|
||||
params: { locale },
|
||||
children
|
||||
}: {
|
||||
params: { locale?: SupportedLocale };
|
||||
children: ReactNode[] | ReactNode | string;
|
||||
}) {
|
||||
const cartId = cookies().get('cartId')?.value;
|
||||
let cart;
|
||||
|
||||
if (cartId) {
|
||||
cart = await getCart(cartId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Navbar cart={cart} locale={locale} />
|
||||
{children}
|
||||
<Suspense>
|
||||
<Footer cart={cart} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -3,7 +3,6 @@ import { notFound } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { GridTileImage } from 'components/grid/tile';
|
||||
import Footer from 'components/layout/footer';
|
||||
import { Gallery } from 'components/product/gallery';
|
||||
import { ProductDescription } from 'components/product/product-description';
|
||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
||||
@ -81,8 +80,8 @@ export default async function ProductPage({ params }: { params: { handle: string
|
||||
__html: JSON.stringify(productJsonLd)
|
||||
}}
|
||||
/>
|
||||
<div className="mx-auto max-w-screen-xl px-4">
|
||||
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 dark:border-neutral-800 dark:bg-black md:p-12 lg:flex-row">
|
||||
<div className="mx-auto max-w-screen-xl px-4 py-24">
|
||||
<div className="flex flex-col p-8 md:p-12 lg:flex-row lg:space-x-6">
|
||||
<div className="h-full w-full basis-full lg:basis-4/6">
|
||||
<Gallery
|
||||
images={product.images.map((image: Image) => ({
|
||||
@ -100,9 +99,6 @@ export default async function ProductPage({ params }: { params: { handle: string
|
||||
<RelatedProducts id={product.id} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense>
|
||||
<Footer />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -110,18 +106,20 @@ export default async function ProductPage({ params }: { params: { handle: string
|
||||
async function RelatedProducts({ id }: { id: string }) {
|
||||
const relatedProducts = await getProductRecommendations({ productId: id });
|
||||
|
||||
console.debug({ relatedProducts });
|
||||
|
||||
if (!relatedProducts.length) return null;
|
||||
|
||||
return (
|
||||
<div className="py-8">
|
||||
<h2 className="mb-4 text-2xl font-bold">Related Products</h2>
|
||||
<div className="py-24">
|
||||
<h2 className="font-multilingual mb-4 text-2xl">Related Products</h2>
|
||||
<ul className="flex w-full gap-4 overflow-x-auto pt-1">
|
||||
{relatedProducts.map((product) => (
|
||||
<li
|
||||
key={product.handle}
|
||||
className="aspect-square w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5"
|
||||
>
|
||||
<Link className="relative h-full w-full" href={`/product/${product.handle}`}>
|
||||
<Link className="relative block h-full w-full" href={`/product/${product.handle}`}>
|
||||
<GridTileImage
|
||||
alt={product.title}
|
||||
label={{
|
||||
|
@ -31,7 +31,7 @@ export default async function ProductPage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-screen overflow-scroll">
|
||||
<div>
|
||||
<Navbar cart={cart} locale={locale} />
|
||||
<div className="py-24 md:py-48">
|
||||
<ThreeItemGrid lang={locale} />
|
||||
|
@ -52,7 +52,7 @@ export function AddToCart({
|
||||
});
|
||||
}}
|
||||
className={clsx(
|
||||
'relative flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90',
|
||||
'relative flex w-full items-center justify-center rounded-md bg-white/20 p-4 font-sans tracking-wide text-white hover:opacity-90',
|
||||
{
|
||||
'cursor-not-allowed opacity-60 hover:opacity-60': !availableForSale || !selectedVariantId,
|
||||
'cursor-not-allowed': isPending
|
||||
|
@ -3,7 +3,7 @@ import clsx from 'clsx';
|
||||
|
||||
export default function CloseCart({ className }: { className?: string }) {
|
||||
return (
|
||||
<div className="relative flex h-11 w-11 items-center justify-center rounded-md border border-neutral-200 text-black transition-colors dark:border-neutral-700 dark:text-white">
|
||||
<div className="relative flex h-11 w-11 items-center justify-center rounded-md border border-white/20 text-white transition-colors">
|
||||
<XMarkIcon className={clsx('h-6 transition-all ease-in-out hover:scale-110 ', className)} />
|
||||
</div>
|
||||
);
|
||||
|
@ -64,7 +64,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="fixed bottom-0 right-0 top-0 flex h-full w-full flex-col border-l border-neutral-200 bg-white/80 p-6 text-black backdrop-blur-xl dark:border-neutral-700 dark:bg-black/80 dark:text-white md:w-[390px]">
|
||||
<Dialog.Panel className="fixed bottom-0 right-0 top-0 flex h-full w-full flex-col border-l border-white/20 bg-dark p-6 text-white backdrop-blur-xl md:w-[390px]">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-lg font-semibold">My Cart</p>
|
||||
|
||||
@ -96,10 +96,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<li
|
||||
key={i}
|
||||
className="flex w-full flex-col border-b border-neutral-300 dark:border-neutral-700"
|
||||
>
|
||||
<li key={i} className="flex w-full flex-col border-b border-white/20">
|
||||
<div className="relative flex w-full flex-row justify-between px-1 py-4">
|
||||
<div className="absolute z-40 -mt-2 ml-[55px]">
|
||||
<DeleteItemButton item={item} />
|
||||
@ -109,7 +106,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
onClick={closeCart}
|
||||
className="z-30 flex flex-row space-x-4"
|
||||
>
|
||||
<div className="relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
|
||||
<div className="relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-white/20 bg-white/40">
|
||||
<Image
|
||||
className="h-full w-full object-cover"
|
||||
width={64}
|
||||
@ -127,9 +124,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
{item.merchandise.product.title}
|
||||
</span>
|
||||
{item.merchandise.title !== DEFAULT_OPTION ? (
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{item.merchandise.title}
|
||||
</p>
|
||||
<p className="text-sm text-white">{item.merchandise.title}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
@ -139,7 +134,7 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
amount={item.cost.totalAmount.amount}
|
||||
currencyCode={item.cost.totalAmount.currencyCode}
|
||||
/>
|
||||
<div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
|
||||
<div className="ml-auto flex h-9 flex-row items-center rounded-full border border-white/20">
|
||||
<EditItemQuantityButton item={item} type="minus" />
|
||||
<p className="w-6 text-center">
|
||||
<span className="w-full text-sm">{item.quantity}</span>
|
||||
@ -153,22 +148,22 @@ export default function CartModal({ cart }: { cart: Cart | undefined }) {
|
||||
})}
|
||||
</ul>
|
||||
<div className="py-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 dark:border-neutral-700">
|
||||
<div className="mb-3 flex items-center justify-between border-b border-white/20 pb-1">
|
||||
<p>Taxes</p>
|
||||
<Price
|
||||
className="text-right text-base text-black dark:text-white"
|
||||
className="text-right text-base text-white"
|
||||
amount={cart.cost.totalTaxAmount.amount}
|
||||
currencyCode={cart.cost.totalTaxAmount.currencyCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
|
||||
<div className="mb-3 flex items-center justify-between border-b border-white/20 pb-1 pt-1">
|
||||
<p>Shipping</p>
|
||||
<p className="text-right">Calculated at checkout</p>
|
||||
<p className="text-right text-white/50">Calculated at checkout</p>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center justify-between border-b border-neutral-200 pb-1 pt-1 dark:border-neutral-700">
|
||||
<div className="mb-3 flex items-center justify-between border-b border-white/20 pb-1 pt-1">
|
||||
<p>Total</p>
|
||||
<Price
|
||||
className="text-right text-base text-black dark:text-white"
|
||||
className="text-right text-base text-white"
|
||||
amount={cart.cost.totalAmount.amount}
|
||||
currencyCode={cart.cost.totalAmount.currencyCode}
|
||||
/>
|
||||
|
@ -19,7 +19,7 @@ export default function OpenCart({
|
||||
/>
|
||||
|
||||
{quantity ? (
|
||||
<div className="absolute right-[23%] top-[85%] -mr-2 -mt-2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform text-[12px] font-medium text-white">
|
||||
<div className="absolute right-[23%] top-[85%] -mr-2 -mt-2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 transform font-sans text-[12px] font-medium text-white">
|
||||
{quantity}
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -29,7 +29,9 @@ export default async function Footer({ cart }: { cart?: Cart }) {
|
||||
<div className="flex flex-col space-y-24">
|
||||
<NewsletterFooter />
|
||||
<div className="hidden flex-row items-end space-x-12 pt-24 md:flex">
|
||||
<KanjiLogo className="h-64" />
|
||||
<Link href="/" className="transition-opacity duration-150 hover:opacity-90">
|
||||
<KanjiLogo className="h-64" />
|
||||
</Link>
|
||||
<div className="flex flex-row items-end space-x-6">
|
||||
<div className="flex flex-col items-start space-y-2">
|
||||
<p className="font-japan text-3xl font-extralight">杉の森酒造</p>
|
||||
@ -45,7 +47,9 @@ export default async function Footer({ cart }: { cart?: Cart }) {
|
||||
</div>
|
||||
<div className="w-full md:w-[40%]">
|
||||
<div className="flex w-full flex-row items-end space-x-12 pt-24 md:hidden">
|
||||
<KanjiLogo className="h-64" />
|
||||
<Link href="/" className="transition-opacity duration-150 hover:opacity-90">
|
||||
<KanjiLogo className="h-64" />
|
||||
</Link>
|
||||
<div className="flex grow flex-col items-end space-y-6">
|
||||
<div className="flex flex-col items-start space-y-2">
|
||||
<p className="font-japan text-3xl font-extralight">杉の森酒造</p>
|
||||
|
@ -32,7 +32,7 @@ export default function Navbar({ cart, locale }: { cart?: Cart; locale?: Support
|
||||
>
|
||||
<div className="mx-auto flex max-w-screen-xl flex-row items-start justify-between">
|
||||
<div className="px-4 py-2">
|
||||
<Link href="/">
|
||||
<Link href="/" className="transition-opacity duration-150 hover:opacity-90">
|
||||
<LogoNamemark className={clsx('w-[260px]', 'fill-current')} />
|
||||
</Link>
|
||||
</div>
|
||||
@ -51,7 +51,7 @@ export default function Navbar({ cart, locale }: { cart?: Cart; locale?: Support
|
||||
className={clsx('mx-auto flex max-w-screen-xl flex-row items-start justify-between px-6')}
|
||||
>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<Link href="/" className="transition-opacity duration-150 hover:opacity-90">
|
||||
<LogoNamemark
|
||||
className={clsx(
|
||||
inView ? 'w-[260px] md:w-[600px]' : 'w-[260px] md:w-[260px]',
|
||||
|
@ -30,7 +30,7 @@ export default function NewsletterSignup() {
|
||||
className={clsx(
|
||||
'w-full min-w-0 appearance-none',
|
||||
'bg-white outline-none',
|
||||
'px-4 py-2 focus:outline-none',
|
||||
'px-4 py-3 focus:outline-none',
|
||||
'focus:ring-2 focus:ring-inset focus:ring-emerald-300 focus:ring-offset-0',
|
||||
'text-gray-900 placeholder-gray-400'
|
||||
)}
|
||||
@ -40,7 +40,7 @@ export default function NewsletterSignup() {
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
'px-4 py-2',
|
||||
'px-4 py-3',
|
||||
'transition-colors duration-150',
|
||||
'flex w-full items-center justify-center',
|
||||
'border border-white/30 hover:border-white',
|
||||
|
@ -28,7 +28,7 @@ export default function NewsletterSignup() {
|
||||
className={clsx(
|
||||
'w-full min-w-0 appearance-none',
|
||||
'bg-white outline-none',
|
||||
'px-4 py-2 focus:outline-none',
|
||||
'px-4 py-3 focus:outline-none',
|
||||
'focus:ring-2 focus:ring-inset focus:ring-emerald-300 focus:ring-offset-0',
|
||||
'text-gray-900 placeholder-gray-400'
|
||||
)}
|
||||
@ -38,7 +38,7 @@ export default function NewsletterSignup() {
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
'px-4 py-2',
|
||||
'px-4 py-3',
|
||||
'transition-colors duration-150',
|
||||
'font-multilingual flex w-full items-center justify-center font-extralight',
|
||||
'border border-white/30 hover:border-white',
|
||||
|
@ -24,14 +24,14 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
|
||||
const previousUrl = createUrl(pathname, previousSearchParams);
|
||||
|
||||
const buttonClassName =
|
||||
'h-full px-6 transition-all ease-in-out hover:scale-110 hover:text-black dark:hover:text-white flex items-center justify-center';
|
||||
'h-full px-6 transition-all ease-in-out hover:scale-110 hover:text-white flex items-center justify-center';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden">
|
||||
{images[imageIndex] && (
|
||||
<Image
|
||||
className="h-full w-full object-contain"
|
||||
className="h-full w-full object-cover"
|
||||
fill
|
||||
sizes="(min-width: 1024px) 66vw, 100vw"
|
||||
alt={images[imageIndex]?.altText as string}
|
||||
@ -41,8 +41,8 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
|
||||
)}
|
||||
|
||||
{images.length > 1 ? (
|
||||
<div className="absolute bottom-[15%] flex w-full justify-center">
|
||||
<div className="mx-auto flex h-11 items-center rounded-full border border-white bg-neutral-50/80 text-neutral-500 backdrop-blur dark:border-black dark:bg-neutral-900/80">
|
||||
<div className="absolute bottom-[8%] flex w-full justify-center">
|
||||
<div className="mx-auto flex h-11 items-center rounded-full border border-white/40 bg-dark/40 text-white/70 backdrop-blur">
|
||||
<Link
|
||||
aria-label="Previous product image"
|
||||
href={previousUrl}
|
||||
@ -51,7 +51,7 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
|
||||
>
|
||||
<ArrowLeftIcon className="h-5" />
|
||||
</Link>
|
||||
<div className="mx-1 h-6 w-px bg-neutral-500"></div>
|
||||
<div className="mx-1 h-6 w-px bg-white/40"></div>
|
||||
<Link
|
||||
aria-label="Next product image"
|
||||
href={nextUrl}
|
||||
@ -74,18 +74,19 @@ export function Gallery({ images }: { images: { src: string; altText: string }[]
|
||||
imageSearchParams.set('image', index.toString());
|
||||
|
||||
return (
|
||||
<li key={image.src} className="h-auto w-20">
|
||||
<li key={image.src} className="aspect-square h-auto w-20">
|
||||
<Link
|
||||
aria-label="Enlarge product image"
|
||||
href={createUrl(pathname, imageSearchParams)}
|
||||
scroll={false}
|
||||
className="h-full w-full"
|
||||
className="relative block h-full w-full"
|
||||
>
|
||||
<GridTileImage
|
||||
alt={image.altText}
|
||||
src={image.src}
|
||||
width={80}
|
||||
height={80}
|
||||
fill
|
||||
// width={80}
|
||||
// height={80}
|
||||
active={isActive}
|
||||
/>
|
||||
</Link>
|
||||
|
@ -7,9 +7,9 @@ import { VariantSelector } from './variant-selector';
|
||||
export function ProductDescription({ product }: { product: Product }) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 flex flex-col border-b pb-6 dark:border-neutral-700">
|
||||
<h1 className="mb-2 text-5xl font-medium">{product.title}</h1>
|
||||
<div className="mr-auto w-auto rounded-full bg-blue-600 p-2 text-sm text-white">
|
||||
<div className="mb-6 flex flex-col border-b border-white/20 pb-6">
|
||||
<h1 className="font-multilingual mb-2 text-5xl">{product.title}</h1>
|
||||
<div className="font-multilingual mr-auto w-auto text-lg text-white">
|
||||
<Price
|
||||
amount={product.priceRange.maxVariantPrice.amount}
|
||||
currencyCode={product.priceRange.maxVariantPrice.currencyCode}
|
||||
@ -20,7 +20,7 @@ export function ProductDescription({ product }: { product: Product }) {
|
||||
|
||||
{product.descriptionHtml ? (
|
||||
<Prose
|
||||
className="mb-6 text-sm leading-tight dark:text-white/[60%]"
|
||||
className="mb-6 text-lg leading-tight dark:text-white/[60%]"
|
||||
html={product.descriptionHtml}
|
||||
/>
|
||||
) : null}
|
||||
|
Loading…
x
Reference in New Issue
Block a user