Merge pull request #3 from ramyareye/woocommerce-framework

Woocommerce framework
This commit is contained in:
Reza Babaei 2021-09-18 22:14:29 +03:00 committed by GitHub
commit ff26d9d1c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
139 changed files with 79838 additions and 1028 deletions

View File

@ -1,4 +1,4 @@
# Available providers: local, bigcommerce, shopify, swell, saleor
# Available providers: local, bigcommerce, shopify, swell, saleor, woocommerce
COMMERCE_PROVIDER=
BIGCOMMERCE_STOREFRONT_API_URL=
@ -23,3 +23,6 @@ NEXT_PUBLIC_SALEOR_CHANNEL=
NEXT_PUBLIC_VENDURE_SHOP_API_URL=
NEXT_PUBLIC_VENDURE_LOCAL_URL=
NEXT_PUBLIC_WOOCOMMERCE_SHOP_API_URL=
NEXT_PUBLIC_WOOCOMMERCE_IMAGES_DOMAIN=

2
.gitignore vendored
View File

@ -34,3 +34,5 @@ yarn-error.log*
# vercel
.vercel
.vscode

12
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach to application",
"skipFiles": ["<node_internals>/**"],
"port": 9229
}
]
}

View File

@ -1,6 +1,6 @@
import { FC, useEffect, useState, useCallback } from 'react'
import { Logo, Button, Input } from '@components/ui'
import useLogin from '@framework/auth/use-login'
// import useLogin from '@framework/auth/use-login'
import { useUI } from '@components/ui/context'
import { validate } from 'email-validator'
@ -16,7 +16,7 @@ const LoginView: FC<Props> = () => {
const [disabled, setDisabled] = useState(false)
const { setModalView, closeModal } = useUI()
const login = useLogin()
// const login = useLogin()
const handleLogin = async (e: React.SyntheticEvent<EventTarget>) => {
e.preventDefault()
@ -29,14 +29,14 @@ const LoginView: FC<Props> = () => {
try {
setLoading(true)
setMessage('')
await login({
email,
password,
})
// await login({
// email,
// password,
// })
setLoading(false)
closeModal()
} catch ({ errors }) {
setMessage(errors[0].message)
// setMessage(errors[0].message)
setLoading(false)
setDisabled(false)
}
@ -59,17 +59,17 @@ const LoginView: FC<Props> = () => {
return (
<form
onSubmit={handleLogin}
className="w-80 flex flex-col justify-between p-3"
className="flex flex-col justify-between p-3 w-80"
>
<div className="flex justify-center pb-12 ">
<Logo width="64px" height="64px" />
</div>
<div className="flex flex-col space-y-3">
{message && (
<div className="text-red border border-red p-3">
<div className="p-3 border text-red border-red">
{message}. Did you {` `}
<a
className="text-accent-9 inline font-bold hover:underline cursor-pointer"
className="inline font-bold cursor-pointer text-accent-9 hover:underline"
onClick={() => setModalView('FORGOT_VIEW')}
>
forgot your password?
@ -87,11 +87,11 @@ const LoginView: FC<Props> = () => {
>
Log In
</Button>
<div className="pt-1 text-center text-sm">
<div className="pt-1 text-sm text-center">
<span className="text-accent-7">Don't have an account?</span>
{` `}
<a
className="text-accent-9 font-bold hover:underline cursor-pointer"
className="font-bold cursor-pointer text-accent-9 hover:underline"
onClick={() => setModalView('SIGNUP_VIEW')}
>
Sign Up

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/auth/use-signup'
// import useSignup from '@framework/auth/use-signup'
interface Props {}
@ -18,7 +18,7 @@ const SignUpView: FC<Props> = () => {
const [dirty, setDirty] = useState(false)
const [disabled, setDisabled] = useState(false)
const signup = useSignup()
// const signup = useSignup()
const { setModalView, closeModal } = useUI()
const handleSignup = async (e: React.SyntheticEvent<EventTarget>) => {
@ -32,12 +32,12 @@ const SignUpView: FC<Props> = () => {
try {
setLoading(true)
setMessage('')
await signup({
email,
firstName,
lastName,
password,
})
// await signup({
// email,
// firstName,
// lastName,
// password,
// })
setLoading(false)
closeModal()
} catch ({ errors }) {
@ -63,14 +63,14 @@ const SignUpView: FC<Props> = () => {
return (
<form
onSubmit={handleSignup}
className="w-80 flex flex-col justify-between p-3"
className="flex flex-col justify-between p-3 w-80"
>
<div className="flex justify-center pb-12 ">
<Logo width="64px" height="64px" />
</div>
<div className="flex flex-col space-y-4">
{message && (
<div className="text-red border border-red p-3">{message}</div>
<div className="p-3 border text-red border-red">{message}</div>
)}
<Input placeholder="First Name" onChange={setFirstName} />
<Input placeholder="Last Name" onChange={setLastName} />
@ -80,12 +80,12 @@ const SignUpView: FC<Props> = () => {
<span className="inline-block align-middle ">
<Info width="15" height="15" />
</span>{' '}
<span className="leading-6 text-sm">
<span className="text-sm leading-6">
<strong>Info</strong>: Passwords must be longer than 7 chars and
include numbers.{' '}
</span>
</span>
<div className="pt-2 w-full flex flex-col">
<div className="flex flex-col w-full pt-2">
<Button
variant="slim"
type="submit"
@ -96,11 +96,11 @@ const SignUpView: FC<Props> = () => {
</Button>
</div>
<span className="pt-1 text-center text-sm">
<span className="pt-1 text-sm text-center">
<span className="text-accent-7">Do you have an account?</span>
{` `}
<a
className="text-accent-9 font-bold hover:underline cursor-pointer"
className="font-bold cursor-pointer text-accent-9 hover:underline"
onClick={() => setModalView('LOGIN_VIEW')}
>
Log In

View File

@ -6,9 +6,9 @@ import s from './CartItem.module.css'
import { Trash, Plus, Minus, Cross } from '@components/icons'
import { useUI } from '@components/ui/context'
import type { LineItem } from '@commerce/types/cart'
import usePrice from '@framework/product/use-price'
import useUpdateItem from '@framework/cart/use-update-item'
import useRemoveItem from '@framework/cart/use-remove-item'
// import usePrice from '@framework/product/use-price'
// import useUpdateItem from '@framework/cart/use-update-item'
// import useRemoveItem from '@framework/cart/use-remove-item'
import Quantity from '@components/ui/Quantity'
type ItemOption = {
@ -31,36 +31,36 @@ const CartItem = ({
const { closeSidebarIfPresent } = useUI()
const [removing, setRemoving] = useState(false)
const [quantity, setQuantity] = useState<number>(item.quantity)
const removeItem = useRemoveItem()
const updateItem = useUpdateItem({ item })
// const removeItem = useRemoveItem()
// const updateItem = useUpdateItem({ item })
const { price } = usePrice({
amount: item.variant.price * item.quantity,
baseAmount: item.variant.listPrice * item.quantity,
currencyCode,
})
// const { price } = usePrice({
// amount: item.variant.price * item.quantity,
// baseAmount: item.variant.listPrice * item.quantity,
// currencyCode,
// })
const handleChange = async ({
target: { value },
}: ChangeEvent<HTMLInputElement>) => {
setQuantity(Number(value))
await updateItem({ quantity: Number(value) })
}
// const handleChange = async ({
// target: { value },
// }: ChangeEvent<HTMLInputElement>) => {
// setQuantity(Number(value))
// await updateItem({ quantity: Number(value) })
// }
const increaseQuantity = async (n = 1) => {
const val = Number(quantity) + n
setQuantity(val)
await updateItem({ quantity: val })
}
// const increaseQuantity = async (n = 1) => {
// const val = Number(quantity) + n
// setQuantity(val)
// await updateItem({ quantity: val })
// }
const handleRemove = async () => {
setRemoving(true)
try {
await removeItem(item)
} catch (error) {
setRemoving(false)
}
}
// const handleRemove = async () => {
// setRemoving(true)
// try {
// await removeItem(item)
// } catch (error) {
// setRemoving(false)
// }
// }
// TODO: Add a type for this
const options = (item as any).options
@ -82,8 +82,8 @@ const CartItem = ({
})}
{...rest}
>
<div className="flex flex-row space-x-4 py-4">
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer z-0">
<div className="flex flex-row py-4 space-x-4">
<div className="relative z-0 w-16 h-16 overflow-hidden cursor-pointer bg-violet">
<Link href={`/product/${item.path}`}>
<Image
onClick={() => closeSidebarIfPresent()}
@ -96,7 +96,7 @@ const CartItem = ({
/>
</Link>
</div>
<div className="flex-1 flex flex-col text-base">
<div className="flex flex-col flex-1 text-base">
<Link href={`/product/${item.path}`}>
<span
className={s.productName}
@ -110,18 +110,18 @@ const CartItem = ({
{options.map((option: ItemOption, i: number) => (
<div
key={`${item.id}-${option.name}`}
className="text-sm font-semibold text-accent-7 inline-flex items-center justify-center"
className="inline-flex items-center justify-center text-sm font-semibold text-accent-7"
>
{option.name}
{option.name === 'Color' ? (
<span
className="mx-2 rounded-full bg-transparent border w-5 h-5 p-1 text-accent-9 inline-flex items-center justify-center overflow-hidden"
className="inline-flex items-center justify-center w-5 h-5 p-1 mx-2 overflow-hidden bg-transparent border rounded-full text-accent-9"
style={{
backgroundColor: `${option.value}`,
}}
></span>
) : (
<span className="mx-2 rounded-full bg-transparent border h-5 p-1 text-accent-9 inline-flex items-center justify-center overflow-hidden">
<span className="inline-flex items-center justify-center h-5 p-1 mx-2 overflow-hidden bg-transparent border rounded-full text-accent-9">
{option.value}
</span>
)}
@ -135,10 +135,10 @@ const CartItem = ({
)}
</div>
<div className="flex flex-col justify-between space-y-2 text-sm">
<span>{price}</span>
{/* <span>{price}</span> */}
</div>
</div>
{variant === 'default' && (
{/* {variant === 'default' && (
<Quantity
value={quantity}
handleRemove={handleRemove}
@ -146,7 +146,7 @@ const CartItem = ({
increase={() => increaseQuantity(1)}
decrease={() => increaseQuantity(-1)}
/>
)}
)} */}
</li>
)
}

View File

@ -6,124 +6,33 @@ import CartItem from '../CartItem'
import { Button, Text } from '@components/ui'
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'
// import useCart from '@framework/cart/use-cart'
// import usePrice from '@framework/product/use-price'
import SidebarLayout from '@components/common/SidebarLayout'
const CartSidebarView: FC = () => {
const { closeSidebar, setSidebarView } = useUI()
const { data, isLoading, isEmpty } = useCart()
// const { data, isLoading, isEmpty } = useCart()
const { price: subTotal } = usePrice(
data && {
amount: Number(data.subtotalPrice),
currencyCode: data.currency.code,
}
)
const { price: total } = usePrice(
data && {
amount: Number(data.totalPrice),
currencyCode: data.currency.code,
}
)
const handleClose = () => closeSidebar()
const goToCheckout = () => setSidebarView('CHECKOUT_VIEW')
// const { price: subTotal } = usePrice(
// data && {
// amount: Number(data.subtotalPrice),
// currencyCode: data.currency.code,
// }
// )
// const { price: total } = usePrice(
// data && {
// amount: Number(data.totalPrice),
// currencyCode: data.currency.code,
// }
// )
// const handleClose = () => closeSidebar()
// const goToCheckout = () => setSidebarView('CHECKOUT_VIEW')
const error = null
const success = null
// const error = null
// const success = null
return (
<SidebarLayout
className={cn({
[s.empty]: error || success || isLoading || isEmpty,
})}
handleClose={handleClose}
>
{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" />
</span>
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
Your cart is empty
</h2>
<p className="text-accent-3 px-10 text-center pt-2">
Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
</p>
</div>
) : error ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
<Cross width={24} height={24} />
</span>
<h2 className="pt-6 text-xl font-light text-center">
We couldnt process the purchase. Please check your card information
and try again.
</h2>
</div>
) : success ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
<Check />
</span>
<h2 className="pt-6 text-xl font-light text-center">
Thank you for your order.
</h2>
</div>
) : (
<>
<div className="px-4 sm:px-6 flex-1">
<Link href="/cart">
<Text variant="sectionHeading" onClick={handleClose}>
My Cart
</Text>
</Link>
<ul className={s.lineItemsList}>
{data!.lineItems.map((item: any) => (
<CartItem
key={item.id}
item={item}
currencyCode={data!.currency.code}
/>
))}
</ul>
</div>
<div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm">
<ul className="pb-2">
<li className="flex justify-between py-1">
<span>Subtotal</span>
<span>{subTotal}</span>
</li>
<li className="flex justify-between py-1">
<span>Taxes</span>
<span>Calculated at checkout</span>
</li>
<li className="flex justify-between py-1">
<span>Shipping</span>
<span className="font-bold tracking-wide">FREE</span>
</li>
</ul>
<div className="flex justify-between border-t border-accent-2 py-3 font-bold mb-2">
<span>Total</span>
<span>{total}</span>
</div>
<div>
{process.env.COMMERCE_CUSTOMCHECKOUT_ENABLED ? (
<Button Component="a" width="100%" onClick={goToCheckout}>
Proceed to Checkout ({total})
</Button>
) : (
<Button href="/checkout" Component="a" width="100%">
Proceed to Checkout
</Button>
)}
</div>
</div>
</>
)}
</SidebarLayout>
)
return <div />
}
export default CartSidebarView

View File

@ -4,8 +4,8 @@ import { FC } from 'react'
import CartItem from '@components/cart/CartItem'
import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context'
import useCart from '@framework/cart/use-cart'
import usePrice from '@framework/product/use-price'
// import useCart from '@framework/cart/use-cart'
// import usePrice from '@framework/product/use-price'
import ShippingWidget from '../ShippingWidget'
import PaymentWidget from '../PaymentWidget'
import SidebarLayout from '@components/common/SidebarLayout'
@ -13,27 +13,27 @@ import s from './CheckoutSidebarView.module.css'
const CheckoutSidebarView: FC = () => {
const { setSidebarView } = useUI()
const { data } = useCart()
// const { data } = useCart()
const { price: subTotal } = usePrice(
data && {
amount: Number(data.subtotalPrice),
currencyCode: data.currency.code,
}
)
const { price: total } = usePrice(
data && {
amount: Number(data.totalPrice),
currencyCode: data.currency.code,
}
)
// const { price: subTotal } = usePrice(
// data && {
// amount: Number(data.subtotalPrice),
// currencyCode: data.currency.code,
// }
// )
// const { price: total } = usePrice(
// data && {
// amount: Number(data.totalPrice),
// currencyCode: data.currency.code,
// }
// )
return (
<SidebarLayout
className={s.root}
handleBack={() => setSidebarView('CART_VIEW')}
>
<div className="px-4 sm:px-6 flex-1">
<div className="flex-1 px-4 sm:px-6">
<Link href="/cart">
<Text variant="sectionHeading">Checkout</Text>
</Link>
@ -42,22 +42,22 @@ const CheckoutSidebarView: FC = () => {
<ShippingWidget onClick={() => setSidebarView('SHIPPING_VIEW')} />
<ul className={s.lineItemsList}>
{data!.lineItems.map((item: any) => (
{/* {data!.lineItems.map((item: any) => (
<CartItem
key={item.id}
item={item}
currencyCode={data!.currency.code}
variant="display"
/>
))}
))} */}
</ul>
</div>
<div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm">
<div className="sticky bottom-0 left-0 right-0 z-20 flex-shrink-0 w-full px-6 py-6 text-sm border-t sm:px-6 bg-accent-0">
<ul className="pb-2">
<li className="flex justify-between py-1">
<span>Subtotal</span>
<span>{subTotal}</span>
{/* <span>{subTotal}</span> */}
</li>
<li className="flex justify-between py-1">
<span>Taxes</span>
@ -68,9 +68,9 @@ const CheckoutSidebarView: FC = () => {
<span className="font-bold tracking-wide">FREE</span>
</li>
</ul>
<div className="flex justify-between border-t border-accent-2 py-3 font-bold mb-2">
<div className="flex justify-between py-3 mb-2 font-bold border-t border-accent-2">
<span>Total</span>
<span>{total}</span>
{/* <span>{total}</span> */}
</div>
<div>
{/* Once data is correcly filled */}

View File

@ -18,7 +18,7 @@ import LoginView from '@components/auth/LoginView'
import s from './Layout.module.css'
const Loading = () => (
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
<div className="flex items-center justify-center p-3 text-center w-80 h-80">
<LoadingDots />
</div>
)
@ -104,7 +104,7 @@ const Layout: FC<Props> = ({
return (
<CommerceProvider locale={locale}>
<div className={cn(s.root)}>
<Navbar links={navBarlinks} />
{/* <Navbar links={navBarlinks} /> */}
<main className="fit">{children}</main>
<Footer pages={pageProps.pages} />
<ModalUI />

View File

@ -8,7 +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 useLogout from '@framework/auth/use-logout'
import {
disableBodyScroll,
@ -36,7 +36,7 @@ const LINKS = [
]
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
const logout = useLogout()
// const logout = useLogout()
const { pathname } = useRouter()
const { theme, setTheme } = useTheme()
const [display, setDisplay] = useState(false)
@ -110,7 +110,7 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
<li>
<a
className={cn(s.link, 'border-t border-accent-2 mt-4')}
onClick={() => logout()}
// onClick={() => logout()}
>
Logout
</a>

View File

@ -2,8 +2,8 @@ import { FC } from 'react'
import Link from 'next/link'
import cn from 'classnames'
import type { LineItem } from '@commerce/types/cart'
import useCart from '@framework/cart/use-cart'
import useCustomer from '@framework/customer/use-customer'
// import useCart from '@framework/cart/use-cart'
// import useCustomer from '@framework/customer/use-customer'
import { Avatar } from '@components/common'
import { Heart, Bag } from '@components/icons'
import { useUI } from '@components/ui/context'
@ -18,19 +18,24 @@ interface Props {
const countItem = (count: number, item: LineItem) => count + item.quantity
const UserNav: FC<Props> = ({ className }) => {
const { data } = useCart()
const { data: customer } = useCustomer()
// const { data } = useCart()
// const { data: customer } = useCustomer()
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
// const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
return (
<nav className={cn(s.root, className)}>
<ul className={s.list}>
{process.env.COMMERCE_CART_ENABLED && (
<li className={s.item}>
<Button className={s.item} variant="naked" onClick={toggleSidebar} aria-label="Cart">
<Button
className={s.item}
variant="naked"
onClick={toggleSidebar}
aria-label="Cart"
>
<Bag />
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
{/* {itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>} */}
</Button>
</li>
)}
@ -45,17 +50,17 @@ const UserNav: FC<Props> = ({ className }) => {
)}
{process.env.COMMERCE_CUSTOMERAUTH_ENABLED && (
<li className={s.item}>
{customer ? (
{/* {customer ? (
<DropdownMenu />
) : (
<button
className={s.avatarButton}
aria-label="Menu"
onClick={() => openModal()}
>
<Avatar />
</button>
)}
) : ( */}
<button
className={s.avatarButton}
aria-label="Menu"
onClick={() => openModal()}
>
<Avatar />
</button>
{/* )} */}
</li>
)}
</ul>

View File

@ -5,7 +5,7 @@ import type { Product } from '@commerce/types/product'
import s from './ProductCard.module.css'
import Image, { ImageProps } 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 {
@ -25,11 +25,11 @@ const ProductCard: FC<Props> = ({
noNameTag = false,
variant = 'default',
}) => {
const { price } = usePrice({
amount: product.price.value,
baseAmount: product.price.retailPrice,
currencyCode: product.price.currencyCode!,
})
// const { price } = usePrice({
// amount: product.price.value,
// baseAmount: product.price.retailPrice,
// currencyCode: product.price.currencyCode!,
// })
const rootClassName = cn(
s.root,
@ -74,7 +74,7 @@ const ProductCard: FC<Props> = ({
<span>{product.name}</span>
</h3>
<div className={s.price}>
{`${price} ${product.price?.currencyCode}`}
{/* {`${price} ${product.price?.currencyCode}`} */}
</div>
</div>
)}
@ -104,10 +104,10 @@ const ProductCard: FC<Props> = ({
variant={product.variants[0] as any}
/>
)}
<ProductTag
{/* <ProductTag
name={product.name}
price={`${price} ${product.price?.currencyCode}`}
/>
/> */}
<div className={s.imageContainer}>
{product?.images && (
<Image

View File

@ -1,5 +1,5 @@
import s from './ProductSidebar.module.css'
import { useAddItem } from '@framework/cart'
// import { useAddItem } from '@framework/cart'
import { FC, useEffect, useState } from 'react'
import { ProductOptions } from '@components/product'
import type { Product } from '@commerce/types/product'
@ -16,7 +16,7 @@ interface ProductSidebarProps {
}
const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
const addItem = useAddItem()
// const addItem = useAddItem()
const { openSidebar } = useUI()
const [loading, setLoading] = useState(false)
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({})
@ -29,11 +29,11 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
const addToCart = async () => {
setLoading(true)
try {
await addItem({
productId: String(product.id),
variantId: String(variant ? variant.id : product.variants[0].id),
})
openSidebar()
// await addItem({
// productId: String(product.id),
// variantId: String(variant ? variant.id : product.variants[0].id),
// })
// openSidebar()
setLoading(false)
} catch (err) {
setLoading(false)
@ -48,12 +48,12 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
setSelectedOptions={setSelectedOptions}
/>
<Text
className="pb-4 break-words w-full max-w-xl"
className="w-full max-w-xl pb-4 break-words"
html={product.descriptionHtml || product.description}
/>
<div className="flex flex-row justify-between items-center">
<div className="flex flex-row items-center justify-between">
<Rating value={4} />
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
<div className="pr-1 text-sm font-medium text-accent-6">36 reviews</div>
</div>
<div>
{process.env.COMMERCE_CART_ENABLED && (

View File

@ -4,7 +4,7 @@ import { NextSeo } from 'next-seo'
import s from './ProductView.module.css'
import { FC } from 'react'
import type { Product } from '@commerce/types/product'
import usePrice from '@framework/product/use-price'
// import usePrice from '@framework/product/use-price'
import { WishlistButton } from '@components/wishlist'
import { ProductSlider, ProductCard } from '@components/product'
import { Container, Text } from '@components/ui'
@ -16,22 +16,22 @@ interface ProductViewProps {
}
const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
const { price } = usePrice({
amount: product.price.value,
baseAmount: product.price.retailPrice,
currencyCode: product.price.currencyCode!,
})
// const { price } = usePrice({
// amount: product.price.value,
// baseAmount: product.price.retailPrice,
// currencyCode: product.price.currencyCode!,
// })
return (
<>
<Container className="max-w-none w-full" clean>
<Container className="w-full max-w-none" clean>
<div className={cn(s.root, 'fit')}>
<div className={cn(s.main, 'fit')}>
<ProductTag
{/* <ProductTag
name={product.name}
price={`${price} ${product.price?.currencyCode}`}
fontSize={32}
/>
/> */}
<div className={s.sliderContainer}>
<ProductSlider key={product.id}>
{product.images.map((image, i) => (
@ -61,13 +61,13 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
<ProductSidebar product={product} className={s.sidebar} />
</div>
<hr className="mt-7 border-accent-2" />
<section className="py-12 px-6 mb-10">
<section className="px-6 py-12 mb-10">
<Text variant="sectionHeading">Related Products</Text>
<div className={s.relatedProductsGrid}>
{relatedProducts.map((p) => (
<div
key={p.path}
className="animated fadeIn bg-accent-0 border border-accent-2"
className="border animated fadeIn bg-accent-0 border-accent-2"
>
<ProductCard
noNameTag

View File

@ -23,7 +23,7 @@ export function selectDefaultOptionFromProduct(
updater: Dispatch<SetStateAction<SelectedOptions>>
) {
// Selects the default option
product.variants[0].options?.forEach((v) => {
product?.variants[0]?.options?.forEach((v) => {
updater((choices) => ({
...choices,
[v.displayName.toLowerCase()]: v.values[0].label.toLowerCase(),

View File

@ -9,7 +9,7 @@ import { ProductCard } from '@components/product'
import type { Product } from '@commerce/types/product'
import { Container, Skeleton } from '@components/ui'
import useSearch from '@framework/product/use-search'
// import useSearch from '@framework/product/use-search'
import getSlug from '@lib/get-slug'
import rangeMap from '@lib/range-map'
@ -28,412 +28,415 @@ import {
useSearchMeta,
} from '@lib/search'
export default function Search({ categories, brands }: SearchPropsType) {
const [activeFilter, setActiveFilter] = useState('')
const [toggleFilter, setToggleFilter] = useState(false)
const router = useRouter()
const { asPath, locale } = router
const { q, sort } = router.query
// `q` can be included but because categories and designers can't be searched
// in the same way of products, it's better to ignore the search input if one
// of those is selected
const query = filterQuery({ sort })
const { pathname, category, brand } = useSearchMeta(asPath)
const activeCategory = categories.find((cat: any) => cat.slug === category)
const activeBrand = brands.find(
(b: any) => getSlug(b.node.path) === `brands/${brand}`
)?.node
const { data } = useSearch({
search: typeof q === 'string' ? q : '',
categoryId: activeCategory?.id,
brandId: (activeBrand as any)?.entityId,
sort: typeof sort === 'string' ? sort : '',
locale,
})
const handleClick = (event: any, filter: string) => {
if (filter !== activeFilter) {
setToggleFilter(true)
} else {
setToggleFilter(!toggleFilter)
}
setActiveFilter(filter)
}
return (
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 mt-3 mb-20">
<div className="col-span-8 lg:col-span-2 order-1 lg:order-none">
{/* Categories */}
<div className="relative inline-block w-full">
<div className="lg:hidden">
<span className="rounded-md shadow-sm">
<button
type="button"
onClick={(e) => handleClick(e, 'categories')}
className="flex justify-between w-full rounded-sm border border-accent-3 px-4 py-3 bg-accent-0 text-sm leading-5 font-medium text-accent-4 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8 transition ease-in-out duration-150"
id="options-menu"
aria-haspopup="true"
aria-expanded="true"
>
{activeCategory?.name
? `Category: ${activeCategory?.name}`
: 'All Categories'}
<svg
className="-mr-1 ml-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</span>
</div>
<div
className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
activeFilter !== 'categories' || toggleFilter !== true
? 'hidden'
: ''
}`}
>
<div className="rounded-sm bg-accent-0 shadow-xs lg:bg-none lg:shadow-none">
<div
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<ul>
<li
className={cn(
'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
{
underline: !activeCategory?.name,
}
)}
>
<Link
href={{ pathname: getCategoryPath('', brand), query }}
>
<a
onClick={(e) => handleClick(e, 'categories')}
className={
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
}
>
All Categories
</a>
</Link>
</li>
{categories.map((cat: any) => (
<li
key={cat.path}
className={cn(
'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
{
underline: activeCategory?.id === cat.id,
}
)}
>
<Link
href={{
pathname: getCategoryPath(cat.path, brand),
query,
}}
>
<a
onClick={(e) => handleClick(e, 'categories')}
className={
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
}
>
{cat.name}
</a>
</Link>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
{/* Designs */}
<div className="relative inline-block w-full">
<div className="lg:hidden mt-3">
<span className="rounded-md shadow-sm">
<button
type="button"
onClick={(e) => handleClick(e, 'brands')}
className="flex justify-between w-full rounded-sm border border-accent-3 px-4 py-3 bg-accent-0 text-sm leading-5 font-medium text-accent-8 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8 transition ease-in-out duration-150"
id="options-menu"
aria-haspopup="true"
aria-expanded="true"
>
{activeBrand?.name
? `Design: ${activeBrand?.name}`
: 'All Designs'}
<svg
className="-mr-1 ml-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</span>
</div>
<div
className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
activeFilter !== 'brands' || toggleFilter !== true
? 'hidden'
: ''
}`}
>
<div className="rounded-sm bg-accent-0 shadow-xs lg:bg-none lg:shadow-none">
<div
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<ul>
<li
className={cn(
'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
{
underline: !activeBrand?.name,
}
)}
>
<Link
href={{
pathname: getDesignerPath('', category),
query,
}}
>
<a
onClick={(e) => handleClick(e, 'brands')}
className={
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
}
>
All Designers
</a>
</Link>
</li>
{brands.flatMap(({ node }: { node: any }) => (
<li
key={node.path}
className={cn(
'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
{
// @ts-ignore Shopify - Fix this types
underline: activeBrand?.entityId === node.entityId,
}
)}
>
<Link
href={{
pathname: getDesignerPath(node.path, category),
query,
}}
>
<a
onClick={(e) => handleClick(e, 'brands')}
className={
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
}
>
{node.name}
</a>
</Link>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
{/* Products */}
<div className="col-span-8 order-3 lg:order-none">
{(q || activeCategory || activeBrand) && (
<div className="mb-12 transition ease-in duration-75">
{data ? (
<>
<span
className={cn('animated', {
fadeIn: data.found,
hidden: !data.found,
})}
>
Showing {data.products.length} results{' '}
{q && (
<>
for "<strong>{q}</strong>"
</>
)}
</span>
<span
className={cn('animated', {
fadeIn: !data.found,
hidden: data.found,
})}
>
{q ? (
<>
There are no products that match "<strong>{q}</strong>"
</>
) : (
<>
There are no products that match the selected category.
</>
)}
</span>
</>
) : q ? (
<>
Searching for: "<strong>{q}</strong>"
</>
) : (
<>Searching...</>
)}
</div>
)}
{data ? (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{data.products.map((product: Product) => (
<ProductCard
variant="simple"
key={product.path}
className="animated fadeIn"
product={product}
imgProps={{
width: 480,
height: 480,
}}
/>
))}
</div>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{rangeMap(12, (i) => (
<Skeleton key={i}>
<div className="w-60 h-60" />
</Skeleton>
))}
</div>
)}{' '}
</div>
{/* Sort */}
<div className="col-span-8 lg:col-span-2 order-2 lg:order-none">
<div className="relative inline-block w-full">
<div className="lg:hidden">
<span className="rounded-md shadow-sm">
<button
type="button"
onClick={(e) => handleClick(e, 'sort')}
className="flex justify-between w-full rounded-sm border border-accent-3 px-4 py-3 bg-accent-0 text-sm leading-5 font-medium text-accent-4 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8 transition ease-in-out duration-150"
id="options-menu"
aria-haspopup="true"
aria-expanded="true"
>
{sort ? SORT[sort as keyof typeof SORT] : 'Relevance'}
<svg
className="-mr-1 ml-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</span>
</div>
<div
className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
activeFilter !== 'sort' || toggleFilter !== true ? 'hidden' : ''
}`}
>
<div className="rounded-sm bg-accent-0 shadow-xs lg:bg-none lg:shadow-none">
<div
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<ul>
<li
className={cn(
'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
{
underline: !sort,
}
)}
>
<Link href={{ pathname, query: filterQuery({ q }) }}>
<a
onClick={(e) => handleClick(e, 'sort')}
className={
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
}
>
Relevance
</a>
</Link>
</li>
{Object.entries(SORT).map(([key, text]) => (
<li
key={key}
className={cn(
'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
{
underline: sort === key,
}
)}
>
<Link
href={{
pathname,
query: filterQuery({ q, sort: key }),
}}
>
<a
onClick={(e) => handleClick(e, 'sort')}
className={
'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
}
>
{text}
</a>
</Link>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</Container>
)
export default function Search() {
return <div />
}
// export default function Search({ categories, brands }: SearchPropsType) {
// const [activeFilter, setActiveFilter] = useState('')
// const [toggleFilter, setToggleFilter] = useState(false)
// const router = useRouter()
// const { asPath, locale } = router
// const { q, sort } = router.query
// // `q` can be included but because categories and designers can't be searched
// // in the same way of products, it's better to ignore the search input if one
// // of those is selected
// const query = filterQuery({ sort })
// const { pathname, category, brand } = useSearchMeta(asPath)
// const activeCategory = categories.find((cat: any) => cat.slug === category)
// const activeBrand = brands.find(
// (b: any) => getSlug(b.node.path) === `brands/${brand}`
// )?.node
// const { data } = useSearch({
// search: typeof q === 'string' ? q : '',
// categoryId: activeCategory?.id,
// brandId: (activeBrand as any)?.entityId,
// sort: typeof sort === 'string' ? sort : '',
// locale,
// })
// const handleClick = (event: any, filter: string) => {
// if (filter !== activeFilter) {
// setToggleFilter(true)
// } else {
// setToggleFilter(!toggleFilter)
// }
// setActiveFilter(filter)
// }
// return (
// <Container>
// <div className="grid grid-cols-1 gap-4 mt-3 mb-20 lg:grid-cols-12">
// <div className="order-1 col-span-8 lg:col-span-2 lg:order-none">
// {/* Categories */}
// <div className="relative inline-block w-full">
// <div className="lg:hidden">
// <span className="rounded-md shadow-sm">
// <button
// type="button"
// onClick={(e) => handleClick(e, 'categories')}
// className="flex justify-between w-full px-4 py-3 text-sm font-medium leading-5 transition duration-150 ease-in-out border rounded-sm border-accent-3 bg-accent-0 text-accent-4 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8"
// id="options-menu"
// aria-haspopup="true"
// aria-expanded="true"
// >
// {activeCategory?.name
// ? `Category: ${activeCategory?.name}`
// : 'All Categories'}
// <svg
// className="w-5 h-5 ml-2 -mr-1"
// xmlns="http://www.w3.org/2000/svg"
// viewBox="0 0 20 20"
// fill="currentColor"
// >
// <path
// fillRule="evenodd"
// d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
// clipRule="evenodd"
// />
// </svg>
// </button>
// </span>
// </div>
// <div
// className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
// activeFilter !== 'categories' || toggleFilter !== true
// ? 'hidden'
// : ''
// }`}
// >
// <div className="rounded-sm shadow-xs bg-accent-0 lg:bg-none lg:shadow-none">
// <div
// role="menu"
// aria-orientation="vertical"
// aria-labelledby="options-menu"
// >
// <ul>
// <li
// className={cn(
// 'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
// {
// underline: !activeCategory?.name,
// }
// )}
// >
// <Link
// href={{ pathname: getCategoryPath('', brand), query }}
// >
// <a
// onClick={(e) => handleClick(e, 'categories')}
// className={
// 'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
// }
// >
// All Categories
// </a>
// </Link>
// </li>
// {categories.map((cat: any) => (
// <li
// key={cat.path}
// className={cn(
// 'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
// {
// underline: activeCategory?.id === cat.id,
// }
// )}
// >
// <Link
// href={{
// pathname: getCategoryPath(cat.path, brand),
// query,
// }}
// >
// <a
// onClick={(e) => handleClick(e, 'categories')}
// className={
// 'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
// }
// >
// {cat.name}
// </a>
// </Link>
// </li>
// ))}
// </ul>
// </div>
// </div>
// </div>
// </div>
// {/* Designs */}
// <div className="relative inline-block w-full">
// <div className="mt-3 lg:hidden">
// <span className="rounded-md shadow-sm">
// <button
// type="button"
// onClick={(e) => handleClick(e, 'brands')}
// className="flex justify-between w-full px-4 py-3 text-sm font-medium leading-5 transition duration-150 ease-in-out border rounded-sm border-accent-3 bg-accent-0 text-accent-8 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8"
// id="options-menu"
// aria-haspopup="true"
// aria-expanded="true"
// >
// {activeBrand?.name
// ? `Design: ${activeBrand?.name}`
// : 'All Designs'}
// <svg
// className="w-5 h-5 ml-2 -mr-1"
// xmlns="http://www.w3.org/2000/svg"
// viewBox="0 0 20 20"
// fill="currentColor"
// >
// <path
// fillRule="evenodd"
// d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
// clipRule="evenodd"
// />
// </svg>
// </button>
// </span>
// </div>
// <div
// className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
// activeFilter !== 'brands' || toggleFilter !== true
// ? 'hidden'
// : ''
// }`}
// >
// <div className="rounded-sm shadow-xs bg-accent-0 lg:bg-none lg:shadow-none">
// <div
// role="menu"
// aria-orientation="vertical"
// aria-labelledby="options-menu"
// >
// <ul>
// <li
// className={cn(
// 'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
// {
// underline: !activeBrand?.name,
// }
// )}
// >
// <Link
// href={{
// pathname: getDesignerPath('', category),
// query,
// }}
// >
// <a
// onClick={(e) => handleClick(e, 'brands')}
// className={
// 'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
// }
// >
// All Designers
// </a>
// </Link>
// </li>
// {brands.flatMap(({ node }: { node: any }) => (
// <li
// key={node.path}
// className={cn(
// 'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
// {
// // @ts-ignore Shopify - Fix this types
// underline: activeBrand?.entityId === node.entityId,
// }
// )}
// >
// <Link
// href={{
// pathname: getDesignerPath(node.path, category),
// query,
// }}
// >
// <a
// onClick={(e) => handleClick(e, 'brands')}
// className={
// 'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
// }
// >
// {node.name}
// </a>
// </Link>
// </li>
// ))}
// </ul>
// </div>
// </div>
// </div>
// </div>
// </div>
// {/* Products */}
// <div className="order-3 col-span-8 lg:order-none">
// {(q || activeCategory || activeBrand) && (
// <div className="mb-12 transition duration-75 ease-in">
// {data ? (
// <>
// <span
// className={cn('animated', {
// fadeIn: data.found,
// hidden: !data.found,
// })}
// >
// Showing {data.products.length} results{' '}
// {q && (
// <>
// for "<strong>{q}</strong>"
// </>
// )}
// </span>
// <span
// className={cn('animated', {
// fadeIn: !data.found,
// hidden: data.found,
// })}
// >
// {q ? (
// <>
// There are no products that match "<strong>{q}</strong>"
// </>
// ) : (
// <>
// There are no products that match the selected category.
// </>
// )}
// </span>
// </>
// ) : q ? (
// <>
// Searching for: "<strong>{q}</strong>"
// </>
// ) : (
// <>Searching...</>
// )}
// </div>
// )}
// {data ? (
// <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
// {data.products.map((product: Product) => (
// <ProductCard
// variant="simple"
// key={product.path}
// className="animated fadeIn"
// product={product}
// imgProps={{
// width: 480,
// height: 480,
// }}
// />
// ))}
// </div>
// ) : (
// <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
// {rangeMap(12, (i) => (
// <Skeleton key={i}>
// <div className="w-60 h-60" />
// </Skeleton>
// ))}
// </div>
// )}{' '}
// </div>
// {/* Sort */}
// <div className="order-2 col-span-8 lg:col-span-2 lg:order-none">
// <div className="relative inline-block w-full">
// <div className="lg:hidden">
// <span className="rounded-md shadow-sm">
// <button
// type="button"
// onClick={(e) => handleClick(e, 'sort')}
// className="flex justify-between w-full px-4 py-3 text-sm font-medium leading-5 transition duration-150 ease-in-out border rounded-sm border-accent-3 bg-accent-0 text-accent-4 hover:text-accent-5 focus:outline-none focus:border-blue-300 focus:shadow-outline-normal active:bg-accent-1 active:text-accent-8"
// id="options-menu"
// aria-haspopup="true"
// aria-expanded="true"
// >
// {sort ? SORT[sort as keyof typeof SORT] : 'Relevance'}
// <svg
// className="w-5 h-5 ml-2 -mr-1"
// xmlns="http://www.w3.org/2000/svg"
// viewBox="0 0 20 20"
// fill="currentColor"
// >
// <path
// fillRule="evenodd"
// d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
// clipRule="evenodd"
// />
// </svg>
// </button>
// </span>
// </div>
// <div
// className={`origin-top-left absolute lg:relative left-0 mt-2 w-full rounded-md shadow-lg lg:shadow-none z-10 mb-10 lg:block ${
// activeFilter !== 'sort' || toggleFilter !== true ? 'hidden' : ''
// }`}
// >
// <div className="rounded-sm shadow-xs bg-accent-0 lg:bg-none lg:shadow-none">
// <div
// role="menu"
// aria-orientation="vertical"
// aria-labelledby="options-menu"
// >
// <ul>
// <li
// className={cn(
// 'block text-sm leading-5 text-accent-4 lg:text-base lg:no-underline lg:font-bold lg:tracking-wide hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
// {
// underline: !sort,
// }
// )}
// >
// <Link href={{ pathname, query: filterQuery({ q }) }}>
// <a
// onClick={(e) => handleClick(e, 'sort')}
// className={
// 'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
// }
// >
// Relevance
// </a>
// </Link>
// </li>
// {Object.entries(SORT).map(([key, text]) => (
// <li
// key={key}
// className={cn(
// 'block text-sm leading-5 text-accent-4 hover:bg-accent-1 lg:hover:bg-transparent hover:text-accent-8 focus:outline-none focus:bg-accent-1 focus:text-accent-8',
// {
// underline: sort === key,
// }
// )}
// >
// <Link
// href={{
// pathname,
// query: filterQuery({ q, sort: key }),
// }}
// >
// <a
// onClick={(e) => handleClick(e, 'sort')}
// className={
// 'block lg:inline-block px-4 py-2 lg:p-0 lg:my-2 lg:mx-4'
// }
// >
// {text}
// </a>
// </Link>
// </li>
// ))}
// </ul>
// </div>
// </div>
// </div>
// </div>
// </div>
// </div>
// </Container>
// )
// }
Search.Layout = Layout

View File

@ -2,10 +2,10 @@ import React, { FC, useState } from 'react'
import cn from 'classnames'
import { useUI } from '@components/ui'
import { Heart } from '@components/icons'
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 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 s from './WishlistButton.module.css'
import type { Product, ProductVariant } from '@commerce/types/product'
@ -20,10 +20,10 @@ const WishlistButton: FC<Props> = ({
className,
...props
}) => {
const { data } = useWishlist()
const addItem = useAddItem()
const removeItem = useRemoveItem()
const { data: customer } = useCustomer()
// const { data } = useWishlist()
// const addItem = useAddItem()
// const removeItem = useRemoveItem()
// const { data: customer } = useCustomer()
const { openModal, setModalView } = useUI()
const [loading, setLoading] = useState(false)
@ -41,22 +41,22 @@ const WishlistButton: FC<Props> = ({
if (loading) return
// A login is required before adding an item to the wishlist
if (!customer) {
setModalView('LOGIN_VIEW')
return openModal()
}
// if (!customer) {
// setModalView('LOGIN_VIEW')
// return openModal()
// }
setLoading(true)
try {
if (itemInWishlist) {
await removeItem({ id: itemInWishlist.id! })
} else {
await addItem({
productId,
variantId: variant?.id!,
})
}
// if (itemInWishlist) {
// await removeItem({ id: itemInWishlist.id! })
// } else {
// await addItem({
// productId,
// variantId: variant?.id!,
// })
// }
setLoading(false)
} catch (err) {

View File

@ -8,9 +8,9 @@ import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context'
import type { Product } from '@commerce/types/product'
import usePrice from '@framework/product/use-price'
import useAddItem from '@framework/cart/use-add-item'
import useRemoveItem from '@framework/wishlist/use-remove-item'
// 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 {
product: Product
@ -19,11 +19,11 @@ interface Props {
const placeholderImg = '/product-img-placeholder.svg'
const WishlistCard: FC<Props> = ({ product }) => {
const { price } = usePrice({
amount: product.price?.value,
baseAmount: product.price?.retailPrice,
currencyCode: product.price?.currencyCode!,
})
// const { price } = usePrice({
// amount: product.price?.value,
// baseAmount: product.price?.retailPrice,
// currencyCode: product.price?.currencyCode!,
// })
// @ts-ignore Wishlist is not always enabled
const removeItem = useRemoveItem({ wishlist: { includeProducts: true } })
const [loading, setLoading] = useState(false)
@ -71,7 +71,7 @@ const WishlistCard: FC<Props> = ({ product }) => {
</div>
<div className="col-span-7">
<h3 className="text-2xl mb-2">
<h3 className="mb-2 text-2xl">
<Link href={`/product${product.path}`}>
<a>{product.name}</a>
</Link>
@ -91,8 +91,8 @@ const WishlistCard: FC<Props> = ({ product }) => {
Add to Cart
</Button>
</div>
<div className="col-span-2 flex flex-col justify-between">
<div className="flex justify-end font-bold">{price}</div>
<div className="flex flex-col justify-between col-span-2">
{/* <div className="flex justify-end font-bold">{price}</div> */}
<div className="flex justify-end">
<button onClick={handleRemove}>
<Trash />

View File

@ -14,6 +14,7 @@ const PROVIDERS = [
'shopify',
'swell',
'vendure',
'woocommerce',
]
function getProviderName() {
@ -25,6 +26,8 @@ function getProviderName() {
? 'shopify'
: process.env.NEXT_PUBLIC_SWELL_STORE_ID
? 'swell'
: process.env.NEXT_PUBLIC_WOOCOMMERCE_SHOP_API_URL
? 'woocommerce'
: 'local')
)
}

View File

@ -9,7 +9,7 @@ export { default as checkoutToCart } from './checkout-to-cart'
export { default as handleLogin, handleAutomaticLogin } from './handle-login'
export { default as handleAccountActivation } from './handle-account-activation'
export { default as throwUserErrors } from './throw-user-errors'
export * from './queries'
export * from './mutations'
// export * from './queries'
// export * from './mutations'
export * from './normalize'
export * from './customer-token'

View File

@ -0,0 +1,3 @@
COMMERCE_PROVIDER=woocommerce
NEXT_PUBLIC_WOOCOMMERCE_SHOP_API_URL=

View File

@ -0,0 +1,135 @@
## Shopify Provider
**Demo:** https://shopify.demo.vercel.store/
Before getting starter, a [Shopify](https://www.shopify.com/) account and store is required before using the provider.
Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
```bash
cp framework/shopify/.env.template .env.local
```
Then, set the environment variables in `.env.local` to match the ones from your store.
## Contribute
Our commitment to Open Source can be found [here](https://vercel.com/oss).
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues).
## Modifications
These modifications are temporarily until contributions are made to remove them.
### Adding item to Cart
```js
// components/product/ProductView/ProductView.tsx
const ProductView: FC<Props> = ({ product }) => {
const addToCart = async () => {
setLoading(true)
try {
await addItem({
productId: product.id,
variantId: variant ? variant.id : product.variants[0].id,
})
openSidebar()
setLoading(false)
} catch (err) {
setLoading(false)
}
}
}
```
### Proceed to Checkout
```js
// components/cart/CartSidebarView/CartSidebarView.tsx
import { useCommerce } from '@framework'
const CartSidebarView: FC = () => {
const { checkout } = useCommerce()
return (
<Button href={checkout.webUrl} Component="a" width="100%">
Proceed to Checkout
</Button>
)
}
```
## APIs
Collections of APIs to fetch data from a Shopify store.
The data is fetched using the [Shopify JavaScript Buy SDK](https://github.com/Shopify/js-buy-sdk#readme). Read the [Shopify Storefront API reference](https://shopify.dev/docs/storefront-api/reference) for more information.
### getProduct
Get a single product by its `handle`.
```js
import getProduct from '@framework/product/get-product'
import { getConfig } from '@framework/api'
const config = getConfig()
const product = await getProduct({
variables: { slug },
config,
})
```
### getAllProducts
```js
import getAllProducts from '@framework/product/get-all-products'
import { getConfig } from '@framework/api'
const config = getConfig()
const { products } = await getAllProducts({
variables: { first: 12 },
config,
})
```
### getAllCollections
```js
import getAllCollections from '@framework/product/get-all-collections'
import { getConfig } from '@framework/api'
const config = getConfig()
const collections = await getAllCollections({
config,
})
```
### getAllPages
```js
import getAllPages from '@framework/common/get-all-pages'
import { getConfig } from '@framework/api'
const config = getConfig()
const pages = await getAllPages({
variables: { first: 12 },
config,
})
```
## Code generation
This provider makes use of GraphQL code generation. The [schema.graphql](./schema.graphql) and [schema.d.ts](./schema.d.ts) files contain the generated types & schema introspection results.
When developing the provider, changes to any GraphQL operations should be followed by re-generation of the types and schema files:
From the project root dir, run:
```sh
yarn generate:shopify
```

View File

@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@ -0,0 +1,38 @@
import {
WOOCOMMERCE_CHECKOUT_ID_COOKIE,
WOOCOMMERCE_CHECKOUT_URL_COOKIE,
WOOCOMMERCE_CUSTOMER_TOKEN_COOKIE,
} from '../../../const'
import associateCustomerWithCheckoutMutation from '../../../utils/mutations/associate-customer-with-checkout'
import type { CheckoutEndpoint } from '.'
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
req,
res,
config,
}) => {
const { cookies } = req
const checkoutUrl = cookies[WOOCOMMERCE_CHECKOUT_URL_COOKIE]
const customerCookie = cookies[WOOCOMMERCE_CUSTOMER_TOKEN_COOKIE]
if (customerCookie) {
try {
await config.fetch(associateCustomerWithCheckoutMutation, {
variables: {
checkoutId: cookies[WOOCOMMERCE_CHECKOUT_ID_COOKIE],
customerAccessToken: cookies[WOOCOMMERCE_CUSTOMER_TOKEN_COOKIE],
},
})
} catch (error) {
console.error(error)
}
}
if (checkoutUrl) {
res.redirect(checkoutUrl)
} else {
res.redirect('/cart')
}
}
export default checkout

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
import type { CheckoutSchema } from '../../../types/checkout'
import type { WOOCOMMERCEAPI } from '../..'
import checkout from './checkout'
export type CheckoutAPI = GetAPISchema<WOOCOMMERCEAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = { checkout }
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@ -0,0 +1 @@
export default function (_commerce: any) {}

View File

@ -0,0 +1,50 @@
import {
CommerceAPI,
CommerceAPIConfig,
getCommerceApi as commerceApi,
} from '@commerce/api'
import {
API_URL,
WOOCOMMERCE_CUSTOMER_TOKEN_COOKIE,
WOOCOMMERCE_CHECKOUT_ID_COOKIE,
} from '../const'
import fetchGraphqlApi from './utils/fetch-graphql-api'
import * as operations from './operations'
if (!API_URL) {
throw new Error(
`The environment variable NEXT_PUBLIC_WOOCOMMERCE_STORE_DOMAIN is missing and it's required to access your store`
)
}
export interface WooCommerceConfig extends CommerceAPIConfig {}
const ONE_DAY = 60 * 60 * 24
//TODO we don't have a apiToken here
const config: WooCommerceConfig = {
commerceUrl: API_URL,
apiToken: '',
customerCookie: WOOCOMMERCE_CUSTOMER_TOKEN_COOKIE,
cartCookie: WOOCOMMERCE_CHECKOUT_ID_COOKIE,
cartCookieMaxAge: ONE_DAY * 30,
fetch: fetchGraphqlApi,
}
export const provider = {
config,
operations,
}
export type Provider = typeof provider
export type WOOCOMMERCEAPI<P extends Provider = Provider> = CommerceAPI<P>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): WOOCOMMERCEAPI<P> {
return commerceApi(customProvider)
}

View File

@ -0,0 +1,69 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import {
GetAllPagesQuery,
GetAllPagesQueryVariables,
PageEdge,
} from '../../schema'
import { normalizePages } from '../../utils'
import type { ShopifyConfig, Provider } from '..'
import type { GetAllPagesOperation, Page } from '../../types/page'
import getAllPagesQuery from '../../utils/queries/get-all-pages-query'
export default function getAllPagesOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllPages<T extends GetAllPagesOperation>(opts?: {
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>(
opts: {
config?: Partial<ShopifyConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllPages<T extends GetAllPagesOperation>({
query = getAllPagesQuery,
config,
variables,
}: {
url?: string
config?: Partial<ShopifyConfig>
variables?: GetAllPagesQueryVariables
preview?: boolean
query?: string
} = {}): Promise<T['data']> {
const { fetch, locale, locales = ['en-US', 'es'] } = commerce.getConfig(
config
)
const { data } = await fetch<GetAllPagesQuery, GetAllPagesQueryVariables>(
query,
{
variables,
},
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return {
pages: locales.reduce<Page[]>(
(arr, locale) =>
arr.concat(normalizePages(data.pages.edges as PageEdge[], locale)),
[]
),
}
}
return getAllPages
}

View File

@ -0,0 +1,58 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import { GetAllProductPathsOperation } from '../../types/product'
import {
GetAllProductPathsQuery,
GetAllProductPathsQueryVariables,
} from '../../schema'
import type { WooCommerceConfig, Provider } from '..'
import getAllProductsQuery from '../../wp/queries/get-all-products-paths-query'
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<
T extends GetAllProductPathsOperation
>(opts?: {
variables?: T['variables']
config?: WooCommerceConfig
}): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
opts: {
variables?: T['variables']
config?: WooCommerceConfig
} & OperationOptions
): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
query = getAllProductsQuery,
config,
variables,
}: {
query?: string
config?: WooCommerceConfig
variables?: T['variables']
} = {}): Promise<T['data']> {
config = commerce.getConfig(config)
const { data } = await config.fetch<
GetAllProductPathsQuery,
GetAllProductPathsQueryVariables
>(query, { variables })
const products = data?.products?.edges
? data.products.edges.map((edge) => ({
path: `/${edge?.node?.slug}`,
}))
: []
return {
products,
}
}
return getAllProductPaths
}

View File

@ -0,0 +1,79 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import { GetAllProductsOperation } from '../../types/product'
import {
GetAllProductsQuery,
GetAllProductsQueryVariables,
SimpleProduct,
} from '../../schema'
import type { WooCommerceConfig, Provider } from '..'
import getAllProductsQuery from '../../wp/queries/get-all-products-query'
import { normalizeProduct } from '../../utils'
import type { Product } from '../../types/product'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
variables?: T['variables']
config?: Partial<WooCommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getAllProducts<T extends GetAllProductsOperation>(
opts: {
variables?: T['variables']
config?: Partial<WooCommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getAllProducts<T extends GetAllProductsOperation>({
query = getAllProductsQuery,
variables,
config,
}: {
query?: string
variables?: T['variables']
config?: Partial<WooCommerceConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(config)
// console.log({ a: 'reza', query, variables, config, fetch, locale })
try {
const { data } = await fetch<
GetAllProductsQuery,
GetAllProductsQueryVariables
>(
query,
{ variables },
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
let products: Product[] = []
if (data?.products?.edges) {
data?.products?.edges?.map((edge) =>
products.push(normalizeProduct(edge?.node as SimpleProduct))
)
}
return {
products,
}
} catch (e) {
throw e
}
}
return getAllProducts
}

View File

@ -0,0 +1,64 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import { normalizePage } from '../../utils'
import type { ShopifyConfig, Provider } from '..'
import {
GetPageQuery,
GetPageQueryVariables,
Page as ShopifyPage,
} from '../../schema'
import { GetPageOperation } from '../../types/page'
import getPageQuery from '../../utils/queries/get-page-query'
export default function getPageOperation({
commerce,
}: OperationContext<Provider>) {
async function getPage<T extends GetPageOperation>(opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']>
async function getPage<T extends GetPageOperation>(
opts: {
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getPage<T extends GetPageOperation>({
query = getPageQuery,
variables,
config,
}: {
query?: string
variables: T['variables']
config?: Partial<ShopifyConfig>
preview?: boolean
}): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(config)
const {
data: { node: page },
} = await fetch<GetPageQuery, GetPageQueryVariables>(
query,
{
variables,
},
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return page ? { page: normalizePage(page as ShopifyPage, locale) } : {}
}
return getPage
}

View File

@ -0,0 +1,64 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import { GetProductOperation } from '../../types/product'
import { normalizeProduct } from '../../utils'
import getProductQuery from '../../wp/queries/get-product-query'
import type { WooCommerceConfig, Provider } from '..'
import { GetProductBySlugQuery, SimpleProduct } from '../../schema'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct<T extends GetProductOperation>(opts: {
variables: T['variables']
config?: Partial<WooCommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getProduct<T extends GetProductOperation>(
opts: {
variables: T['variables']
config?: Partial<WooCommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getProduct<T extends GetProductOperation>({
query = getProductQuery,
variables,
config: cfg,
}: {
query?: string
variables: T['variables']
config?: Partial<WooCommerceConfig>
preview?: boolean
}): Promise<T['data']> {
const { fetch, locale } = commerce.getConfig(cfg)
const {
data: { product },
} = await fetch<GetProductBySlugQuery>(
query,
{
variables,
},
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return {
...(product && {
product: normalizeProduct(product as SimpleProduct),
}),
}
}
return getProduct
}

View File

@ -0,0 +1,62 @@
import type {
OperationContext,
OperationOptions,
} from '@commerce/api/operations'
import { GetSiteInfoQueryVariables } from '../../schema'
import type { WooCommerceConfig, Provider } from '..'
import { GetSiteInfoOperation } from '../../types/site'
import { getCategories, getBrands, getSiteInfoQuery } from '../../utils'
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>(opts?: {
config?: Partial<WooCommerceConfig>
preview?: boolean
}): Promise<T['data']>
async function getSiteInfo<T extends GetSiteInfoOperation>(
opts: {
config?: Partial<WooCommerceConfig>
preview?: boolean
} & OperationOptions
): Promise<T['data']>
async function getSiteInfo<T extends GetSiteInfoOperation>({
query = getSiteInfoQuery,
config,
variables,
}: {
query?: string
config?: Partial<WooCommerceConfig>
preview?: boolean
variables?: GetSiteInfoQueryVariables
} = {}): Promise<T['data']> {
const cfg = commerce.getConfig(config)
console.log(cfg)
// const categoriesPromise = getCategories(cfg)
// const brandsPromise = getBrands(cfg)
/*
const { fetch, locale } = cfg
const { data } = await fetch<GetSiteInfoQuery, GetSiteInfoQueryVariables>(
query,
{ variables },
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
*/
return {
// categories: await categoriesPromise,
// brands: await brandsPromise,
}
}
return getSiteInfo
}

View File

@ -0,0 +1,7 @@
// export { default as getAllPages } from './get-all-pages'
// export { default as getPage } from './get-page'
export { default as getAllProducts } from './get-all-products'
export { default as getAllProductPaths } from './get-all-product-paths'
export { default as getProduct } from './get-product'
// export { default as getSiteInfo } from './get-site-info'
// export { default as login } from './login'

View File

@ -0,0 +1,48 @@
import type { ServerResponse } from 'http'
import type { OperationContext } from '@commerce/api/operations'
import type { LoginOperation } from '../../types/login'
import type { ShopifyConfig, Provider } from '..'
import {
customerAccessTokenCreateMutation,
setCustomerToken,
throwUserErrors,
} from '../../utils'
import { CustomerAccessTokenCreateMutation } from '../../schema'
export default function loginOperation({
commerce,
}: OperationContext<Provider>) {
async function login<T extends LoginOperation>({
query = customerAccessTokenCreateMutation,
variables,
config,
}: {
query?: string
variables: T['variables']
res: ServerResponse
config?: ShopifyConfig
}): Promise<T['data']> {
config = commerce.getConfig(config)
const {
data: { customerAccessTokenCreate },
} = await config.fetch<CustomerAccessTokenCreateMutation>(query, {
variables,
})
throwUserErrors(customerAccessTokenCreate?.customerUserErrors)
const customerAccessToken = customerAccessTokenCreate?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return {
result: customerAccessToken?.accessToken,
}
}
return login
}

View File

@ -0,0 +1,29 @@
import type { GraphQLFetcher } from '@commerce/api'
import fetch from './fetch'
import { API_URL } from '../../const'
import { getError } from '../../utils/handle-fetch-response'
const fetchGraphqlApi: GraphQLFetcher = async (
query: string,
{ variables } = {},
fetchOptions
) => {
const res = await fetch(API_URL, {
...fetchOptions,
method: 'POST',
headers: {
...fetchOptions?.headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
const result = await res.json()
return result
}
export default fetchGraphqlApi

View File

@ -0,0 +1,2 @@
import zeitFetch from '@vercel/fetch'
export default zeitFetch()

View File

@ -0,0 +1,62 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
import type { LoginHook } from '../types/login'
import useCustomer from '../customer/use-customer'
import {
setCustomerToken,
throwUserErrors,
customerAccessTokenCreateMutation,
} from '../utils'
import { Mutation, MutationCustomerAccessTokenCreateArgs } from '../schema'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<LoginHook> = {
fetchOptions: {
query: customerAccessTokenCreateMutation,
},
async fetcher({ input: { email, password }, options, fetch }) {
if (!(email && password)) {
throw new CommerceError({
message:
'An email and password are required to login',
})
}
const { customerAccessTokenCreate } = await fetch<
Mutation,
MutationCustomerAccessTokenCreateArgs
>({
...options,
variables: {
input: { email, password },
},
})
throwUserErrors(customerAccessTokenCreate?.customerUserErrors)
const customerAccessToken = customerAccessTokenCreate?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return null
},
useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer()
return useCallback(
async function login(input) {
const data = await fetch({ input })
await revalidate()
return data
},
[fetch, revalidate]
)
},
}

View File

@ -0,0 +1,37 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/auth/use-logout'
import type { LogoutHook } from '../types/logout'
import useCustomer from '../customer/use-customer'
import customerAccessTokenDeleteMutation from '../utils/mutations/customer-access-token-delete'
import { getCustomerToken, setCustomerToken } from '../utils/customer-token'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<LogoutHook> = {
fetchOptions: {
query: customerAccessTokenDeleteMutation,
},
async fetcher({ options, fetch }) {
await fetch({
...options,
variables: {
customerAccessToken: getCustomerToken(),
},
})
setCustomerToken(null)
return null
},
useHook: ({ fetch }) => () => {
const { mutate } = useCustomer()
return useCallback(
async function logout() {
const data = await fetch()
await mutate(null, false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,65 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
import type { SignupHook } from '../types/signup'
import useCustomer from '../customer/use-customer'
import { Mutation, MutationCustomerCreateArgs } from '../schema'
import {
handleAutomaticLogin,
throwUserErrors,
customerCreateMutation,
} from '../utils'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<SignupHook> = {
fetchOptions: {
query: customerCreateMutation,
},
async fetcher({
input: { firstName, lastName, email, password },
options,
fetch,
}) {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
const { customerCreate } = await fetch<
Mutation,
MutationCustomerCreateArgs
>({
...options,
variables: {
input: {
firstName,
lastName,
email,
password,
},
},
})
throwUserErrors(customerCreate?.customerUserErrors)
await handleAutomaticLogin(fetch, { email, password })
return null
},
useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer()
return useCallback(
async function signup(input) {
const data = await fetch({ input })
await revalidate()
return data
},
[fetch, revalidate]
)
},
}

View File

@ -0,0 +1,6 @@
// export { default as useCart } from './use-cart'
// export { default as useAddItem } from './use-add-item'
// export { default as useUpdateItem } from './use-update-item'
// export { default as useRemoveItem } from './use-remove-item'
export {}

View File

@ -0,0 +1,63 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
import type { AddItemHook } from '../types/cart'
import useCart from './use-cart'
import {
checkoutLineItemAddMutation,
getCheckoutId,
checkoutToCart,
} from '../utils'
import { Mutation, MutationCheckoutLineItemsAddArgs } from '../schema'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
query: checkoutLineItemAddMutation,
},
async fetcher({ input: item, options, fetch }) {
if (
item.quantity &&
(!Number.isInteger(item.quantity) || item.quantity! < 1)
) {
throw new CommerceError({
message: 'The item quantity has to be a valid integer greater than 0',
})
}
const { checkoutLineItemsAdd } = await fetch<
Mutation,
MutationCheckoutLineItemsAddArgs
>({
...options,
variables: {
checkoutId: getCheckoutId(),
lineItems: [
{
variantId: item.variantId,
quantity: item.quantity ?? 1,
},
],
},
})
return checkoutToCart(checkoutLineItemsAdd)
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCart()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,59 @@
import { useMemo } from 'react'
import useCommerceCart, { UseCart } from '@commerce/cart/use-cart'
import { SWRHook } from '@commerce/utils/types'
import { checkoutCreate, checkoutToCart } from '../utils'
import getCheckoutQuery from '../utils/queries/get-checkout-query'
import { GetCartHook } from '../types/cart'
import {
GetCheckoutQuery,
GetCheckoutQueryVariables,
CheckoutDetailsFragment,
} from '../schema'
export default useCommerceCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = {
fetchOptions: {
query: getCheckoutQuery,
},
async fetcher({ input: { cartId: checkoutId }, options, fetch }) {
let checkout
if (checkoutId) {
const data = await fetch({
...options,
variables: {
checkoutId: checkoutId,
},
})
checkout = data.node
}
if (checkout?.completedAt || !checkoutId) {
checkout = await checkoutCreate(fetch)
}
return checkoutToCart({ checkout })
},
useHook:
({ useData }) =>
(input) => {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems.length ?? 0) <= 0
},
enumerable: true,
},
}),
[response]
)
},
}

View File

@ -0,0 +1,67 @@
import { useCallback } from 'react'
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
import type { Cart, LineItem, RemoveItemHook } from '../types/cart'
import useCart from './use-cart'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
export type RemoveItemActionInput<T = any> = T extends LineItem
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
import {
checkoutLineItemRemoveMutation,
getCheckoutId,
checkoutToCart,
} from '../utils'
import { Mutation, MutationCheckoutLineItemsRemoveArgs } from '../schema'
export const handler = {
fetchOptions: {
query: checkoutLineItemRemoveMutation,
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
const data = await fetch<Mutation, MutationCheckoutLineItemsRemoveArgs>({
...options,
variables: { checkoutId: getCheckoutId(), lineItemIds: [itemId] },
})
return checkoutToCart(data.checkoutLineItemsRemove)
},
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) => <
T extends LineItem | undefined = undefined
>(
ctx: { item?: T } = {}
) => {
const { item } = ctx
const { mutate } = useCart()
const removeItem: RemoveItemFn<LineItem> = async (input) => {
const itemId = input?.id ?? item?.id
if (!itemId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({ input: { itemId } })
await mutate(data, false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@ -0,0 +1,105 @@
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import type {
HookFetcherContext,
MutationHookContext,
} from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
import useCart from './use-cart'
import { handler as removeItemHandler } from './use-remove-item'
import type { UpdateItemHook, LineItem } from '../types/cart'
import {
getCheckoutId,
checkoutLineItemUpdateMutation,
checkoutToCart,
} from '../utils'
import { Mutation, MutationCheckoutLineItemsUpdateArgs } from '../schema'
export type UpdateItemActionInput<T = any> = T extends LineItem
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler = {
fetchOptions: {
query: checkoutLineItemUpdateMutation,
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
if (Number.isInteger(item.quantity)) {
// Also allow the update hook to remove an item if the quantity is lower than 1
if (item.quantity! < 1) {
return removeItemHandler.fetcher({
options: removeItemHandler.fetchOptions,
input: { itemId },
fetch,
})
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
})
}
const { checkoutLineItemsUpdate } = await fetch<
Mutation,
MutationCheckoutLineItemsUpdateArgs
>({
...options,
variables: {
checkoutId: getCheckoutId(),
lineItems: [
{
id: itemId,
quantity: item.quantity,
},
],
},
})
return checkoutToCart(checkoutLineItemsUpdate)
},
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) => <
T extends LineItem | undefined = undefined
>(
ctx: {
item?: T
wait?: number
} = {}
) => {
const { item } = ctx
const { mutate } = useCart() as any
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
if (!itemId || !productId || !variantId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({
input: {
item: {
productId,
variantId,
quantity: input.quantity,
},
itemId,
},
})
await mutate(data, false)
return data
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,28 @@
{
"schema": {
"${NEXT_PUBLIC_WOOCOMMERCE_SHOP_API_URL}": {}
},
"documents": [
{
"./framework/woocommerce/wp/**/*.{ts,tsx}": {
"noRequire": true
}
}
],
"generates": {
"./framework/woocommerce/schema.ts": {
"plugins": ["typescript", "typescript-operations"],
"config": {
"scalars": {
"ID": "string"
}
}
},
"./framework/woocommerce/schema.graphql": {
"plugins": ["schema-ast"]
}
},
"hooks": {
"afterAllFileWrite": ["prettier --write"]
}
}

View File

@ -0,0 +1,10 @@
{
"provider": "woocommerce",
"features": {
"cart": false,
"search": false,
"wishlist": false,
"customerAuth": false,
"customCheckout": false
}
}

View File

@ -0,0 +1,9 @@
export const WOOCOMMERCE_CHECKOUT_ID_COOKIE = 'woocommerce_checkoutId'
export const WOOCOMMERCE_CHECKOUT_URL_COOKIE = 'woocommerce_checkoutUrl'
export const WOOCOMMERCE_CUSTOMER_TOKEN_COOKIE = 'woocommerce_customerToken'
export const WOOCOMMERCE_COOKIE_EXPIRE = 30
export const API_URL = process.env.NEXT_PUBLIC_WOOCOMMERCE_SHOP_API_URL ?? '/'

View File

@ -0,0 +1,3 @@
// export { default as useCustomer } from './use-customer'
export {}

View File

@ -0,0 +1,34 @@
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import type { CustomerHook } from '../types/customer'
import { SWRHook } from '@commerce/utils/types'
import { getCustomerQuery, getCustomerToken } from '../utils'
import { GetCustomerQuery, GetCustomerQueryVariables } from '../schema'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = {
fetchOptions: {
query: getCustomerQuery,
},
async fetcher({ options, fetch }) {
const customerAccessToken = getCustomerToken()
if (customerAccessToken) {
const data = await fetch<GetCustomerQuery, GetCustomerQueryVariables>({
...options,
variables: { customerAccessToken: getCustomerToken() },
})
return data.customer
}
return null
},
useHook:
({ useData }) =>
(input) => {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
},
}

View File

@ -0,0 +1,21 @@
import { Fetcher } from '@commerce/utils/types'
import { API_URL } from './const'
import { handleFetchResponse } from './utils'
const fetcher: Fetcher = async ({
url = API_URL,
method = 'POST',
variables,
query,
}) => {
const { locale, ...vars } = variables ?? {}
return handleFetchResponse(
await fetch(url, {
method,
body: JSON.stringify({ query, variables: vars }),
headers: { 'Content-Type': 'application/json' },
})
)
}
export default fetcher

View File

@ -0,0 +1,9 @@
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
import { wooCommerceProvider, WooCommerceProvider } from './provider'
export { wooCommerceProvider }
export type { WooCommerceProvider }
export const CommerceProvider = getCommerceProvider(wooCommerceProvider)
export const useCommerce = () => useCoreCommerce<WooCommerceProvider>()

View File

@ -0,0 +1,8 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['localhost'],
},
}

View File

@ -0,0 +1,2 @@
export * from '@commerce/product/use-price'
export { default } from '@commerce/product/use-price'

View File

@ -0,0 +1,89 @@
import { SWRHook } from '@commerce/utils/types'
import useSearch, { UseSearch } from '@commerce/product/use-search'
import {
CollectionEdge,
GetAllProductsQuery,
GetProductsFromCollectionQueryVariables,
Product as ShopifyProduct,
ProductEdge,
} from '../schema'
import {
getAllProductsQuery,
getCollectionProductsQuery,
getSearchVariables,
normalizeProduct,
} from '../utils'
import type { SearchProductsHook } from '../types/product'
export type SearchProductsInput = {
search?: string
categoryId?: number
brandId?: number
sort?: string
locale?: string
}
export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: {
query: getAllProductsQuery,
},
async fetcher({ input, options, fetch }) {
const { categoryId, brandId } = input
const method = options?.method
const variables = getSearchVariables(input)
let products
// change the query to getCollectionProductsQuery when categoryId is set
if (categoryId) {
const data = await fetch<
CollectionEdge,
GetProductsFromCollectionQueryVariables
>({
query: getCollectionProductsQuery,
method,
variables,
})
// filter on client when brandId & categoryId are set since is not available on collection product query
products = brandId
? data.node?.products?.edges?.filter(
({ node: { vendor } }: ProductEdge) =>
vendor.replace(/\s+/g, '-').toLowerCase() === brandId
)
: data.node?.products?.edges
} else {
const data = await fetch<GetAllProductsQuery>({
query: options.query,
method,
variables,
})
products = data.products?.edges
}
return {
products: products?.map(({ node }) =>
normalizeProduct(node as ShopifyProduct)
),
found: !!products?.length,
}
},
useHook: ({ useData }) => (input = {}) => {
return useData({
input: [
['search', input.search],
['categoryId', input.categoryId],
['brandId', input.brandId],
['sort', input.sort],
['locale', input.locale],
],
swrOptions: {
revalidateOnFocus: false,
...input.swrOptions,
},
})
},
}

View File

@ -0,0 +1,17 @@
import { WOOCOMMERCE_CHECKOUT_ID_COOKIE } from './const'
// import { handler as useCart } from './cart/use-cart'
// import { handler as useAddItem } from './cart/use-add-item'
// import { handler as useUpdateItem } from './cart/use-update-item'
// import { handler as useRemoveItem } from './cart/use-remove-item'
import fetcher from './fetcher'
export const wooCommerceProvider = {
locale: 'en-us',
fetcher,
// cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
cartCookie: WOOCOMMERCE_CHECKOUT_ID_COOKIE,
}
export type WooCommerceProvider = typeof wooCommerceProvider

18379
framework/woocommerce/schema.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
import * as Core from '@commerce/types/cart'
export * from '@commerce/types/cart'
export type ShopifyCart = {}
/**
* Extend core cart types
*/
export type Cart = Core.Cart & {
lineItems: Core.LineItem[]
url?: string
}
export type CartTypes = Core.CartTypes
export type CartHooks = Core.CartHooks<CartTypes>
export type GetCartHook = CartHooks['getCart']
export type AddItemHook = CartHooks['addItem']
export type UpdateItemHook = CartHooks['updateItem']
export type RemoveItemHook = CartHooks['removeItem']
export type CartSchema = Core.CartSchema<CartTypes>
export type CartHandlers = Core.CartHandlers<CartTypes>
export type GetCartHandler = CartHandlers['getCart']
export type AddItemHandler = CartHandlers['addItem']
export type UpdateItemHandler = CartHandlers['updateItem']
export type RemoveItemHandler = CartHandlers['removeItem']

View File

@ -0,0 +1 @@
export * from '@commerce/types/checkout'

View File

@ -0,0 +1 @@
export * from '@commerce/types/common'

View File

@ -0,0 +1,5 @@
import * as Core from '@commerce/types/customer'
export * from '@commerce/types/customer'
export type CustomerSchema = Core.CustomerSchema

View File

@ -0,0 +1,25 @@
import * as Cart from './cart'
import * as Checkout from './checkout'
import * as Common from './common'
import * as Customer from './customer'
import * as Login from './login'
import * as Logout from './logout'
import * as Page from './page'
import * as Product from './product'
import * as Signup from './signup'
import * as Site from './site'
import * as Wishlist from './wishlist'
export type {
Cart,
Checkout,
Common,
Customer,
Login,
Logout,
Page,
Product,
Signup,
Site,
Wishlist,
}

View File

@ -0,0 +1,8 @@
import * as Core from '@commerce/types/login'
// import type { CustomerAccessTokenCreateInput } from '../schema'
export * from '@commerce/types/login'
export type LoginOperation = Core.LoginOperation & {
variables: unknown
}

View File

@ -0,0 +1 @@
export * from '@commerce/types/logout'

View File

@ -0,0 +1,11 @@
import * as Core from '@commerce/types/page'
export * from '@commerce/types/page'
export type Page = Core.Page
export type PageTypes = {
page: Page
}
export type GetAllPagesOperation = Core.GetAllPagesOperation<PageTypes>
export type GetPageOperation = Core.GetPageOperation<PageTypes>

View File

@ -0,0 +1 @@
export * from '@commerce/types/product'

View File

@ -0,0 +1 @@
export * from '@commerce/types/signup'

View File

@ -0,0 +1 @@
export * from '@commerce/types/site'

View File

@ -0,0 +1 @@
export * from '@commerce/types/wishlist'

View File

@ -0,0 +1,33 @@
import Cookies from 'js-cookie'
import {
SHOPIFY_CHECKOUT_ID_COOKIE,
SHOPIFY_CHECKOUT_URL_COOKIE,
SHOPIFY_COOKIE_EXPIRE,
} from '../const'
import checkoutCreateMutation from './mutations/checkout-create'
import { CheckoutCreatePayload } from '../schema'
export const checkoutCreate = async (
fetch: any
): Promise<CheckoutCreatePayload> => {
const data = await fetch({
query: checkoutCreateMutation,
})
const checkout = data.checkoutCreate?.checkout
const checkoutId = checkout?.id
if (checkoutId) {
const options = {
expires: SHOPIFY_COOKIE_EXPIRE,
}
Cookies.set(SHOPIFY_CHECKOUT_ID_COOKIE, checkoutId, options)
Cookies.set(SHOPIFY_CHECKOUT_URL_COOKIE, checkout.webUrl, options)
}
return checkout
}
export default checkoutCreate

View File

@ -0,0 +1,42 @@
import type { Cart } from '../types/cart'
import { CommerceError } from '@commerce/utils/errors'
import {
CheckoutLineItemsAddPayload,
CheckoutLineItemsRemovePayload,
CheckoutLineItemsUpdatePayload,
CheckoutCreatePayload,
CheckoutUserError,
Checkout,
Maybe,
} from '../schema'
import { normalizeCart } from './normalize'
import throwUserErrors from './throw-user-errors'
export type CheckoutQuery = {
checkout: Checkout
checkoutUserErrors?: Array<CheckoutUserError>
}
export type CheckoutPayload =
| CheckoutLineItemsAddPayload
| CheckoutLineItemsUpdatePayload
| CheckoutLineItemsRemovePayload
| CheckoutCreatePayload
| CheckoutQuery
const checkoutToCart = (checkoutPayload?: Maybe<CheckoutPayload>): Cart => {
const checkout = checkoutPayload?.checkout
throwUserErrors(checkoutPayload?.checkoutUserErrors)
if (!checkout) {
throw new CommerceError({
message: 'Missing checkout object from response',
})
}
return normalizeCart(checkout)
}
export default checkoutToCart

View File

@ -0,0 +1,21 @@
import Cookies, { CookieAttributes } from 'js-cookie'
import { SHOPIFY_COOKIE_EXPIRE, SHOPIFY_CUSTOMER_TOKEN_COOKIE } from '../const'
export const getCustomerToken = () => Cookies.get(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
export const setCustomerToken = (
token: string | null,
options?: CookieAttributes
) => {
if (!token) {
Cookies.remove(SHOPIFY_CUSTOMER_TOKEN_COOKIE)
} else {
Cookies.set(
SHOPIFY_CUSTOMER_TOKEN_COOKIE,
token,
options ?? {
expires: SHOPIFY_COOKIE_EXPIRE,
}
)
}
}

View File

@ -0,0 +1,44 @@
import {
GetAllProductVendorsQuery,
GetAllProductVendorsQueryVariables,
} from '../schema'
import { ShopifyConfig } from '../api'
import getAllProductVendors from './queries/get-all-product-vendors-query'
export type Brand = {
entityId: string
name: string
path: string
}
export type BrandEdge = {
node: Brand
}
export type Brands = BrandEdge[]
const getBrands = async (config: ShopifyConfig): Promise<BrandEdge[]> => {
const { data } = await config.fetch<
GetAllProductVendorsQuery,
GetAllProductVendorsQueryVariables
>(getAllProductVendors, {
variables: {
first: 250,
},
})
let vendorsStrings = data.products.edges.map(({ node: { vendor } }) => vendor)
return [...new Set(vendorsStrings)].map((v) => {
const id = v.replace(/\s+/g, '-').toLowerCase()
return {
node: {
entityId: id,
name: v,
path: `brands/${id}`,
},
}
})
}
export default getBrands

View File

@ -0,0 +1,34 @@
import type { Category } from '../types/site'
import { ShopifyConfig } from '../api'
import { CollectionEdge } from '../schema'
import { normalizeCategory } from './normalize'
import getSiteCollectionsQuery from './queries/get-all-collections-query'
const getCategories = async ({
fetch,
locale,
}: ShopifyConfig): Promise<Category[]> => {
const { data } = await fetch(
getSiteCollectionsQuery,
{
variables: {
first: 250,
},
},
{
...(locale && {
headers: {
'Accept-Language': locale,
},
}),
}
)
return (
data.collections?.edges?.map(({ node }: CollectionEdge) =>
normalizeCategory(node)
) ?? []
)
}
export default getCategories

View File

@ -0,0 +1,8 @@
import Cookies from 'js-cookie'
import { SHOPIFY_CHECKOUT_ID_COOKIE } from '../const'
const getCheckoutId = (id?: string) => {
return id ?? Cookies.get(SHOPIFY_CHECKOUT_ID_COOKIE)
}
export default getCheckoutId

View File

@ -0,0 +1,31 @@
import getSortVariables from './get-sort-variables'
import { SearchProductsBody } from '../types/product'
export const getSearchVariables = ({
brandId,
search,
categoryId,
sort,
locale,
}: SearchProductsBody) => {
let query = ''
if (search) {
query += `product_type:${search} OR title:${search} OR tag:${search} `
}
if (brandId) {
query += `${search ? 'AND ' : ''}vendor:${brandId}`
}
return {
categoryId,
query,
...getSortVariables(sort, !!categoryId),
...(locale && {
locale,
}),
}
}
export default getSearchVariables

View File

@ -0,0 +1,32 @@
const getSortVariables = (sort?: string, isCategory: boolean = false) => {
let output = {}
switch (sort) {
case 'price-asc':
output = {
sortKey: 'PRICE',
reverse: false,
}
break
case 'price-desc':
output = {
sortKey: 'PRICE',
reverse: true,
}
break
case 'trending-desc':
output = {
sortKey: 'BEST_SELLING',
reverse: false,
}
break
case 'latest-desc':
output = {
sortKey: isCategory ? 'CREATED' : 'CREATED_AT',
reverse: true,
}
break
}
return output
}
export default getSortVariables

View File

@ -0,0 +1,30 @@
import { FetcherOptions } from '@commerce/utils/types'
import throwUserErrors from './throw-user-errors'
import {
MutationCustomerActivateArgs,
MutationCustomerActivateByUrlArgs,
} from '../schema'
import { Mutation } from '../schema'
import { customerActivateByUrlMutation } from './mutations'
const handleAccountActivation = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
input: MutationCustomerActivateByUrlArgs
) => {
try {
const { customerActivateByUrl } = await fetch<
Mutation,
MutationCustomerActivateArgs
>({
query: customerActivateByUrlMutation,
variables: {
input,
},
})
throwUserErrors(customerActivateByUrl?.customerUserErrors)
} catch (error) {}
}
export default handleAccountActivation

View File

@ -0,0 +1,27 @@
import { FetcherError } from '@commerce/utils/errors'
export function getError(errors: any[] | null, status: number) {
errors = errors ?? [{ message: 'Failed to fetch WooCommerce API' }]
return new FetcherError({ errors, status })
}
export async function getAsyncError(res: Response) {
const data = await res.json()
return getError(data.errors, res.status)
}
const handleFetchResponse = async (res: Response) => {
if (res.ok) {
const { data, errors } = await res.json()
if (errors && errors.length) {
throw getError(errors, res.status)
}
return data
}
throw await getAsyncError(res)
}
export default handleFetchResponse

View File

@ -0,0 +1,36 @@
import { FetcherOptions } from '@commerce/utils/types'
import { CustomerAccessTokenCreateInput } from '../schema'
import { setCustomerToken } from './customer-token'
import { customerAccessTokenCreateMutation } from './mutations'
import throwUserErrors from './throw-user-errors'
const handleLogin = (data: any) => {
const response = data.customerAccessTokenCreate
throwUserErrors(response?.customerUserErrors)
const customerAccessToken = response?.customerAccessToken
const accessToken = customerAccessToken?.accessToken
if (accessToken) {
setCustomerToken(accessToken)
}
return customerAccessToken
}
export const handleAutomaticLogin = async (
fetch: <T = any, B = Body>(options: FetcherOptions<B>) => Promise<T>,
input: CustomerAccessTokenCreateInput
) => {
try {
const loginData = await fetch({
query: customerAccessTokenCreateMutation,
variables: {
input,
},
})
handleLogin(loginData)
} catch (error) {}
}
export default handleLogin

View File

@ -0,0 +1,15 @@
export { default as handleFetchResponse } from './handle-fetch-response'
// export { default as getSearchVariables } from './get-search-variables'
// export { default as getSortVariables } from './get-sort-variables'
// export { default as getBrands } from './get-brands'
// export { default as getCategories } from './get-categories'
// export { default as getCheckoutId } from './get-checkout-id'
// export { default as checkoutCreate } from './checkout-create'
// export { default as checkoutToCart } from './checkout-to-cart'
// export { default as handleLogin, handleAutomaticLogin } from './handle-login'
// export { default as handleAccountActivation } from './handle-account-activation'
// export { default as throwUserErrors } from './throw-user-errors'
export * from './queries'
export * from './mutations'
export * from './normalize'
// export * from './customer-token'

View File

@ -0,0 +1,18 @@
const associateCustomerWithCheckoutMutation = /* GraphQl */ `
mutation associateCustomerWithCheckout($checkoutId: ID!, $customerAccessToken: String!) {
checkoutCustomerAssociateV2(checkoutId: $checkoutId, customerAccessToken: $customerAccessToken) {
checkout {
id
}
checkoutUserErrors {
code
field
message
}
customer {
id
}
}
}
`
export default associateCustomerWithCheckoutMutation

View File

@ -0,0 +1,19 @@
import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutCreateMutation = /* GraphQL */ `
mutation checkoutCreate($input: CheckoutCreateInput = {}) {
checkoutCreate(input: $input) {
checkoutUserErrors {
code
field
message
}
checkout {
...checkoutDetails
}
}
}
${checkoutDetailsFragment}
`
export default checkoutCreateMutation

View File

@ -0,0 +1,22 @@
import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemAddMutation = /* GraphQL */ `
mutation checkoutLineItemAdd(
$checkoutId: ID!
$lineItems: [CheckoutLineItemInput!]!
) {
checkoutLineItemsAdd(checkoutId: $checkoutId, lineItems: $lineItems) {
checkoutUserErrors {
code
field
message
}
checkout {
...checkoutDetails
}
}
}
${checkoutDetailsFragment}
`
export default checkoutLineItemAddMutation

View File

@ -0,0 +1,21 @@
import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemRemoveMutation = /* GraphQL */ `
mutation checkoutLineItemRemove($checkoutId: ID!, $lineItemIds: [ID!]!) {
checkoutLineItemsRemove(
checkoutId: $checkoutId
lineItemIds: $lineItemIds
) {
checkoutUserErrors {
code
field
message
}
checkout {
...checkoutDetails
}
}
}
${checkoutDetailsFragment}
`
export default checkoutLineItemRemoveMutation

View File

@ -0,0 +1,22 @@
import { checkoutDetailsFragment } from '../queries/get-checkout-query'
const checkoutLineItemUpdateMutation = /* GraphQL */ `
mutation checkoutLineItemUpdate(
$checkoutId: ID!
$lineItems: [CheckoutLineItemUpdateInput!]!
) {
checkoutLineItemsUpdate(checkoutId: $checkoutId, lineItems: $lineItems) {
checkoutUserErrors {
code
field
message
}
checkout {
...checkoutDetails
}
}
}
${checkoutDetailsFragment}
`
export default checkoutLineItemUpdateMutation

View File

@ -0,0 +1,16 @@
const customerAccessTokenCreateMutation = /* GraphQL */ `
mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
customerAccessTokenCreate(input: $input) {
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerAccessTokenCreateMutation

View File

@ -0,0 +1,14 @@
const customerAccessTokenDeleteMutation = /* GraphQL */ `
mutation customerAccessTokenDelete($customerAccessToken: String!) {
customerAccessTokenDelete(customerAccessToken: $customerAccessToken) {
deletedAccessToken
deletedCustomerAccessTokenId
userErrors {
field
message
}
}
}
`
export default customerAccessTokenDeleteMutation

View File

@ -0,0 +1,19 @@
const customerActivateByUrlMutation = /* GraphQL */ `
mutation customerActivateByUrl($activationUrl: URL!, $password: String!) {
customerActivateByUrl(activationUrl: $activationUrl, password: $password) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerActivateByUrlMutation

View File

@ -0,0 +1,19 @@
const customerActivateMutation = /* GraphQL */ `
mutation customerActivate($id: ID!, $input: CustomerActivateInput!) {
customerActivate(id: $id, input: $input) {
customer {
id
}
customerAccessToken {
accessToken
expiresAt
}
customerUserErrors {
code
field
message
}
}
}
`
export default customerActivateMutation

View File

@ -0,0 +1,15 @@
const customerCreateMutation = /* GraphQL */ `
mutation customerCreate($input: CustomerCreateInput!) {
customerCreate(input: $input) {
customerUserErrors {
code
field
message
}
customer {
id
}
}
}
`
export default customerCreateMutation

View File

@ -0,0 +1,9 @@
export { default as customerCreateMutation } from './customer-create'
export { default as checkoutCreateMutation } from './checkout-create'
export { default as checkoutLineItemAddMutation } from './checkout-line-item-add'
export { default as checkoutLineItemUpdateMutation } from './checkout-line-item-update'
export { default as checkoutLineItemRemoveMutation } from './checkout-line-item-remove'
export { default as customerAccessTokenCreateMutation } from './customer-access-token-create'
export { default as customerAccessTokenDeleteMutation } from './customer-access-token-delete'
export { default as customerActivateMutation } from './customer-activate'
export { default as customerActivateByUrlMutation } from './customer-activate-by-url'

View File

@ -0,0 +1,58 @@
import type { Product, ProductImage } from '../types/product'
import { SimpleProduct, ProductToMediaItemConnection } from '../schema'
const normalizeProductImages = ({
edges,
}: ProductToMediaItemConnection): ProductImage[] => {
const edges_ =
edges
?.filter((edge) => edge?.node)
.map((edge) => {
return {
url: edge?.node?.sourceUrl ?? '',
alt: edge?.node?.altText ?? edge?.node?.title ?? '',
}
}) ?? []
return edges_
}
export function normalizeProduct({
id,
name,
sku,
description,
shortDescription,
slug,
image,
galleryImages,
price,
...rest
}: SimpleProduct): Product {
const images: ProductToMediaItemConnection = galleryImages ?? { edges: [] }
if (image) {
images.edges?.push({ node: image })
}
const product = {
id,
options: [],
variants: [],
name: name ?? id,
sku: sku ?? 'sku',
path: slug ?? id,
slug: slug?.replace(/^\/+|\/+$/g, ''),
images: normalizeProductImages(images),
price: { value: 0, currencyCode: 'USD' },
description: description ?? shortDescription ?? '',
descriptionHtml: description ?? shortDescription ?? '',
}
if (price) {
product.price.value = Number(price.substring(1))
}
return product
}

Some files were not shown because too many files have changed in this diff Show More