Agnostic UI (#199)

* changes

* Progress

* Normalized Products output

* Progress

* Restored Index Agnostic

* Progress

* Reordering

* Moved normalizer to BC function

* Removed Futures

* More Types

* More Types

* More Types

* Fix useCallback

* Progress, Changes types, readme and restoring functionality

* Changes

* TS Issues

* Changes

* Normalizer

* Normalizing more operations

* Normalizing more operations

* changes

* Merge Issues

* Cleanup

* change

* changes

* index.ts broke my tree shaking

* slug

* Normalized Options and Swatches

* Restored Add to cart

* Correct Variant Added to Cart

* Normalizing Cart Responses

* Changes

* changes breaking

* Adding immutable normalizer for Product

* Cart Normalized

* changes

* Progress

* More updates

* Removed some comments

* Add loading state for data hooks

* Bug fix

* Changed the way isEmpty works

* Improve navbar performance

* Added useResponse hook

* added useResponse to useWhishlist

* Added husky and lint-staged

* Ran prettier fix

* Added more cart types

* Moved types.d.ts to the commerce folder

* Minor changes

* Moved normalizer to happen after fetch

* updated useCart types

* Updated normalizer for useData

* Added new normalizer for the cart to the UI

* More corrections for useCart

* Updated cart update hooks

* Removed import

* Progress

* Switch away from global types

* Making multiple changes

* Improved types for operations

* Moved and updated cart types

* Updated the useAddItem and useRemoveItem hooks

* Minor life improvement

* Minor change

* Implement Shopify Provider

* Update README.md

* Update README.md

* normalizations & missing files

* Update index.ts

* fixes

* Update normalize.ts

* fix: cart error on first load

* shopify checkout redirect & api handler

* Update get-checkout-id.ts

* userAvatar

* Fix: color option

* Update normalize.ts

* changes

* Update next.config.js

* start customer auth & signup

* Update config.ts

* Login, Sign Up, Log Out, and checkout & customer association

* Automatic login after sign-up

* Update handle-login.ts

* MOving stuff around and adding temporal new files

* changes

* Replace use-cart with the new hook

* Removed old hook

* Improved HookHandler type

* Moved types

* Simplified useData types

* Updated Fetcher type

* Moved SwrOptions type

* Removed duplicated fetcher

* Moved provider to its own file

* Added proper type for fetch input

* Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic"

This reverts commit 23c8ed7c2d, reversing
changes made to bf50965a39.

* change readme

* Revert "Merge branch 'master' of https://github.com/vercel/commerce into agnostic"

This reverts commit bf50965a39, reversing
changes made to 0dad4ddedb.

* Revert "Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic""

This reverts commit c9a43f1bce.

* align with upstream changes

* Updated how the hook input is handled

* Add more options to the hook handler

* Final touches to the hook handler type

* Moved useWishlist to use new handler

* Move useCustomer to the new hook

* Added a default fetcher

* query all products for vendors & paths, improve search

* Update use-search.tsx

* fix cart after upstream changes

* Shopify Provider (#186)

* Start of Shopify provider

* add missing comment to documentation

* add missing env vars to documentation

* update reference to types file

* Moved useSearch to the new hook

* Removed old use-data lib

* Removed generics for result and body

* Removed normalizr

* Wishlist

* New changes and initial Features API

* Fixed some types

* Fixed more types

* fixes after upstream changes

* Fixed product types

* Fixed another product type

* Updated type

* Fixed remaining issues with types

* Added a MutationHandler

* Moved the handlers to each hook

* Moved the fetcher to its own file

* Moved handler to each hook

* Added initial version of useAddItem

* Added better mutation types, and moved some hooks

* Removed use-cart-actions

* Added initial version of useAddItem

* Updated types

* Update use-add-item.tsx

* changes

* Changes

* Reordering and changes

* Adding Features APO

* Adding wishlist api

* Implementing FeaturesAPI with Wishlist

* Removing bug with touchstart

* Adding tyni typing

* moved use-remove-item

* Removed MutationHandler type

* Moved more hooks and updated types to make them smaller

* Moved data hooks to new format

* Removed no longer required types

* Removed useResponse helper

* Updated useData type

* Moved wishlist use-add-item

* Moved wishlist use-remove-item to provider

* Moved use-login and use-logout

* Update use-signup

* Removed use-action helper

* Moved auth & cart hooks + several fixes

* Updated cart item, fixed deprecations

* Update next.config.js

* Updates to wishlist feature

* Moved the features to be environment variable only

* More changes for wishlist config

* Disable wishlist

* Removed useWishlistActions

* Updated readme

* updates

* typos

* Updated the way the provider config is set

* Removed features.ts

* Removed bootstrap.js

* Aligned with upstream changes

* Updates

* shopify: changes

* shopify: changes

* Update next.config.js

* Shopify Provider Updates (#209)

* Implement Shopify Provider

* Update README.md

* Update README.md

* normalizations & missing files

* Update index.ts

* fixes

* Update normalize.ts

* fix: cart error on first load

* shopify checkout redirect & api handler

* Update get-checkout-id.ts

* Fix: color option

* Update normalize.ts

* changes

* Update next.config.js

* start customer auth & signup

* Update config.ts

* Login, Sign Up, Log Out, and checkout & customer association

* Automatic login after sign-up

* Update handle-login.ts

* changes

* Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic"

This reverts commit 23c8ed7c2d, reversing
changes made to bf50965a39.

* change readme

* Revert "Merge branch 'master' of https://github.com/vercel/commerce into agnostic"

This reverts commit bf50965a39, reversing
changes made to 0dad4ddedb.

* Revert "Revert "Merge branch 'agnostic' of https://github.com/vercel/commerce into agnostic""

This reverts commit c9a43f1bce.

* align with upstream changes

* query all products for vendors & paths, improve search

* Update use-search.tsx

* fix cart after upstream changes

* fixes after upstream changes

* Moved handler to each hook

* Added initial version of useAddItem

* Updated types

* Update use-add-item.tsx

* Moved auth & cart hooks + several fixes

* Updated cart item, fixed deprecations

* Update next.config.js

* Aligned with upstream changes

* Updates

* Update next.config.js

* Updated the commerce config structure

* Removed @framework imports within framework providers

* Fixed types

* changes

* Adding extra config

* Adding shopify commit

* Adding env templates to the providers

* Ignore some types

* Adding link for Cart

* Adding customCheckout

* multiple changes to fix the wishlist

* Shopify Provier Updates (#212)

* changes

* Adding shopify commit

* Changed to query page by id

* Fixed page query, Changed use-search GraphQl query

* Update use-search.tsx

* remove unused util

* Changed cookie expiration

* Update tsconfig.json

Co-authored-by: okbel <curciobel@gmail.com>

* Bump and adding dependency

* Adding color checks

* Now colors work with lighter colors

* Stable commerce.config.json

* Updated main readme

* Readme changes

* Default to bigcommerce

Co-authored-by: bc <bc@bcs-MacBook-Pro.fibertel.com.ar>
Co-authored-by: Luis Alvarez <luis@vercel.com>
Co-authored-by: cond0r <pinte_catalin@yahoo.com>
Co-authored-by: Peter Mekhaeil <4616064+petermekhaeil@users.noreply.github.com>
This commit is contained in:
B
2021-03-04 07:57:25 -03:00
committed by GitHub
parent b121078151
commit 9b71bd77fc
232 changed files with 20545 additions and 1895 deletions

View File

@@ -1,6 +1,6 @@
import { FC, useEffect, useState, useCallback } from 'react'
import { Logo, Button, Input } from '@components/ui'
import useLogin from '@framework/use-login'
import useLogin from '@framework/auth/use-login'
import { useUI } from '@components/ui/context'
import { validate } from 'email-validator'

View File

@@ -3,7 +3,7 @@ import { validate } from 'email-validator'
import { Info } from '@components/icons'
import { useUI } from '@components/ui/context'
import { Logo, Button, Input } from '@components/ui'
import useSignup from '@framework/use-signup'
import useSignup from '@framework/auth/use-signup'
interface Props {}

View File

@@ -2,43 +2,51 @@ import { ChangeEvent, useEffect, useState } from 'react'
import cn from 'classnames'
import Image from 'next/image'
import Link from 'next/link'
import s from './CartItem.module.css'
import { Trash, Plus, Minus } from '@components/icons'
import usePrice from '@framework/use-price'
import { useUI } from '@components/ui/context'
import type { LineItem } from '@framework/types'
import usePrice from '@framework/product/use-price'
import useUpdateItem from '@framework/cart/use-update-item'
import useRemoveItem from '@framework/cart/use-remove-item'
import s from './CartItem.module.css'
type ItemOption = {
name: string,
nameId: number,
value: string,
name: string
nameId: number
value: string
valueId: number
}
const CartItem = ({
item,
currencyCode,
...rest
}: {
item: any
item: LineItem
currencyCode: string
}) => {
const { closeSidebarIfPresent } = useUI()
const { price } = usePrice({
amount: item.extended_sale_price,
baseAmount: item.extended_list_price,
amount: item.variant.price * item.quantity,
baseAmount: item.variant.listPrice * item.quantity,
currencyCode,
})
const updateItem = useUpdateItem(item)
const updateItem = useUpdateItem({ item })
const removeItem = useRemoveItem()
const [quantity, setQuantity] = useState(item.quantity)
const [removing, setRemoving] = useState(false)
const updateQuantity = async (val: number) => {
await updateItem({ quantity: val })
}
const handleQuantity = (e: ChangeEvent<HTMLInputElement>) => {
const val = Number(e.target.value)
if (Number.isInteger(val) && val >= 0) {
setQuantity(e.target.value)
setQuantity(Number(e.target.value))
}
}
const handleBlur = () => {
@@ -62,11 +70,13 @@ const CartItem = ({
try {
// If this action succeeds then there's no need to do `setRemoving(true)`
// because the component will be removed from the view
await removeItem({ id: item.id })
await removeItem(item)
} catch (error) {
setRemoving(false)
}
}
// TODO: Add a type for this
const options = (item as any).options
useEffect(() => {
// Reset the quantity state if the item quantity changes
@@ -80,32 +90,38 @@ const CartItem = ({
className={cn('flex flex-row space-x-8 py-8', {
'opacity-75 pointer-events-none': removing,
})}
{...rest}
>
<div className="w-16 h-16 bg-violet relative overflow-hidden">
<Image
className={s.productImage}
src={item.image_url}
width={150}
height={150}
alt="Product Image"
// The cart item image is already optimized and very small in size
src={item.variant.image!.url}
alt={item.variant.image!.altText}
unoptimized
/>
</div>
<div className="flex-1 flex flex-col text-base">
{/** TODO: Replace this. No `path` found at Cart */}
<Link href={`/product/${item.url.split('/')[3]}`}>
<span className="font-bold text-lg cursor-pointer leading-6">
<Link href={`/product/${item.path}`}>
<span
className="font-bold text-lg cursor-pointer leading-6"
onClick={() => closeSidebarIfPresent()}
>
{item.name}
</span>
</Link>
{item.options && item.options.length > 0 ? (
{options && options.length > 0 ? (
<div className="">
{item.options.map((option:ItemOption, i: number) =>
<span key={`${item.id}-${option.name}`} className="text-sm font-semibold text-accents-7">
{option.value}{ i === item.options.length -1 ? "" : ", " }
{options.map((option: ItemOption, i: number) => (
<span
key={`${item.id}-${option.name}`}
className="text-sm font-semibold text-accents-7"
>
{option.value}
{i === options.length - 1 ? '' : ', '}
</span>
)}
))}
</div>
) : null}
<div className="flex items-center mt-3">
@@ -130,7 +146,10 @@ const CartItem = ({
</div>
<div className="flex flex-col justify-between space-y-2 text-base">
<span>{price}</span>
<button className="flex justify-end" onClick={handleRemove}>
<button
className="flex justify-end outline-none"
onClick={handleRemove}
>
<Trash />
</button>
</div>

View File

@@ -1,42 +1,40 @@
import { FC } from 'react'
import cn from 'classnames'
import { UserNav } from '@components/common'
import { Button } from '@components/ui'
import { Bag, Cross, Check } from '@components/icons'
import { useUI } from '@components/ui/context'
import useCart from '@framework/cart/use-cart'
import usePrice from '@framework/use-price'
import Link from 'next/link'
import CartItem from '../CartItem'
import s from './CartSidebarView.module.css'
import { Button } from '@components/ui'
import { UserNav } from '@components/common'
import { useUI } from '@components/ui/context'
import { Bag, Cross, Check } from '@components/icons'
import useCart from '@framework/cart/use-cart'
import usePrice from '@framework/product/use-price'
const CartSidebarView: FC = () => {
const { closeSidebar } = useUI()
const { data, isEmpty } = useCart()
const { data, isLoading, isEmpty } = useCart()
const { price: subTotal } = usePrice(
data && {
amount: data.base_amount,
amount: Number(data.subtotalPrice),
currencyCode: data.currency.code,
}
)
const { price: total } = usePrice(
data && {
amount: data.cart_amount,
amount: Number(data.totalPrice),
currencyCode: data.currency.code,
}
)
const handleClose = () => closeSidebar()
const items = data?.line_items.physical_items ?? []
const error = null
const success = null
return (
<div
className={cn(s.root, {
[s.empty]: error,
[s.empty]: success,
[s.empty]: isEmpty,
[s.empty]: error || success || isLoading || isEmpty,
})}
>
<header className="px-4 pt-6 pb-4 sm:px-6">
@@ -51,12 +49,12 @@ const CartSidebarView: FC = () => {
</button>
</div>
<div className="space-y-1">
<UserNav className="" />
<UserNav />
</div>
</div>
</header>
{isEmpty ? (
{isLoading || isEmpty ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-dashed border-primary rounded-full flex items-center justify-center w-16 h-16 p-12 bg-secondary text-secondary">
<Bag className="absolute" />
@@ -90,15 +88,20 @@ const CartSidebarView: FC = () => {
) : (
<>
<div className="px-4 sm:px-6 flex-1">
<h2 className="pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide">
My Cart
</h2>
<Link href="/cart">
<h2
className="pt-1 pb-4 text-2xl leading-7 font-bold text-base tracking-wide cursor-pointer inline-block"
onClick={handleClose}
>
My Cart
</h2>
</Link>
<ul className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-t border-accents-3">
{items.map((item: any) => (
{data!.lineItems.map((item: any) => (
<CartItem
key={item.id}
item={item}
currencyCode={data?.currency.code!}
currencyCode={data!.currency.code}
/>
))}
</ul>

View File

@@ -2,7 +2,7 @@ import { FC } from 'react'
import cn from 'classnames'
import Link from 'next/link'
import { useRouter } from 'next/router'
import type { Page } from '@framework/api/operations/get-all-pages'
import type { Page } from '@framework/common/get-all-pages'
import getSlug from '@lib/get-slug'
import { Github, Vercel } from '@components/icons'
import { Logo, Container } from '@components/ui'

View File

@@ -1,5 +1,6 @@
import { FC } from 'react'
import Link from 'next/link'
import type { Product } from '@commerce/types'
import { Grid } from '@components/ui'
import { ProductCard } from '@components/product'
import s from './HomeAllProductsGrid.module.css'
@@ -8,10 +9,14 @@ import { getCategoryPath, getDesignerPath } from '@lib/search'
interface Props {
categories?: any
brands?: any
newestProducts?: any
products?: Product[]
}
const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
const HomeAllProductsGrid: FC<Props> = ({
categories,
brands,
products = [],
}) => {
return (
<div className={s.root}>
<div className={s.asideWrapper}>
@@ -48,13 +53,15 @@ const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
</div>
<div className="flex-1">
<Grid layout="normal">
{newestProducts.map(({ node }: any) => (
{products.map((product) => (
<ProductCard
key={node.path}
product={node}
key={product.path}
product={product}
variant="simple"
imgWidth={480}
imgHeight={480}
imgProps={{
width: 480,
height: 480,
}}
/>
))}
</Grid>
@@ -63,4 +70,4 @@ const Head: FC<Props> = ({ categories, brands, newestProducts }) => {
)
}
export default Head
export default HomeAllProductsGrid

View File

@@ -43,7 +43,7 @@ const I18nWidget: FC = () => {
const currentLocale = locale || defaultLocale
return (
<ClickOutside active={display} onClick={() => setDisplay(false)} >
<ClickOutside active={display} onClick={() => setDisplay(false)}>
<nav className={s.root}>
<div
className="flex items-center relative"

View File

@@ -7,12 +7,11 @@ import { useUI } from '@components/ui/context'
import { Navbar, Footer } from '@components/common'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
import { CartSidebarView } from '@components/cart'
import CartSidebarView from '@components/cart/CartSidebarView'
import LoginView from '@components/auth/LoginView'
import { CommerceProvider } from '@framework'
import type { Page } from '@framework/api/operations/get-all-pages'
import type { Page } from '@framework/common/get-all-pages'
const Loading = () => (
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
@@ -28,10 +27,12 @@ const SignUpView = dynamic(
() => import('@components/auth/SignUpView'),
dynamicProps
)
const ForgotPassword = dynamic(
() => import('@components/auth/ForgotPassword'),
dynamicProps
)
const FeatureBar = dynamic(
() => import('@components/common/FeatureBar'),
dynamicProps
@@ -40,10 +41,14 @@ const FeatureBar = dynamic(
interface Props {
pageProps: {
pages?: Page[]
commerceFeatures: Record<string, boolean>
}
}
const Layout: FC<Props> = ({ children, pageProps }) => {
const Layout: FC<Props> = ({
children,
pageProps: { commerceFeatures, ...pageProps },
}) => {
const {
displaySidebar,
displayModal,
@@ -53,7 +58,6 @@ const Layout: FC<Props> = ({ children, pageProps }) => {
} = useUI()
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const { locale = 'en-US' } = useRouter()
return (
<CommerceProvider locale={locale}>
<div className={cn(s.root)}>

View File

@@ -1,66 +1,50 @@
import { FC, useState, useEffect } from 'react'
import { FC } from 'react'
import Link from 'next/link'
import s from './Navbar.module.css'
import { Logo, Container } from '@components/ui'
import { Searchbar, UserNav } from '@components/common'
import cn from 'classnames'
import throttle from 'lodash.throttle'
import NavbarRoot from './NavbarRoot'
import s from './Navbar.module.css'
const Navbar: FC = () => {
const [hasScrolled, setHasScrolled] = useState(false)
useEffect(() => {
const handleScroll = throttle(() => {
const offset = 0
const { scrollTop } = document.documentElement
const scrolled = scrollTop > offset
setHasScrolled(scrolled)
}, 200)
document.addEventListener('scroll', handleScroll)
return () => {
document.removeEventListener('scroll', handleScroll)
}
}, [])
return (
<div className={cn(s.root, { 'shadow-magical': hasScrolled })}>
<Container>
<div className="relative flex flex-row justify-between py-4 align-center md:py-6">
<div className="flex items-center flex-1">
<Link href="/">
<a className={s.logo} aria-label="Logo">
<Logo />
</a>
const Navbar: FC = () => (
<NavbarRoot>
<Container>
<div className="relative flex flex-row justify-between py-4 align-center md:py-6">
<div className="flex items-center flex-1">
<Link href="/">
<a className={s.logo} aria-label="Logo">
<Logo />
</a>
</Link>
<nav className="hidden ml-6 space-x-4 lg:block">
<Link href="/search">
<a className={s.link}>All</a>
</Link>
<nav className="hidden ml-6 space-x-4 lg:block">
<Link href="/search">
<a className={s.link}>All</a>
</Link>
<Link href="/search?q=clothes">
<a className={s.link}>Clothes</a>
</Link>
<Link href="/search?q=accessories">
<a className={s.link}>Accessories</a>
</Link>
</nav>
</div>
<div className="justify-center flex-1 hidden lg:flex">
<Searchbar />
</div>
<div className="flex justify-end flex-1 space-x-8">
<UserNav />
</div>
<Link href="/search?q=clothes">
<a className={s.link}>Clothes</a>
</Link>
<Link href="/search?q=accessories">
<a className={s.link}>Accessories</a>
</Link>
<Link href="/search?q=shoes">
<a className={s.link}>Shoes</a>
</Link>
</nav>
</div>
<div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar id="mobile-search" />
<div className="justify-center flex-1 hidden lg:flex">
<Searchbar />
</div>
</Container>
</div>
)
}
<div className="flex justify-end flex-1 space-x-8">
<UserNav />
</div>
</div>
<div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar id="mobile-search" />
</div>
</Container>
</NavbarRoot>
)
export default Navbar

View File

@@ -0,0 +1,33 @@
import { FC, useState, useEffect } from 'react'
import throttle from 'lodash.throttle'
import cn from 'classnames'
import s from './Navbar.module.css'
const NavbarRoot: FC = ({ children }) => {
const [hasScrolled, setHasScrolled] = useState(false)
useEffect(() => {
const handleScroll = throttle(() => {
const offset = 0
const { scrollTop } = document.documentElement
const scrolled = scrollTop > offset
if (hasScrolled !== scrolled) {
setHasScrolled(scrolled)
}
}, 200)
document.addEventListener('scroll', handleScroll)
return () => {
document.removeEventListener('scroll', handleScroll)
}
}, [hasScrolled])
return (
<div className={cn(s.root, { 'shadow-magical': hasScrolled })}>
{children}
</div>
)
}
export default NavbarRoot

View File

@@ -8,6 +8,7 @@ import { Avatar } from '@components/common'
import { Moon, Sun } from '@components/icons'
import { useUI } from '@components/ui/context'
import ClickOutside from '@lib/click-outside'
import useLogout from '@framework/auth/use-logout'
import {
disableBodyScroll,
@@ -15,8 +16,6 @@ import {
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
import useLogout from '@framework/use-logout'
interface DropdownMenuProps {
open?: boolean
}

View File

@@ -1,27 +1,26 @@
import { FC } from 'react'
import Link from 'next/link'
import cn from 'classnames'
import type { LineItem } from '@framework/types'
import useCart from '@framework/cart/use-cart'
import useCustomer from '@framework/use-customer'
import useCustomer from '@framework/customer/use-customer'
import { Avatar } from '@components/common'
import { Heart, Bag } from '@components/icons'
import { useUI } from '@components/ui/context'
import DropdownMenu from './DropdownMenu'
import s from './UserNav.module.css'
import { Avatar } from '@components/common'
interface Props {
className?: string
}
const countItem = (count: number, item: any) => count + item.quantity
const countItems = (count: number, items: any[]) =>
items.reduce(countItem, count)
const countItem = (count: number, item: LineItem) => count + item.quantity
const UserNav: FC<Props> = ({ className, children, ...props }) => {
const UserNav: FC<Props> = ({ className }) => {
const { data } = useCart()
const { data: customer } = useCustomer()
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
return (
<nav className={cn(s.root, className)}>
@@ -31,13 +30,15 @@ const UserNav: FC<Props> = ({ className, children, ...props }) => {
<Bag />
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
</li>
<li className={s.item}>
<Link href="/wishlist">
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
<Heart />
</a>
</Link>
</li>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<li className={s.item}>
<Link href="/wishlist">
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
<Heart />
</a>
</Link>
</li>
)}
<li className={s.item}>
{customer ? (
<DropdownMenu />

View File

@@ -0,0 +1,20 @@
const CreditCard = ({ ...props }) => {
return (
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
<path d="M1 10h22" />
</svg>
)
}
export default CreditCard

View File

@@ -0,0 +1,20 @@
const MapPin = ({ ...props }) => {
return (
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
)
}
export default MapPin

View File

@@ -1,15 +1,39 @@
const Vercel = ({ ...props }) => {
return (
<svg width="89" height="20" viewBox="0 0 89 20" fill="none" xmlns="http://www.w3.org/2000/svg" { ...props }>
<path d="M11.5625 0L23.125 20H0L11.5625 0Z" fill="currentColor"/>
<path d="M49.875 10.625C49.875 7.40625 47.5 5.15625 44.0937 5.15625C40.6875 5.15625 38.3125 7.40625 38.3125 10.625C38.3125 13.7812 40.875 16.0937 44.4062 16.0937C46.3438 16.0937 48.0938 15.375 49.2188 14.0625L47.0938 12.8437C46.4375 13.5 45.4688 13.9062 44.4062 13.9062C42.8438 13.9062 41.5 13.0625 41.0312 11.7812L40.9375 11.5625H49.7812C49.8438 11.25 49.875 10.9375 49.875 10.625ZM40.9062 9.6875L40.9688 9.5C41.375 8.15625 42.5625 7.34375 44.0625 7.34375C45.5938 7.34375 46.75 8.15625 47.1562 9.5L47.2188 9.6875H40.9062Z" fill="currentColor"/>
<path d="M83.5313 10.625C83.5313 7.40625 81.1563 5.15625 77.75 5.15625C74.3438 5.15625 71.9688 7.40625 71.9688 10.625C71.9688 13.7812 74.5313 16.0937 78.0625 16.0937C80 16.0937 81.75 15.375 82.875 14.0625L80.75 12.8437C80.0938 13.5 79.125 13.9062 78.0625 13.9062C76.5 13.9062 75.1563 13.0625 74.6875 11.7812L74.5938 11.5625H83.4375C83.5 11.25 83.5313 10.9375 83.5313 10.625ZM74.5625 9.6875L74.625 9.5C75.0313 8.15625 76.2188 7.34375 77.7188 7.34375C79.25 7.34375 80.4063 8.15625 80.8125 9.5L80.875 9.6875H74.5625Z" fill="currentColor"/>
<path d="M68.5313 8.84374L70.6563 7.62499C69.6563 6.06249 67.875 5.18749 65.7188 5.18749C62.3125 5.18749 59.9375 7.43749 59.9375 10.6562C59.9375 13.875 62.3125 16.125 65.7188 16.125C67.875 16.125 69.6563 15.25 70.6563 13.6875L68.5313 12.4687C67.9688 13.4062 66.9688 13.9375 65.7188 13.9375C63.75 13.9375 62.4375 12.625 62.4375 10.6562C62.4375 8.68749 63.75 7.37499 65.7188 7.37499C66.9375 7.37499 67.9688 7.90624 68.5313 8.84374Z" fill="currentColor"/>
<path d="M88.2188 1.75H85.7188V15.8125H88.2188V1.75Z" fill="currentColor"/>
<path d="M40.1563 1.75H37.2813L31.7813 11.25L26.2813 1.75H23.375L31.7813 16.25L40.1563 1.75Z" fill="currentColor"/>
<path d="M57.8438 8.0625C58.125 8.0625 58.4062 8.09375 58.6875 8.15625V5.5C56.5625 5.5625 54.5625 6.75 54.5625 8.21875V5.5H52.0625V15.8125H54.5625V11.3437C54.5625 9.40625 55.9062 8.0625 57.8438 8.0625Z" fill="currentColor"/>
</svg>
return (
<svg
width="89"
height="20"
viewBox="0 0 89 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M11.5625 0L23.125 20H0L11.5625 0Z" fill="currentColor" />
<path
d="M49.875 10.625C49.875 7.40625 47.5 5.15625 44.0937 5.15625C40.6875 5.15625 38.3125 7.40625 38.3125 10.625C38.3125 13.7812 40.875 16.0937 44.4062 16.0937C46.3438 16.0937 48.0938 15.375 49.2188 14.0625L47.0938 12.8437C46.4375 13.5 45.4688 13.9062 44.4062 13.9062C42.8438 13.9062 41.5 13.0625 41.0312 11.7812L40.9375 11.5625H49.7812C49.8438 11.25 49.875 10.9375 49.875 10.625ZM40.9062 9.6875L40.9688 9.5C41.375 8.15625 42.5625 7.34375 44.0625 7.34375C45.5938 7.34375 46.75 8.15625 47.1562 9.5L47.2188 9.6875H40.9062Z"
fill="currentColor"
/>
<path
d="M83.5313 10.625C83.5313 7.40625 81.1563 5.15625 77.75 5.15625C74.3438 5.15625 71.9688 7.40625 71.9688 10.625C71.9688 13.7812 74.5313 16.0937 78.0625 16.0937C80 16.0937 81.75 15.375 82.875 14.0625L80.75 12.8437C80.0938 13.5 79.125 13.9062 78.0625 13.9062C76.5 13.9062 75.1563 13.0625 74.6875 11.7812L74.5938 11.5625H83.4375C83.5 11.25 83.5313 10.9375 83.5313 10.625ZM74.5625 9.6875L74.625 9.5C75.0313 8.15625 76.2188 7.34375 77.7188 7.34375C79.25 7.34375 80.4063 8.15625 80.8125 9.5L80.875 9.6875H74.5625Z"
fill="currentColor"
/>
<path
d="M68.5313 8.84374L70.6563 7.62499C69.6563 6.06249 67.875 5.18749 65.7188 5.18749C62.3125 5.18749 59.9375 7.43749 59.9375 10.6562C59.9375 13.875 62.3125 16.125 65.7188 16.125C67.875 16.125 69.6563 15.25 70.6563 13.6875L68.5313 12.4687C67.9688 13.4062 66.9688 13.9375 65.7188 13.9375C63.75 13.9375 62.4375 12.625 62.4375 10.6562C62.4375 8.68749 63.75 7.37499 65.7188 7.37499C66.9375 7.37499 67.9688 7.90624 68.5313 8.84374Z"
fill="currentColor"
/>
<path
d="M88.2188 1.75H85.7188V15.8125H88.2188V1.75Z"
fill="currentColor"
/>
<path
d="M40.1563 1.75H37.2813L31.7813 11.25L26.2813 1.75H23.375L31.7813 16.25L40.1563 1.75Z"
fill="currentColor"
/>
<path
d="M57.8438 8.0625C58.125 8.0625 58.4062 8.09375 58.6875 8.15625V5.5C56.5625 5.5625 54.5625 6.75 54.5625 8.21875V5.5H52.0625V15.8125H54.5625V11.3437C54.5625 9.40625 55.9062 8.0625 57.8438 8.0625Z"
fill="currentColor"
/>
</svg>
)
}

View File

@@ -14,3 +14,5 @@ export { default as RightArrow } from './RightArrow'
export { default as Info } from './Info'
export { default as ChevronUp } from './ChevronUp'
export { default as Vercel } from './Vercel'
export { default as MapPin } from './MapPin'
export { default as CreditCard } from './CreditCard'

View File

@@ -1,103 +1,88 @@
import { FC } from 'react'
import cn from 'classnames'
import Link from 'next/link'
import Image from 'next/image'
import type { FC } from 'react'
import type { Product } from '@commerce/types'
import s from './ProductCard.module.css'
import Image, { ImageProps } from 'next/image'
import WishlistButton from '@components/wishlist/WishlistButton'
import usePrice from '@framework/use-price'
import type { ProductNode } from '@framework/api/operations/get-all-products'
interface Props {
className?: string
product: ProductNode
product: Product
variant?: 'slim' | 'simple'
imgWidth: number | string
imgHeight: number | string
imgLayout?: 'fixed' | 'intrinsic' | 'responsive' | undefined
imgPriority?: boolean
imgLoading?: 'eager' | 'lazy'
imgSizes?: string
imgProps?: Omit<ImageProps, 'src'>
}
const placeholderImg = '/product-img-placeholder.svg'
const ProductCard: FC<Props> = ({
className,
product: p,
product,
variant,
imgWidth,
imgHeight,
imgPriority,
imgLoading,
imgSizes,
imgLayout = 'responsive',
}) => {
const src = p.images.edges?.[0]?.node?.urlOriginal!
const placeholderImg = '/product-img-placeholder.svg';
const { price } = usePrice({
amount: p.prices?.price?.value,
baseAmount: p.prices?.retailPrice?.value,
currencyCode: p.prices?.price?.currencyCode!,
})
return (
<Link href={`/product${p.path}`}>
<a
className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}
>
{variant === 'slim' ? (
<div className="relative overflow-hidden box-border">
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
{p.name}
</span>
</div>
imgProps,
...props
}) => (
<Link href={`/product/${product.slug}`} {...props}>
<a className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}>
{variant === 'slim' ? (
<div className="relative overflow-hidden box-border">
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
{product.name}
</span>
</div>
{product?.images && (
<Image
quality="85"
width={imgWidth}
sizes={imgSizes}
height={imgHeight}
layout={imgLayout}
loading={imgLoading}
priority={imgPriority}
src={p.images.edges?.[0]?.node.urlOriginal! || placeholderImg}
alt={p.images.edges?.[0]?.node.altText || 'Product Image'}
src={product.images[0].url || placeholderImg}
alt={product.name || 'Product Image'}
height={320}
width={320}
layout="fixed"
{...imgProps}
/>
</div>
) : (
<>
<div className={s.squareBg} />
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
<div className="absolute top-0 left-0 pr-16 max-w-full">
<h3 className={s.productTitle}>
<span>{p.name}</span>
</h3>
<span className={s.productPrice}>{price}</span>
</div>
)}
</div>
) : (
<>
<div className={s.squareBg} />
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
<div className="absolute top-0 left-0 pr-16 max-w-full">
<h3 className={s.productTitle}>
<span>{product.name}</span>
</h3>
<span className={s.productPrice}>
{product.price.value}
&nbsp;
{product.price.currencyCode}
</span>
</div>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
productId={p.entityId}
variant={p.variants.edges?.[0]!}
productId={product.id}
variant={product.variants[0] as any}
/>
</div>
<div className={s.imageContainer}>
)}
</div>
<div className={s.imageContainer}>
{product?.images && (
<Image
quality="85"
src={src || placeholderImg}
alt={p.name}
alt={product.name || 'Product Image'}
className={s.productImage}
width={imgWidth}
sizes={imgSizes}
height={imgHeight}
layout={imgLayout}
loading={imgLoading}
priority={imgPriority}
src={product.images[0].url || placeholderImg}
height={540}
width={540}
quality="85"
layout="responsive"
{...imgProps}
/>
</div>
</>
)}
</a>
</Link>
)
}
)}
</div>
</>
)}
</a>
</Link>
)
export default ProductCard

View File

@@ -1,5 +1,12 @@
import { useKeenSlider } from 'keen-slider/react'
import React, { Children, FC, isValidElement, useState, useRef, useEffect } from 'react'
import React, {
Children,
FC,
isValidElement,
useState,
useRef,
useEffect,
} from 'react'
import cn from 'classnames'
import s from './ProductSlider.module.css'
@@ -25,7 +32,7 @@ const ProductSlider: FC = ({ children }) => {
const touchXPosition = event.touches[0].pageX
// Size of the touch area
const touchXRadius = event.touches[0].radiusX || 0
// We set a threshold (10px) on both sizes of the screen,
// if the touch area overlaps with the screen edges
// it's likely to trigger the navigation. We prevent the
@@ -33,15 +40,22 @@ const ProductSlider: FC = ({ children }) => {
if (
touchXPosition - touchXRadius < 10 ||
touchXPosition + touchXRadius > window.innerWidth - 10
) event.preventDefault()
)
event.preventDefault()
}
sliderContainerRef.current!
.addEventListener('touchstart', preventNavigation)
sliderContainerRef.current!.addEventListener(
'touchstart',
preventNavigation
)
return () => {
sliderContainerRef.current!
.removeEventListener('touchstart', preventNavigation)
if (sliderContainerRef.current) {
sliderContainerRef.current!.removeEventListener(
'touchstart',
preventNavigation
)
}
}
}, [])

View File

@@ -7,7 +7,7 @@
}
.productDisplay {
@apply relative flex px-0 pb-0 relative box-border col-span-1 bg-violet;
@apply relative flex px-0 pb-0 box-border col-span-1 bg-violet;
min-height: 600px;
@screen md {

View File

@@ -1,51 +1,48 @@
import { FC, useState } from 'react'
import cn from 'classnames'
import Image from 'next/image'
import { NextSeo } from 'next-seo'
import { FC, useState } from 'react'
import s from './ProductView.module.css'
import { useUI } from '@components/ui/context'
import { Swatch, ProductSlider } from '@components/product'
import { Button, Container, Text } from '@components/ui'
import usePrice from '@framework/use-price'
import useAddItem from '@framework/cart/use-add-item'
import type { ProductNode } from '@framework/api/operations/get-product'
import {
getCurrentVariant,
getProductOptions,
SelectedOptions,
} from '../helpers'
import { Swatch, ProductSlider } from '@components/product'
import { Button, Container, Text, useUI } from '@components/ui'
import type { Product } from '@commerce/types'
import usePrice from '@framework/product/use-price'
import { useAddItem } from '@framework/cart'
import { getVariant, SelectedOptions } from '../helpers'
import WishlistButton from '@components/wishlist/WishlistButton'
interface Props {
className?: string
children?: any
product: ProductNode
product: Product
}
const ProductView: FC<Props> = ({ product }) => {
const addItem = useAddItem()
const { price } = usePrice({
amount: product.prices?.price?.value,
baseAmount: product.prices?.retailPrice?.value,
currencyCode: product.prices?.price?.currencyCode!,
amount: product.price.value,
baseAmount: product.price.retailPrice,
currencyCode: product.price.currencyCode!,
})
const { openSidebar } = useUI()
const options = getProductOptions(product)
const [loading, setLoading] = useState(false)
const [choices, setChoices] = useState<SelectedOptions>({
size: null,
color: null,
})
const variant = getCurrentVariant(product, choices)
// Select the correct variant based on choices
const variant = getVariant(product, choices)
const addToCart = async () => {
setLoading(true)
try {
await addItem({
productId: product.entityId,
variantId: variant?.node.entityId!,
productId: String(product.id),
variantId: String(variant ? variant.id : product.variants[0].id),
})
openSidebar()
setLoading(false)
@@ -65,7 +62,7 @@ const ProductView: FC<Props> = ({ product }) => {
description: product.description,
images: [
{
url: product.images.edges?.[0]?.node.urlOriginal!,
url: product.images[0]?.url!,
width: 800,
height: 600,
alt: product.name,
@@ -80,18 +77,18 @@ const ProductView: FC<Props> = ({ product }) => {
<div className={s.price}>
{price}
{` `}
{product.prices?.price.currencyCode}
{product.price?.currencyCode}
</div>
</div>
<div className={s.sliderContainer}>
<ProductSlider key={product.entityId}>
{product.images.edges?.map((image, i) => (
<div key={image?.node.urlOriginal} className={s.imageContainer}>
<ProductSlider key={product.id}>
{product.images.map((image, i) => (
<div key={image.url} className={s.imageContainer}>
<Image
className={s.img}
src={image?.node.urlOriginal!}
alt={image?.node.altText || 'Product Image'}
src={image.url!}
alt={image.alt || 'Product Image'}
width={1050}
height={1050}
priority={i === 0}
@@ -102,20 +99,21 @@ const ProductView: FC<Props> = ({ product }) => {
</ProductSlider>
</div>
</div>
<div className={s.sidebar}>
<section>
{options?.map((opt: any) => (
{product.options?.map((opt) => (
<div className="pb-4" key={opt.displayName}>
<h2 className="uppercase font-medium">{opt.displayName}</h2>
<div className="flex flex-row py-4">
{opt.values.map((v: any, i: number) => {
const active = (choices as any)[opt.displayName]
{opt.values.map((v, i: number) => {
const active = (choices as any)[
opt.displayName.toLowerCase()
]
return (
<Swatch
key={`${v.entityId}-${i}`}
active={v.label === active}
key={`${opt.id}-${i}`}
active={v.label.toLowerCase() === active}
variant={opt.displayName}
color={v.hexColors ? v.hexColors[0] : ''}
label={v.label}
@@ -123,7 +121,7 @@ const ProductView: FC<Props> = ({ product }) => {
setChoices((choices) => {
return {
...choices,
[opt.displayName]: v.label,
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
}
})
}}
@@ -145,18 +143,19 @@ const ProductView: FC<Props> = ({ product }) => {
className={s.button}
onClick={addToCart}
loading={loading}
disabled={!variant}
disabled={!variant && product.options.length > 0}
>
Add to Cart
</Button>
</div>
</div>
<WishlistButton
className={s.wishlistButton}
productId={product.entityId}
variant={variant!}
/>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<WishlistButton
className={s.wishlistButton}
productId={product.id}
variant={product.variants[0]! as any}
/>
)}
</div>
</Container>
)

View File

@@ -1,4 +1,5 @@
.root {
composes: root from 'components/ui/Button/Button.module.css';
@apply h-12 w-12 bg-primary text-primary rounded-full mr-3 inline-flex
items-center justify-center cursor-pointer transition duration-150 ease-in-out
p-0 shadow-none border-gray-200 border box-border;

View File

@@ -13,7 +13,7 @@ interface Props {
color?: string
}
const Swatch: FC<Props & ButtonProps> = ({
const Swatch: FC<Omit<ButtonProps, 'variant'> & Props> = ({
className,
color = '',
label,

View File

@@ -1,56 +1,22 @@
import type { ProductNode } from '@framework/api/operations/get-product'
import type { Product } from '@commerce/types'
export type SelectedOptions = {
size: string | null
color: string | null
}
export type ProductOption = {
displayName: string
values: any
}
// Returns the available options of a product
export function getProductOptions(product: ProductNode) {
const options = product.productOptions.edges?.reduce<ProductOption[]>(
(arr, edge) => {
if (edge?.node.__typename === 'MultipleChoiceOption') {
arr.push({
displayName: edge.node.displayName.toLowerCase(),
values: edge.node.values.edges?.map((edge) => edge?.node),
})
}
return arr
},
[]
)
return options
}
// Finds a variant in the product that matches the selected options
export function getCurrentVariant(product: ProductNode, opts: SelectedOptions) {
const variant = product.variants.edges?.find((edge) => {
const { node } = edge ?? {}
const numberOfDefinedOpts = Object.values(opts).filter(value => value !== null).length;
const numberOfEdges = node?.productOptions?.edges?.length;
const isEdgeEqualToOption = ([key, value]:[string, string | null]) =>
node?.productOptions.edges?.find((edge) => {
export function getVariant(product: Product, opts: SelectedOptions) {
const variant = product.variants.find((variant) => {
return Object.entries(opts).every(([key, value]) =>
variant.options.find((option) => {
if (
edge?.node.__typename === 'MultipleChoiceOption' &&
edge.node.displayName.toLowerCase() === key
option.__typename === 'MultipleChoiceOption' &&
option.displayName.toLowerCase() === key.toLowerCase()
) {
return edge.node.values.edges?.find(
(valueEdge) => valueEdge?.node.label === value
)
return option.values.find((v) => v.label.toLowerCase() === value)
}
});
return numberOfDefinedOpts === numberOfEdges ?
Object.entries(opts).every(isEdgeEqualToOption)
: Object.entries(opts).some(isEdgeEqualToOption)
})
)
})
return variant ?? product.variants.edges?.[0]
return variant
}

View File

@@ -13,9 +13,9 @@ const Container: FC<Props> = ({ children, className, el = 'div', clean }) => {
'mx-auto max-w-8xl px-6': !clean,
})
let Component: React.ComponentType<React.HTMLAttributes<
HTMLDivElement
>> = el as any
let Component: React.ComponentType<
React.HTMLAttributes<HTMLDivElement>
> = el as any
return <Component className={rootClassName}>{children}</Component>
}

View File

@@ -1,15 +1,16 @@
.root {
@apply w-full;
@apply w-full relative;
height: 320px;
min-width: 100%;
}
.container {
@apply flex flex-row items-center;
}
& > * {
@apply flex-1 px-16 py-4;
width: 430px;
}
.container > * {
@apply relative flex-1 px-16 py-4 h-full;
min-height: 320px;
}
.primary {

View File

@@ -8,6 +8,7 @@ export interface State {
displayToast: boolean
modalView: string
toastText: string
userAvatar: string
}
const initialState = {
@@ -17,6 +18,7 @@ const initialState = {
modalView: 'LOGIN_VIEW',
displayToast: false,
toastText: '',
userAvatar: '',
}
type Action =
@@ -55,9 +57,14 @@ type Action =
| {
type: 'SET_USER_AVATAR'
value: string
}
}
type MODAL_VIEWS = 'SIGNUP_VIEW' | 'LOGIN_VIEW' | 'FORGOT_VIEW'
type MODAL_VIEWS =
| 'SIGNUP_VIEW'
| 'LOGIN_VIEW'
| 'FORGOT_VIEW'
| 'NEW_SHIPPING_ADDRESS'
| 'NEW_PAYMENT_METHOD'
type ToastText = string
export const UIContext = React.createContext<State | any>(initialState)
@@ -157,7 +164,8 @@ export const UIProvider: FC = (props) => {
const openToast = () => dispatch({ type: 'OPEN_TOAST' })
const closeToast = () => dispatch({ type: 'CLOSE_TOAST' })
const setUserAvatar = (value: string) => dispatch({ type: 'SET_USER_AVATAR', value })
const setUserAvatar = (value: string) =>
dispatch({ type: 'SET_USER_AVATAR', value })
const setModalView = (view: MODAL_VIEWS) =>
dispatch({ type: 'SET_MODAL_VIEW', view })
@@ -176,7 +184,7 @@ export const UIProvider: FC = (props) => {
setModalView,
openToast,
closeToast,
setUserAvatar
setUserAvatar,
}),
[state]
)

View File

@@ -10,3 +10,4 @@ export { default as Skeleton } from './Skeleton'
export { default as Modal } from './Modal'
export { default as Text } from './Text'
export { default as Input } from './Input'
export { useUI } from './context'

View File

@@ -1,16 +1,16 @@
import React, { FC, useState } from 'react'
import cn from 'classnames'
import type { ProductNode } from '@framework/api/operations/get-all-products'
import useAddItem from '@framework/wishlist/use-add-item'
import useRemoveItem from '@framework/wishlist/use-remove-item'
import useWishlist from '@framework/wishlist/use-wishlist'
import useCustomer from '@framework/use-customer'
import { useUI } from '@components/ui'
import { Heart } from '@components/icons'
import { useUI } from '@components/ui/context'
import useAddItem from '@framework/wishlist/use-add-item'
import useCustomer from '@framework/customer/use-customer'
import useWishlist from '@framework/wishlist/use-wishlist'
import useRemoveItem from '@framework/wishlist/use-remove-item'
import type { Product, ProductVariant } from '@commerce/types'
type Props = {
productId: number
variant: NonNullable<ProductNode['variants']['edges']>[0]
productId: Product['id']
variant: ProductVariant
} & React.ButtonHTMLAttributes<HTMLButtonElement>
const WishlistButton: FC<Props> = ({
@@ -19,16 +19,19 @@ const WishlistButton: FC<Props> = ({
className,
...props
}) => {
const { data } = useWishlist()
const addItem = useAddItem()
const removeItem = useRemoveItem()
const { data } = useWishlist()
const { data: customer } = useCustomer()
const [loading, setLoading] = useState(false)
const { openModal, setModalView } = useUI()
const [loading, setLoading] = useState(false)
// @ts-ignore Wishlist is not always enabled
const itemInWishlist = data?.items?.find(
// @ts-ignore Wishlist is not always enabled
(item) =>
item.product_id === productId &&
item.variant_id === variant?.node.entityId
item.product_id === Number(productId) &&
(item.variant_id as any) === Number(variant.id)
)
const handleWishlistChange = async (e: any) => {
@@ -50,7 +53,7 @@ const WishlistButton: FC<Props> = ({
} else {
await addItem({
productId,
variantId: variant?.node.entityId!,
variantId: variant?.id!,
})
}

View File

@@ -2,27 +2,28 @@ import { FC, useState } from 'react'
import cn from 'classnames'
import Link from 'next/link'
import Image from 'next/image'
import type { WishlistItem } from '@framework/api/wishlist'
import usePrice from '@framework/use-price'
import useRemoveItem from '@framework/wishlist/use-remove-item'
import useAddItem from '@framework/cart/use-add-item'
import { useUI } from '@components/ui/context'
import { Button, Text } from '@components/ui'
import { Trash } from '@components/icons'
import s from './WishlistCard.module.css'
import { Trash } from '@components/icons'
import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context'
import type { Product } from '@commerce/types'
import usePrice from '@framework/product/use-price'
import useAddItem from '@framework/cart/use-add-item'
import useRemoveItem from '@framework/wishlist/use-remove-item'
interface Props {
item: WishlistItem
product: Product
}
const WishlistCard: FC<Props> = ({ item }) => {
const product = item.product!
const WishlistCard: FC<Props> = ({ product }) => {
const { price } = usePrice({
amount: product.prices?.price?.value,
baseAmount: product.prices?.retailPrice?.value,
currencyCode: product.prices?.price?.currencyCode!,
})
const removeItem = useRemoveItem({ includeProducts: true })
// @ts-ignore Wishlist is not always enabled
const removeItem = useRemoveItem({ wishlist: { includeProducts: true } })
const [loading, setLoading] = useState(false)
const [removing, setRemoving] = useState(false)
const addItem = useAddItem()
@@ -34,7 +35,7 @@ const WishlistCard: FC<Props> = ({ item }) => {
try {
// If this action succeeds then there's no need to do `setRemoving(true)`
// because the component will be removed from the view
await removeItem({ id: item.id! })
await removeItem({ id: product.id! })
} catch (error) {
setRemoving(false)
}
@@ -43,8 +44,8 @@ const WishlistCard: FC<Props> = ({ item }) => {
setLoading(true)
try {
await addItem({
productId: product.entityId,
variantId: product.variants.edges?.[0]?.node.entityId!,
productId: String(product.id),
variantId: String(product.variants[0].id),
})
openSidebar()
setLoading(false)
@@ -57,10 +58,10 @@ const WishlistCard: FC<Props> = ({ item }) => {
<div className={cn(s.root, { 'opacity-75 pointer-events-none': removing })}>
<div className={`col-span-3 ${s.productBg}`}>
<Image
src={product.images.edges?.[0]?.node.urlOriginal!}
src={product.images[0].url}
width={400}
height={400}
alt={product.images.edges?.[0]?.node.altText || 'Product Image'}
alt={product.images[0].alt || 'Product Image'}
/>
</div>

View File

@@ -1 +1,2 @@
export { default as WishlistCard } from './WishlistCard'
export { default as WishlistButton } from './WishlistButton'