add: GridItem & Grid

This commit is contained in:
Minjee Son 2024-09-12 19:28:35 +01:00
parent 3bc03dd7c8
commit a17e00f875
10 changed files with 342 additions and 279 deletions

265
app/data.ts Normal file
View File

@ -0,0 +1,265 @@
import { Product } from 'lib/shopify/types';
//temp: for ProductGridItems test
export const mockProducts: Product[] = [
{
id: 'prod_001',
handle: 'product-1',
availableForSale: true,
title: 'Product 1',
description: 'This is the description for Product 1',
descriptionHtml: '<p>This is the <strong>HTML</strong> description for Product 1</p>',
options: [
{
id: 'option_001',
name: 'Size',
values: ['S', 'M', 'L']
}
],
priceRange: {
maxVariantPrice: {
amount: '100.00',
currencyCode: 'USD'
},
minVariantPrice: {
amount: '80.00',
currencyCode: 'USD'
}
},
featuredImage: {
url: 'https://cdn.shopify.com/static/sample-images/bath.jpeg',
altText: 'Product 1 Featured Image',
width: 500,
height: 500
},
seo: {
title: 'Product 1 SEO Title',
description: 'This is the SEO description for Product 1'
},
tags: ['tag1', 'tag2'],
updatedAt: new Date().toISOString(),
variants: [
{
id: 'variant_001',
title: 'Variant 1',
availableForSale: true,
selectedOptions: [
{
name: 'Size',
value: 'M'
}
],
price: {
amount: '90.00',
currencyCode: 'USD'
}
}
],
images: [
{
url: 'https://cdn.shopify.com/static/sample-images/bath.jpeg',
altText: 'Product 1 Image 1',
width: 500,
height: 500
},
{
url: 'https://cdn.shopify.com/static/sample-images/bath.jpeg',
altText: 'Product 1 Image 2',
width: 400,
height: 400
}
]
},
{
id: 'prod_002',
handle: 'product-2',
availableForSale: false,
title: 'Product 2',
description: 'This is the description for Product 2',
descriptionHtml: '<p>This is the <strong>HTML</strong> description for Product 2</p>',
options: [
{
id: 'option_002',
name: 'Color',
values: ['Red', 'Blue', 'Green']
}
],
priceRange: {
maxVariantPrice: {
amount: '120.00',
currencyCode: 'USD'
},
minVariantPrice: {
amount: '100.00',
currencyCode: 'USD'
}
},
featuredImage: {
url: 'https://cdn.shopify.com/static/sample-images/teapot.jpg',
altText: 'Product 2 Featured Image',
width: 500,
height: 500
},
seo: {
title: 'Product 2 SEO Title',
description: 'This is the SEO description for Product 2'
},
tags: ['tag3', 'tag4'],
updatedAt: new Date().toISOString(),
variants: [
{
id: 'variant_002',
title: 'Variant 2',
availableForSale: false,
selectedOptions: [
{
name: 'Color',
value: 'Red'
}
],
price: {
amount: '110.00',
currencyCode: 'USD'
}
}
],
images: [
{
url: 'https://cdn.shopify.com/static/sample-images/teapot.jpg',
altText: 'Product 2 Image 1',
width: 500,
height: 500
}
]
},
{
id: 'prod_003',
handle: 'product-3',
availableForSale: true,
title: 'Product 3',
description: 'This is the description for Product 3',
descriptionHtml: '<p>This is the <strong>HTML</strong> description for Product 3</p>',
options: [
{
id: 'option_003',
name: 'Size',
values: ['S', 'M', 'L']
}
],
priceRange: {
maxVariantPrice: {
amount: '300.00',
currencyCode: 'USD'
},
minVariantPrice: {
amount: '80.00',
currencyCode: 'USD'
}
},
featuredImage: {
url: 'https://cdn.shopify.com/static/sample-images/bath.jpeg',
altText: 'Product 3 Featured Image',
width: 500,
height: 500
},
seo: {
title: 'Product 3 SEO Title',
description: 'This is the SEO description for Product 3'
},
tags: ['tag3', 'tag2'],
updatedAt: new Date().toISOString(),
variants: [
{
id: 'variant_003',
title: 'Variant 3',
availableForSale: true,
selectedOptions: [
{
name: 'Size',
value: 'M'
}
],
price: {
amount: '90.00',
currencyCode: 'USD'
}
}
],
images: [
{
url: 'https://cdn.shopify.com/static/sample-images/bath.jpeg',
altText: 'Product 3 Image 1',
width: 500,
height: 500
},
{
url: 'https://cdn.shopify.com/static/sample-images/bath.jpeg',
altText: 'Product 3 Image 2',
width: 400,
height: 400
}
]
},
{
id: 'prod_004',
handle: 'product-4',
availableForSale: false,
title: 'Product 4',
description: 'This is the description for Product 4',
descriptionHtml: '<p>This is the <strong>HTML</strong> description for Product 4</p>',
options: [
{
id: 'option_004',
name: 'Color',
values: ['Red', 'Blue', 'Green']
}
],
priceRange: {
maxVariantPrice: {
amount: '140.00',
currencyCode: 'USD'
},
minVariantPrice: {
amount: '100.00',
currencyCode: 'USD'
}
},
featuredImage: {
url: 'https://cdn.shopify.com/static/sample-images/teapot.jpg',
altText: 'Product 4 Featured Image',
width: 500,
height: 500
},
seo: {
title: 'Product 4 SEO Title',
description: 'This is the SEO description for Product 4'
},
tags: ['tag3', 'tag4'],
updatedAt: new Date().toISOString(),
variants: [
{
id: 'variant_004',
title: 'Variant 4',
availableForSale: false,
selectedOptions: [
{
name: 'Color',
value: 'Red'
}
],
price: {
amount: '110.00',
currencyCode: 'USD'
}
}
],
images: [
{
url: 'https://cdn.shopify.com/static/sample-images/teapot.jpg',
altText: 'Product 4 Image 1',
width: 500,
height: 500
}
]
}
];

View File

@ -1,3 +1,4 @@
import Footer from 'components/layout/footer';
import { Navbar } from 'components/layout/navbar';
import { GeistSans } from 'geist/font/sans';
import { getCart } from 'lib/shopify';
@ -43,6 +44,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
<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">
<Navbar />
<main>{children}</main>
<Footer />
</body>
</html>
);

View File

@ -1,9 +1,10 @@
import { mockProducts } from 'app/data';
import Error from 'app/error';
import Footer from 'components/layout/footer';
import Grid from 'components/grid';
import { Search } from 'components/layout/search';
import { PriceBox } from 'components/price-box';
import ProductGridItems from 'components/product/product-grid-items';
import { getCollectionProducts } from 'lib/shopify';
import type { Product } from 'lib/shopify/types';
import Image from 'next/image';
//Todo: change to proper metadata
@ -30,157 +31,48 @@ export default async function HomePage() {
}
alt={products[0].featuredImage.altText || 'Main product'}
fill
objectFit="cover"
quality={100}
priority
className="object-cover"
/>
</div>
<div className="absolute bottom-20 flex w-full flex-col items-center text-lightText">
<h1 className="text-xl">{products[0].title}</h1>
<span className="mb-6 mt-1 text-sm text-lightText/80">Read more</span>
<div className="text-mainBg flex w-[384px] justify-center gap-[10px]">
<div className="flex w-[384px] justify-center gap-[10px] text-mainBg">
<PriceBox title="Box of 20" price={2460} />
<PriceBox title="Single Cigar" price={120} />
</div>
</div>
<Search />
</section>
<Footer />
<Search />
<Grid className="grid-cols-2 sm:grid-cols-4">
{mockProducts.slice(0, 4).map(({ featuredImage, id, title, handle }) => (
<ProductGridItems
key={id}
src={featuredImage.url}
title={title}
handle={handle}
ratio="2/3"
/>
))}
</Grid>
<Grid className="grid-cols-1 sm:grid-cols-3">
{mockProducts.slice(0, 3).map(({ featuredImage, id, title, handle }) => (
<ProductGridItems
key={id}
src={featuredImage.url}
title={title}
handle={handle}
ratio="2/3"
/>
))}
</Grid>
<Grid className="grid-cols-1 sm:grid-cols-2">
{mockProducts.slice(0, 2).map(({ featuredImage, id, title, handle }) => (
<ProductGridItems key={id} src={featuredImage.url} title={title} handle={handle} />
))}
</Grid>
</>
);
}
//temp: for ProductGridItems test
const mockProducts: Product[] = [
{
id: 'prod_001',
handle: 'product-1',
availableForSale: true,
title: 'Product 1',
description: 'This is the description for Product 1',
descriptionHtml: '<p>This is the <strong>HTML</strong> description for Product 1</p>',
options: [
{
id: 'option_001',
name: 'Size',
values: ['S', 'M', 'L']
}
],
priceRange: {
maxVariantPrice: {
amount: '100.00',
currencyCode: 'USD'
},
minVariantPrice: {
amount: '80.00',
currencyCode: 'USD'
}
},
featuredImage: {
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
altText: 'Product 1 Featured Image',
width: 500,
height: 500
},
seo: {
title: 'Product 1 SEO Title',
description: 'This is the SEO description for Product 1'
},
tags: ['tag1', 'tag2'],
updatedAt: new Date().toISOString(),
variants: [
{
id: 'variant_001',
title: 'Variant 1',
availableForSale: true,
selectedOptions: [
{
name: 'Size',
value: 'M'
}
],
price: {
amount: '90.00',
currencyCode: 'USD'
}
}
],
images: [
{
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
altText: 'Product 1 Image 1',
width: 500,
height: 500
},
{
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
altText: 'Product 1 Image 2',
width: 400,
height: 400
}
]
},
{
id: 'prod_002',
handle: 'product-2',
availableForSale: false,
title: 'Product 2',
description: 'This is the description for Product 2',
descriptionHtml: '<p>This is the <strong>HTML</strong> description for Product 2</p>',
options: [
{
id: 'option_002',
name: 'Color',
values: ['Red', 'Blue', 'Green']
}
],
priceRange: {
maxVariantPrice: {
amount: '120.00',
currencyCode: 'USD'
},
minVariantPrice: {
amount: '100.00',
currencyCode: 'USD'
}
},
featuredImage: {
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
altText: 'Product 2 Featured Image',
width: 500,
height: 500
},
seo: {
title: 'Product 2 SEO Title',
description: 'This is the SEO description for Product 2'
},
tags: ['tag3', 'tag4'],
updatedAt: new Date().toISOString(),
variants: [
{
id: 'variant_002',
title: 'Variant 2',
availableForSale: false,
selectedOptions: [
{
name: 'Color',
value: 'Red'
}
],
price: {
amount: '110.00',
currencyCode: 'USD'
}
}
],
images: [
{
url: 'https://cdn.shopify.com/static/sample-images/garnished.jpeg',
altText: 'Product 2 Image 1',
width: 500,
height: 500
}
]
}
];

View File

@ -1,6 +1,6 @@
import Footer from 'components/layout/footer';
import Collections from 'components/layout/search/collections';
import FilterList from 'components/layout/search/filter';
import Collections from 'components/layout/search/*not-in-use/collections';
import FilterList from 'components/layout/search/*not-in-use/filter';
import { sorting } from 'lib/constants';
import ChildrenWrapper from './children-wrapper';

View File

@ -2,7 +2,7 @@ import clsx from 'clsx';
function Grid(props: React.ComponentProps<'ul'>) {
return (
<ul {...props} className={clsx('grid grid-flow-row gap-4', props.className)}>
<ul {...props} className={clsx('grid grid-flow-row', props.className)}>
{props.children}
</ul>
);
@ -10,7 +10,7 @@ function Grid(props: React.ComponentProps<'ul'>) {
function GridItem(props: React.ComponentProps<'li'>) {
return (
<li {...props} className={clsx('aspect-square transition-opacity', props.className)}>
<li {...props} className={clsx(props.className)}>
{props.children}
</li>
);

View File

@ -1,61 +0,0 @@
import { GridTileImage } from 'components/grid/tile';
import { getCollectionProducts } from 'lib/shopify';
import type { Product } from 'lib/shopify/types';
import Link from 'next/link';
function ThreeItemGridItem({
item,
size,
priority
}: {
item: Product;
size: 'full' | 'half';
priority?: boolean;
}) {
return (
<div
className={size === 'full' ? 'md:col-span-4 md:row-span-2' : 'md:col-span-2 md:row-span-1'}
>
<Link
className="relative block aspect-square h-full w-full"
href={`/product/${item.handle}`}
prefetch={true}
>
<GridTileImage
src={item.featuredImage.url}
fill
sizes={
size === 'full' ? '(min-width: 768px) 66vw, 100vw' : '(min-width: 768px) 33vw, 100vw'
}
priority={priority}
alt={item.title}
label={{
position: size === 'full' ? 'center' : 'bottom',
title: item.title as string,
amount: item.priceRange.maxVariantPrice.amount,
currencyCode: item.priceRange.maxVariantPrice.currencyCode
}}
/>
</Link>
</div>
);
}
export async function ThreeItemGrid() {
// Collections that start with `hidden-*` are hidden from the search page.
const homepageItems = await getCollectionProducts({
collection: 'hidden-homepage-featured-items'
});
if (!homepageItems[0] || !homepageItems[1] || !homepageItems[2]) return null;
const [firstProduct, secondProduct, thirdProduct] = homepageItems;
return (
<section className="mx-auto grid max-w-screen-2xl gap-4 px-4 pb-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]">
<ThreeItemGridItem size="full" item={firstProduct} priority={true} />
<ThreeItemGridItem size="half" item={secondProduct} priority={true} />
<ThreeItemGridItem size="half" item={thirdProduct} />
</section>
);
}

View File

@ -1,49 +1,19 @@
import clsx from 'clsx';
import Image from 'next/image';
import Label from '../label';
export function GridTileImage({
isInteractive = true,
active,
label,
title,
...props
}: {
isInteractive?: boolean;
active?: boolean;
label?: {
title: string;
amount: string;
currencyCode: string;
position?: 'bottom' | 'center';
};
title?: string;
} & React.ComponentProps<typeof Image>) {
return (
<div
className={clsx(
'group flex h-full w-full items-center justify-center overflow-hidden rounded-lg border bg-white hover:border-blue-600 dark:bg-black',
{
relative: label,
'border-2 border-blue-600': active,
'border-neutral-200 dark:border-neutral-800': !active
}
)}
>
{props.src ? (
<Image
className={clsx('relative h-full w-full object-contain', {
'transition duration-300 ease-in-out group-hover:scale-105': isInteractive
})}
{...props}
/>
<>
{props.src ? <Image fill className="h-full w-full object-cover" {...props} /> : null}
{title ? (
<h3 className="absolute bottom-0 w-full p-10 text-center text-[15px] text-lightText">
{title}
</h3>
) : null}
{label ? (
<Label
title={label.title}
amount={label.amount}
currencyCode={label.currencyCode}
position={label.position}
/>
) : null}
</div>
</>
);
}

View File

@ -1,32 +0,0 @@
import Grid from 'components/grid';
import { GridTileImage } from 'components/grid/tile';
import { Product } from 'lib/shopify/types';
import Link from 'next/link';
export default function ProductGridItems({ products }: { products: Product[] }) {
return (
<>
{products.map((product) => (
<Grid.Item key={product.handle} className="animate-fadeIn">
<Link
className="relative inline-block h-full w-full"
href={`/product/${product.handle}`}
prefetch={true}
>
<GridTileImage
alt={product.title}
label={{
title: product.title,
amount: product.priceRange.maxVariantPrice.amount,
currencyCode: product.priceRange.maxVariantPrice.currencyCode
}}
src={product.featuredImage?.url}
fill
sizes="(min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
/>
</Link>
</Grid.Item>
))}
</>
);
}

View File

@ -0,0 +1,27 @@
import { clsx } from 'clsx';
import Grid from 'components/grid';
import { GridTileImage } from 'components/grid/tile';
import Link from 'next/link';
export default function ProductGridItems({
src,
title,
handle,
ratio
}: {
src: string;
title: string;
handle: string;
ratio?: '2/3' | 'square';
}) {
return (
<Grid.Item
key={handle}
className={clsx('relative w-full', ratio === '2/3' ? `aspect-[2/3]` : 'aspect-square')}
>
<Link className="h-full w-full" href={`/product/${handle}`} prefetch={true}>
<GridTileImage alt={title} title={title} src={src} fill />
</Link>
</Grid.Item>
);
}

View File

@ -5,8 +5,8 @@ module.exports = {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.shopify.com',
pathname: '/s/files/**'
hostname: 'cdn.shopify.com'
// pathname: '/s/files/**'
}
]
}