mirror of
https://github.com/vercel/commerce.git
synced 2025-07-23 04:36:49 +00:00
Merge branch 'master' into arzafran/tweak-banner
This commit is contained in:
@@ -22,10 +22,6 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
||||
|
||||
return (
|
||||
<div className="bg-black text-white">
|
||||
<hr
|
||||
className="hidden md:block mt-4 border-gray-700"
|
||||
style={{ flexBasis: '100%', height: 0 }}
|
||||
/>
|
||||
<footer className={rootClassName}>
|
||||
<Link href="/">
|
||||
<a className="flex flex-initial items-center md:items-start font-bold md:mr-24">
|
||||
|
@@ -20,7 +20,7 @@ const Navbar: FC<Props> = ({ className }) => {
|
||||
<Logo />
|
||||
</a>
|
||||
</Link>
|
||||
<nav className="space-x-4 ml-6 hidden md:block">
|
||||
<nav className="space-x-4 ml-6 sm:hidden lg:block">
|
||||
<Link href="/">
|
||||
<a className={s.link}>All</a>
|
||||
</Link>
|
||||
@@ -33,11 +33,11 @@ const Navbar: FC<Props> = ({ className }) => {
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="md:flex flex-1 justify-center hidden">
|
||||
<div className="lg:flex flex-1 justify-center sm:hidden">
|
||||
<Searchbar />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-initial md:flex-1 justify-end space-x-8">
|
||||
<div className="flex flex-1 justify-end space-x-8">
|
||||
<Toggle
|
||||
checked={theme === 'dark'}
|
||||
onChange={() =>
|
||||
@@ -47,7 +47,8 @@ const Navbar: FC<Props> = ({ className }) => {
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
<div className="block flex pb-4 md:hidden px-4 md:px-6">
|
||||
|
||||
<div className="sm:flex pb-4 lg:px-6 lg:hidden">
|
||||
<Searchbar />
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,30 +1,24 @@
|
||||
import cn from 'classnames'
|
||||
import s from './ProductCard.module.css'
|
||||
import { FC, ReactNode, Component } from 'react'
|
||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
|
||||
import { Heart } from '@components/icon'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: ReactNode[] | Component[] | any[]
|
||||
node: ProductData
|
||||
product: ProductNode
|
||||
variant?: 'slim' | 'simple'
|
||||
}
|
||||
|
||||
interface ProductData {
|
||||
name: string
|
||||
images: any
|
||||
prices: any
|
||||
path: string
|
||||
}
|
||||
|
||||
const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
|
||||
const ProductCard: FC<Props> = ({ className, product: p, variant }) => {
|
||||
if (variant === 'slim') {
|
||||
return (
|
||||
<div className="relative overflow-hidden box-border">
|
||||
<img
|
||||
className="object-scale-down h-48"
|
||||
src={p.images.edges[0].node.urlSmall}
|
||||
src={p.images.edges?.[0]?.node.urlSmall}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-end mr-8">
|
||||
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
||||
@@ -41,7 +35,7 @@ const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
|
||||
<div className="absolute z-10 inset-0 flex items-center justify-center">
|
||||
<img
|
||||
className="w-full object-cover"
|
||||
src={p.images.edges[0].node.urlXL}
|
||||
src={p.images.edges?.[0]?.node.urlXL}
|
||||
/>
|
||||
</div>
|
||||
<div className={cn(s.squareBg, { [s.gray]: variant === 'simple' })} />
|
||||
@@ -50,7 +44,7 @@ const ProductCard: FC<Props> = ({ className, node: p, variant }) => {
|
||||
<p className={s.productTitle}>
|
||||
<span>{p.name}</span>
|
||||
</p>
|
||||
<span className={s.productPrice}>${p.prices.price.value}</span>
|
||||
<span className={s.productPrice}>${p.prices?.price.value}</span>
|
||||
</div>
|
||||
<div className={s.wishlistButton}>
|
||||
<Heart />
|
||||
|
22
components/product/ProductSlider/ProductSlider.module.css
Normal file
22
components/product/ProductSlider/ProductSlider.module.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.root {
|
||||
@apply relative w-full h-full;
|
||||
overflow-y: hidden;
|
||||
|
||||
& > div {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rootPanel {
|
||||
@apply absolute flex flex-row inset-0 z-20 m-20;
|
||||
}
|
||||
|
||||
.leftPanel {
|
||||
@apply flex-1;
|
||||
cursor: url('/cursor-left.png'), auto;
|
||||
}
|
||||
|
||||
.rightPanel {
|
||||
@apply flex-1;
|
||||
cursor: url('/cursor-right.png'), auto;
|
||||
}
|
@@ -1,7 +1,6 @@
|
||||
import { FC, useState } from 'react'
|
||||
import React from 'react'
|
||||
import React, { FC, useState } from 'react'
|
||||
import SwipeableViews from 'react-swipeable-views'
|
||||
|
||||
import s from './ProductSlider.module.css'
|
||||
interface Props {
|
||||
children?: any
|
||||
}
|
||||
@@ -19,14 +18,19 @@ const ProductSlider: FC<Props> = ({ children }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<div className="absolute flex flex-row inset-0 z-10 opacity-0">
|
||||
<div className="flex-1 bg-cyan" onClick={goBack}></div>
|
||||
<div className="flex-1 bg-pink" onClick={goNext}></div>
|
||||
</div>
|
||||
<SwipeableViews index={idx} onChangeIndex={setIdx}>
|
||||
<div className={s.root}>
|
||||
<SwipeableViews
|
||||
index={idx}
|
||||
onChangeIndex={setIdx}
|
||||
containerStyle={{ overflow: 'visible' }}
|
||||
slideStyle={{ overflow: 'visible' }}
|
||||
>
|
||||
{children}
|
||||
</SwipeableViews>
|
||||
<div className={s.rootPanel}>
|
||||
<div className={s.leftPanel} onClick={goBack}></div>
|
||||
<div className={s.rightPanel} onClick={goNext}></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -1,7 +1,89 @@
|
||||
.button {
|
||||
min-width: 300px;
|
||||
.root {
|
||||
@apply relative grid items-start gap-8 grid-cols-1;
|
||||
|
||||
@screen lg {
|
||||
@apply grid-cols-12 pt-10;
|
||||
}
|
||||
}
|
||||
|
||||
.productDisplay {
|
||||
@apply relative flex px-0 pb-0 relative box-border col-span-7;
|
||||
margin-right: -2rem;
|
||||
margin-left: -2rem;
|
||||
min-height: 400px;
|
||||
|
||||
@screen md {
|
||||
min-height: 700px;
|
||||
}
|
||||
|
||||
@screen lg {
|
||||
@apply mx-0;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.squareBg {
|
||||
@apply absolute inset-24 z-0 bg-violet;
|
||||
@apply absolute inset-0 bg-violet z-0;
|
||||
max-height: 250px;
|
||||
|
||||
@screen md {
|
||||
@apply inset-20;
|
||||
max-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.nameBox {
|
||||
@apply absolute top-6 left-0 z-50;
|
||||
|
||||
& .name {
|
||||
@apply px-6 py-2 bg-primary text-primary font-bold text-3xl;
|
||||
}
|
||||
|
||||
& .price {
|
||||
@apply px-6 py-2 pb-4 bg-primary text-primary font-bold inline-block tracking-wide;
|
||||
}
|
||||
|
||||
@screen md {
|
||||
& .name,
|
||||
& .price {
|
||||
@apply bg-violet text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@apply flex flex-col col-span-5;
|
||||
|
||||
@screen lg {
|
||||
padding-top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sliderContainer {
|
||||
@apply absolute z-10 inset-0 flex items-center justify-center overflow-x-hidden;
|
||||
}
|
||||
|
||||
.img {
|
||||
@apply w-full h-full object-cover;
|
||||
|
||||
@screen md {
|
||||
height: 100%;
|
||||
margin-top: -2.75rem;
|
||||
}
|
||||
|
||||
@screen lg {
|
||||
height: 150%;
|
||||
margin-top: -10%;
|
||||
}
|
||||
|
||||
@screen xl {
|
||||
height: 170%;
|
||||
margin-top: -19%;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
min-width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
@@ -1,30 +1,35 @@
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { FC, useState } from 'react'
|
||||
import s from './ProductView.module.css'
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import { Button, Container } from '@components/ui'
|
||||
import { Swatch, ProductSlider } from '@components/product'
|
||||
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
||||
import type { Product } from '@lib/bigcommerce/api/operations/get-product'
|
||||
import { getProductOptions } from '../helpers'
|
||||
|
||||
import s from './ProductView.module.css'
|
||||
import { isDesktop } from '@lib/browser'
|
||||
import cn from 'classnames'
|
||||
interface Props {
|
||||
className?: string
|
||||
children?: any
|
||||
product: Product
|
||||
product: ProductNode
|
||||
}
|
||||
|
||||
const ProductView: FC<Props> = ({ product, className }) => {
|
||||
const addItem = useAddItem()
|
||||
const { openSidebar } = useUI()
|
||||
const options = getProductOptions(product)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [validMedia, setValidMedia] = useState(false)
|
||||
|
||||
const [choices, setChoices] = useState<Record<string, any>>({
|
||||
size: null,
|
||||
color: null,
|
||||
})
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
useEffect(() => {
|
||||
setValidMedia(isDesktop())
|
||||
}, [])
|
||||
|
||||
const addToCart = async () => {
|
||||
setLoading(true)
|
||||
@@ -59,39 +64,40 @@ const ProductView: FC<Props> = ({ product, className }) => {
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex flex-row items-start fit my-12">
|
||||
<div className="absolute top-0 left-0 z-50">
|
||||
<h1 className="px-6 py-2 bg-violet text-white font-bold text-3xl">
|
||||
{product.name}
|
||||
</h1>
|
||||
<div className="px-6 py-2 pb-4 bg-violet text-white font-bold inline-block traking">
|
||||
{product.prices?.price.value}
|
||||
{` `}
|
||||
{product.prices?.price.currencyCode}
|
||||
<div className={cn(s.root, 'fit')}>
|
||||
<div className={cn(s.productDisplay, 'fit')}>
|
||||
<div className={s.squareBg}></div>
|
||||
<div className={s.nameBox}>
|
||||
<h1 className={s.name}>{product.name}</h1>
|
||||
<div className={s.price}>
|
||||
{product.prices?.price.value}
|
||||
{` `}
|
||||
{product.prices?.price.currencyCode}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 px-24 pb-0 relative fit box-border">
|
||||
<div className="absolute z-10 inset-0 flex items-center justify-center">
|
||||
<div className={s.sliderContainer}>
|
||||
<ProductSlider>
|
||||
{/** TODO: Change with Image Component */}
|
||||
{/** TODO: Change with Image Component **/}
|
||||
{product.images.edges?.map((image, i) => (
|
||||
<img
|
||||
key={image?.node.urlSmall}
|
||||
className="w-full object-cover"
|
||||
className={s.img}
|
||||
src={image?.node.urlXL}
|
||||
loading={i === 0 ? 'eager' : 'lazy'}
|
||||
/>
|
||||
))}
|
||||
</ProductSlider>
|
||||
</div>
|
||||
<div className="absolute z-10 bottom-10 left-1/2 transform -translate-x-1/2 inline-block">
|
||||
<img src="/slider-arrows.png" />
|
||||
</div>
|
||||
<div className={s.squareBg}></div>
|
||||
|
||||
{!validMedia && (
|
||||
<div className="absolute z-10 bottom-10 left-1/2 transform -translate-x-1/2 inline-block">
|
||||
<img src="/slider-arrows.png" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col pt-24">
|
||||
<div className={s.sidebar}>
|
||||
<section>
|
||||
{options?.map((opt: any) => (
|
||||
<div className="pb-4" key={opt.displayName}>
|
||||
|
@@ -1,13 +1,32 @@
|
||||
.root {
|
||||
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
|
||||
items-center justify-center cursor-pointer transition duration-100 ease-in-out
|
||||
p-0 shadow-none border-gray-200 border box-border text-black;
|
||||
p-0 shadow-none border-gray-200 border box-border;
|
||||
|
||||
& > span {
|
||||
@apply absolute;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply transform scale-110 bg-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.active.size {
|
||||
@apply border-accents-9 border-2;
|
||||
.color {
|
||||
@apply text-black transition duration-100 ease-in-out;
|
||||
|
||||
&:hover {
|
||||
@apply text-black;
|
||||
}
|
||||
|
||||
&.dark,
|
||||
&.dark:hover {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.root:hover {
|
||||
@apply transform scale-110 bg-hover;
|
||||
.active {
|
||||
&.size {
|
||||
@apply border-accents-9 border-2;
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ interface Props {
|
||||
|
||||
const Swatch: FC<Props & ButtonProps> = ({
|
||||
className,
|
||||
color,
|
||||
color = '',
|
||||
label,
|
||||
variant = 'size',
|
||||
active,
|
||||
@@ -23,13 +23,14 @@ const Swatch: FC<Props & ButtonProps> = ({
|
||||
}) => {
|
||||
variant = variant?.toLowerCase()
|
||||
label = label?.toLowerCase()
|
||||
const isDarkBg = isDark(color)
|
||||
|
||||
const rootClassName = cn(
|
||||
s.root,
|
||||
{
|
||||
[s.active]: active,
|
||||
[s.size]: variant === 'size',
|
||||
[s.color]: color,
|
||||
[s.dark]: color ? isDark(color) : false,
|
||||
},
|
||||
className
|
||||
)
|
||||
@@ -41,11 +42,7 @@ const Swatch: FC<Props & ButtonProps> = ({
|
||||
{...props}
|
||||
>
|
||||
{variant === 'color' && active && (
|
||||
<span
|
||||
className={cn('absolute', {
|
||||
'text-white': isDarkBg,
|
||||
})}
|
||||
>
|
||||
<span>
|
||||
<Check />
|
||||
</span>
|
||||
)}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { Product } from '@lib/bigcommerce/api/operations/get-product'
|
||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
||||
|
||||
export function getProductOptions(product: Product) {
|
||||
export function getProductOptions(product: ProductNode) {
|
||||
// console.log(product)
|
||||
const options = product.productOptions.edges?.map(({ node }: any) => ({
|
||||
displayName: node.displayName.toLowerCase(),
|
||||
|
@@ -52,7 +52,7 @@ const Sidebar: FC<Props> = ({ className, children, show = true, close }) => {
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-black bg-opacity-25 transition-opacity"
|
||||
className="absolute inset-0 bg-black bg-opacity-50 transition-opacity"
|
||||
// Close the sidebar when clicking on the backdrop
|
||||
onClick={close}
|
||||
/>
|
||||
|
Reference in New Issue
Block a user