Merge pull request #8 from sesamyab/jonatanschulz/ch226/product-grids-layout

Layout product grid
This commit is contained in:
frankjonatanschultz 2021-08-27 16:25:39 +02:00 committed by GitHub
commit f1041b5be9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 121 additions and 298 deletions

View File

@ -18,7 +18,7 @@
--accent-0: #fff; --accent-0: #fff;
--accent-1: #fafafa; --accent-1: #fafafa;
--accent-2: #eaeaea; --accent-2: #e2e2e2;
--accent-3: #b4afaf; --accent-3: #b4afaf;
--accent-4: #888888; --accent-4: #888888;
--accent-5: #666666; --accent-5: #666666;
@ -27,8 +27,6 @@
--accent-8: #111111; --accent-8: #111111;
--accent-9: #000; --accent-9: #000;
--font-sans: 'Inter', sans-serif;
--cyan: #22b8cf; --cyan: #22b8cf;
--green: #37b679; --green: #37b679;
--purple: #f81ce5; --purple: #f81ce5;
@ -39,46 +37,53 @@
--violet-dark: #4c2889; --violet-dark: #4c2889;
} }
*, @layer base {
*:before, *,
*:after { *:before,
box-sizing: inherit; *:after {
} box-sizing: inherit;
letter-spacing: -0.025em;
}
html {
height: 100%;
box-sizing: border-box;
touch-action: manipulation;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@apply text-html;
}
@screen lg {
html { html {
@apply text-html-lg; height: 100%;
box-sizing: border-box;
touch-action: manipulation;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@apply text-html;
}
@screen lg {
html {
@apply text-html-lg;
}
}
html,
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--primary);
color: var(--text-primary);
overscroll-behavior-x: none;
}
body {
position: relative;
min-height: 100%;
margin: 0;
}
a {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
} }
} }
html,
body {
font-family: var(--font-sans);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--primary);
color: var(--text-primary);
overscroll-behavior-x: none;
}
body { .tag {
position: relative; @apply inline-block py-2 px-5 text-4xl border border-black rounded-full;
min-height: 100%;
margin: 0;
}
a {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
} }
.animated { .animated {

View File

@ -57,11 +57,6 @@ const HomeAllProductsGrid: FC<Props> = ({
<ProductCard <ProductCard
key={product.path} key={product.path}
product={product} product={product}
variant="simple"
imgProps={{
width: 480,
height: 480,
}}
/> />
))} ))}
</Grid> </Grid>

View File

@ -1,114 +1,19 @@
.root { .root {
@apply relative max-h-full w-full box-border overflow-hidden @apply relative max-h-full w-full box-border overflow-hidden
bg-no-repeat bg-center bg-cover transition-transform transition-transform ease-linear cursor-pointer inline-block;
ease-linear cursor-pointer inline-block bg-accent-1;
height: 100% !important; height: 100% !important;
} }
.root:hover {
& .productImage {
transform: scale(1.2625);
}
& .header .name span,
& .header .price,
& .wishlistButton {
@apply bg-secondary text-secondary;
}
&:nth-child(6n + 1) .header .name span,
&:nth-child(6n + 1) .header .price,
&:nth-child(6n + 1) .wishlistButton {
@apply bg-violet text-white;
}
&:nth-child(6n + 5) .header .name span,
&:nth-child(6n + 5) .header .price,
&:nth-child(6n + 5) .wishlistButton {
@apply bg-blue text-white;
}
&:nth-child(6n + 3) .header .name span,
&:nth-child(6n + 3) .header .price,
&:nth-child(6n + 3) .wishlistButton {
@apply bg-pink text-white;
}
&:nth-child(6n + 6) .header .name span,
&:nth-child(6n + 6) .header .price,
&:nth-child(6n + 6) .wishlistButton {
@apply bg-cyan text-white;
}
}
.header {
@apply transition-colors ease-in-out duration-500
absolute top-0 left-0 z-20 pr-16;
}
.header .name {
@apply pt-0 max-w-full w-full leading-extra-loose
transition-colors ease-in-out duration-500;
font-size: 2rem;
letter-spacing: 0.4px;
}
.header .name span {
@apply py-4 px-6 bg-primary text-primary font-bold
transition-colors ease-in-out duration-500;
font-size: inherit;
letter-spacing: inherit;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.header .price {
@apply pt-2 px-6 pb-4 text-sm bg-primary text-accent-9
font-semibold inline-block tracking-wide
transition-colors ease-in-out duration-500;
}
.imageContainer { .imageContainer {
@apply flex items-center justify-center overflow-hidden; @apply relative flex items-center justify-center bg-primary-2 mb-4;
padding: 12% 15%;
}
.imageContainerInner {
@apply relative w-full;
padding-bottom: 100%;
} }
.imageContainer > div { .typeIcon {
min-width: 100%; @apply absolute bg-accent-2 rounded-full p-1.5 -bottom-2 right-2.5;
} font-size: 0;
.imageContainer .productImage {
@apply transform transition-transform duration-500
object-cover scale-120;
}
.root .wishlistButton {
@apply top-0 right-0 z-30 absolute;
}
/* Variant Simple */
.simple .header .name {
@apply pt-2 text-lg leading-10 -mt-1;
}
.simple .header .price {
@apply text-sm;
}
/* Variant Slim */
.slim {
@apply bg-transparent relative overflow-hidden
box-border;
}
.slim .header {
@apply absolute inset-0 flex items-center justify-end mr-8 z-20;
}
.slim span {
@apply bg-accent-9 text-accent-0 inline-block p-3
font-bold text-xl break-words;
}
.root:global(.secondary) .header span {
@apply bg-accent-0 text-accent-9;
} }

View File

@ -3,27 +3,20 @@ import cn from 'classnames'
import Link from 'next/link' import Link from 'next/link'
import type { Product } from '@commerce/types/product' import type { Product } from '@commerce/types/product'
import s from './ProductCard.module.css' import s from './ProductCard.module.css'
import Image, { ImageProps } from 'next/image' import Image from 'next/image'
import WishlistButton from '@components/wishlist/WishlistButton'
import usePrice from '@framework/product/use-price' import usePrice from '@framework/product/use-price'
import ProductTag from '../ProductTag'
interface Props { interface Props {
className?: string className?: string
product: Product product: Product
noNameTag?: boolean noNameTag?: boolean
imgProps?: Omit<ImageProps, 'src' | 'layout' | 'placeholder' | 'blurDataURL'>
variant?: 'default' | 'slim' | 'simple'
} }
const placeholderImg = '/product-img-placeholder.svg' const placeholderImg = '/product-img-placeholder.svg'
const ProductCard: FC<Props> = ({ const ProductCard: FC<Props> = ({
product, product,
imgProps,
className, className,
noNameTag = false,
variant = 'default',
}) => { }) => {
const { price } = usePrice({ const { price } = usePrice({
amount: product.price.value, amount: product.price.value,
@ -33,97 +26,40 @@ const ProductCard: FC<Props> = ({
const rootClassName = cn( const rootClassName = cn(
s.root, s.root,
{ [s.slim]: variant === 'slim', [s.simple]: variant === 'simple' },
className className
) )
return ( return (
<Link href={`/products/${product.slug}`}> <Link href={`/products/${product.slug}`}>
<a className={rootClassName}> <a className={rootClassName}>
{variant === 'slim' && ( <div className={s.imageContainer}>
<> <div className={s.imageContainerInner}>
<div className={s.header}>
<span>{product.name}</span>
</div>
{product?.images && ( {product?.images && (
<Image <Image
quality="85"
src={product.images[0]?.url || placeholderImg}
alt={product.name || 'Product Image'} alt={product.name || 'Product Image'}
height={320} className={s.productImage}
width={320} src={product.images[0]?.url || placeholderImg}
layout="fixed" quality="85"
{...imgProps} layout="fill"
objectFit="contain"
/> />
)} )}
</> </div>
)} <div className={s.typeIcon}>
<Image
{variant === 'simple' && ( src="/icon-audiobook.svg"
<> width={22}
{process.env.COMMERCE_WISHLIST_ENABLED && ( height={23}
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={product.variants[0]}
/>
)}
{!noNameTag && (
<div className={s.header}>
<h3 className={s.name}>
<span>{product.name}</span>
</h3>
<div className={s.price}>
{`${price} ${product.price?.currencyCode}`}
</div>
</div>
)}
<div className={s.imageContainer}>
{product?.images && (
<Image
alt={product.name || 'Product Image'}
className={s.productImage}
src={product.images[0]?.url || placeholderImg}
height={540}
width={540}
quality="85"
layout="responsive"
{...imgProps}
/>
)}
</div>
</>
)}
{variant === 'default' && (
<>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={product.variants[0] as any}
/>
)}
<ProductTag
name={product.name}
price={`${price} ${product.price?.currencyCode}`}
/> />
<div className={s.imageContainer}> </div>
{product?.images && ( </div>
<Image <div className="font-bold line-clamp-3">
alt={product.name || 'Product Image'} {product.name}
className={s.productImage} </div>
src={product.images[0]?.url || placeholderImg} <div className="italic">
height={540} Author (missing data)
width={540} </div>
quality="85" <div>{price}</div>
layout="responsive"
{...imgProps}
/>
)}
</div>
</>
)}
</a> </a>
</Link> </Link>
) )

View File

@ -38,7 +38,7 @@
} }
.relatedProductsGrid { .relatedProductsGrid {
@apply grid grid-cols-2 py-2 gap-2 md:grid-cols-4 md:gap-7; @apply grid grid-cols-2 py-2 gap-2 md:grid-cols-6 md:gap-7;
} }
@screen lg { @screen lg {

View File

@ -67,18 +67,13 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
{relatedProducts.map((p) => ( {relatedProducts.map((p) => (
<div <div
key={p.path} key={p.path}
className="animated fadeIn bg-accent-0 border border-accent-2" className="animated fadeIn"
> >
<ProductCard <ProductCard
noNameTag noNameTag
product={p} product={p}
key={p.path} key={p.path}
variant="simple"
className="animated fadeIn" className="animated fadeIn"
imgProps={{
width: 300,
height: 300,
}}
/> />
</div> </div>
))} ))}

View File

@ -313,17 +313,12 @@ export default function Search({ categories, brands }: SearchPropsType) {
</div> </div>
)} )}
{data ? ( {data ? (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{data.products.map((product: Product) => ( {data.products.map((product: Product) => (
<ProductCard <ProductCard
variant="simple"
key={product.path} key={product.path}
className="animated fadeIn" className="animated fadeIn"
product={product} product={product}
imgProps={{
width: 480,
height: 480,
}}
/> />
))} ))}
</div> </div>

View File

@ -1,8 +1,8 @@
.root { .root {
@apply grid grid-cols-1 gap-0; @apply grid grid-cols-2 gap-0;
@screen lg { @screen lg {
@apply grid-cols-3 grid-rows-2; @apply grid-cols-6;
} }
& > * { & > * {
@ -24,7 +24,7 @@
} }
.layoutNormal { .layoutNormal {
@apply gap-3; @apply gap-6;
} }
@screen md { @screen md {

View File

@ -21,6 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@react-spring/web": "^9.2.1", "@react-spring/web": "^9.2.1",
"@tailwindcss/line-clamp": "^0.2.1",
"@vercel/fetch": "^6.1.0", "@vercel/fetch": "^6.1.0",
"autoprefixer": "^10.2.6", "autoprefixer": "^10.2.6",
"body-scroll-lock": "^3.1.5", "body-scroll-lock": "^3.1.5",

View File

@ -1,8 +1,7 @@
import commerce from '@lib/api/commerce' import commerce from '@lib/api/commerce'
import { Layout } from '@components/common' import { Layout } from '@components/common'
import { ProductCard } from '@components/product' import { ProductCard } from '@components/product'
import { Grid, Marquee, Hero } from '@components/ui' import { Grid, Container, Marquee } from '@components/ui'
// import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid'
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
export async function getStaticProps({ export async function getStaticProps({
@ -15,8 +14,6 @@ export async function getStaticProps({
variables: { first: 6 }, variables: { first: 6 },
config, config,
preview, preview,
// Saleor provider only
...({ featured: true } as any),
}) })
const pagesPromise = commerce.getAllPages({ config, preview }) const pagesPromise = commerce.getAllPages({ config, preview })
const siteInfoPromise = commerce.getSiteInfo({ config, preview }) const siteInfoPromise = commerce.getSiteInfo({ config, preview })
@ -40,49 +37,26 @@ export default function Home({
}: InferGetStaticPropsType<typeof getStaticProps>) { }: InferGetStaticPropsType<typeof getStaticProps>) {
return ( return (
<> <>
<Grid variant="filled"> <Container className="py-12">
{products.slice(0, 3).map((product: any, i: number) => ( <h3 className="tag">New releases</h3>
<ProductCard <Grid layout="normal" className="mt-8 mb-16">
key={product.id} {products.slice(0, 6).map((product: any, i: number) => (
product={product} <ProductCard
imgProps={{ key={product.id}
width: i === 0 ? 1080 : 540, product={product}
height: i === 0 ? 1080 : 540, />
}} ))}
/> </Grid>
))} <h3 className="tag">Bestsellers</h3>
</Grid> <Grid layout="normal" className="mt-8 mb-16">
<Marquee variant="secondary"> {products.slice(0, 6).map((product: any, i: number) => (
{products.slice(0, 3).map((product: any, i: number) => ( <ProductCard
<ProductCard key={product.id} product={product} variant="slim" /> key={product.id}
))} product={product}
</Marquee> />
<Hero ))}
headline=" Dessert dragée halvah croissant." </Grid>
description="Cupcake ipsum dolor sit amet lemon drops pastry cotton candy. Sweet carrot cake macaroon bonbon croissant fruitcake jujubes macaroon oat cake. Soufflé bonbon caramels jelly beans. Tiramisu sweet roll cheesecake pie carrot cake. " </Container>
/>
<Grid layout="B" variant="filled">
{products.slice(0, 3).map((product: any, i: number) => (
<ProductCard
key={product.id}
product={product}
imgProps={{
width: i === 0 ? 1080 : 540,
height: i === 0 ? 1080 : 540,
}}
/>
))}
</Grid>
<Marquee>
{products.slice(3).map((product: any, i: number) => (
<ProductCard key={product.id} product={product} variant="slim" />
))}
</Marquee>
{/* <HomeAllProductsGrid
newestProducts={products}
categories={categories}
brands={brands}
/> */}
</> </>
) )
} }

View File

@ -24,7 +24,7 @@ export async function getStaticProps({
}) })
const allProductsPromise = commerce.getAllProducts({ const allProductsPromise = commerce.getAllProducts({
variables: { first: 4 }, variables: { first: 6 },
config, config,
preview, preview,
}) })

View File

@ -0,0 +1,3 @@
<svg width="30" height="32" viewBox="3 2.5 26 27.5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" d="M16 4.5c-4.4183 0-8 3.58172-8 8v2c0 .5523-.44772 1-1 1s-1-.4477-1-1v-2c0-5.52285 4.4772-10 10-10s10 4.47715 10 10v2c0 .5523-.4477 1-1 1s-1-.4477-1-1v-2c0-4.41828-3.5817-8-8-8zM9 25v-3.5c0-1.1046-.89543-2-2-2s-2 .8954-2 2V25c0 1.1046.89543 2 2 2s2-.8954 2-2zm-2-7.5c-2.20914 0-4 1.7909-4 4V25c0 2.2091 1.79086 4 4 4s4-1.7909 4-4v-3.5c0-2.2091-1.79086-4-4-4zm20 4V25c0 1.1046-.8954 2-2 2s-2-.8954-2-2v-3.5c0-1.1046.8954-2 2-2s2 .8954 2 2zm-6 0c0-2.2091 1.7909-4 4-4 2.2091 0 4 1.7909 4 4V25c0 2.2091-1.7909 4-4 4-2.2091 0-4-1.7909-4-4v-3.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 726 B

3
public/icon-ebook.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="23" height="32" viewBox="6 3 20 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" d="M22.5 5h-13C8.67157 5 8 5.67157 8 6.5v20c0 .8284.67157 1.5 1.5 1.5h13c.8284 0 1.5-.6716 1.5-1.5v-20c0-.82843-.6716-1.5-1.5-1.5zm-13-2C7.567 3 6 4.567 6 6.5v20C6 28.433 7.567 30 9.5 30h13c1.933 0 3.5-1.567 3.5-3.5v-20C26 4.567 24.433 3 22.5 3h-13zM22 9c0 .55228-.4477 1-1 1H11c-.5523 0-1-.44772-1-1s.4477-1 1-1h10c.5523 0 1 .44772 1 1zm-1 5c.5523 0 1-.4477 1-1s-.4477-1-1-1H11c-.5523 0-1 .4477-1 1s.4477 1 1 1h10zm-2.5 3c0 .5523-.4477 1-1 1H11c-.5523 0-1-.4477-1-1s.4477-1 1-1h6.5c.5523 0 1 .4477 1 1zM16 26c1.1046 0 2-.8954 2-2s-.8954-2-2-2-2 .8954-2 2 .8954 2 2 2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@ -1,4 +1,5 @@
module.exports = { module.exports = {
plugins: [require('@tailwindcss/line-clamp')],
future: { future: {
purgeLayersByDefault: true, purgeLayersByDefault: true,
applyComplexClasses: true, applyComplexClasses: true,
@ -58,9 +59,13 @@ module.exports = {
magical: magical:
'rgba(0, 0, 0, 0.02) 0px 30px 30px, rgba(0, 0, 0, 0.03) 0px 0px 8px, rgba(0, 0, 0, 0.05) 0px 1px 0px', 'rgba(0, 0, 0, 0.02) 0px 30px 30px, rgba(0, 0, 0, 0.03) 0px 0px 8px, rgba(0, 0, 0, 0.05) 0px 1px 0px',
}, },
fontFamily: {
sans: '"Inter","Helvetica Neue",HelveticaNeue,"TeX Gyre Heros",TeXGyreHeros,FreeSans,"Nimbus Sans L","Liberation Sans",Arimo,Helvetica,sans-serif',
},
fontSize: { fontSize: {
html: '15px', html: '15px',
'html-lg': '18px', 'html-lg': '18px',
'4xl': '2rem',
}, },
lineHeight: { lineHeight: {
'extra-loose': '2.2', 'extra-loose': '2.2',
@ -68,6 +73,12 @@ module.exports = {
scale: { scale: {
120: '1.2', 120: '1.2',
}, },
width: {
'9/12': '75%',
},
height: {
'9/12': '75%',
},
}, },
}, },
} }