forked from crowetic/commerce
Merge branch 'master' of https://github.com/okbel/e-comm-example
This commit is contained in:
commit
16d6369b64
29
README.md
29
README.md
@ -1,7 +1,32 @@
|
|||||||
# Next.js Commerce
|
# Next.js Commerce
|
||||||
|
The all-in-one starter kit for high-performance e-commerce sites. With a few clicks, Next.js developers can clone, deploy and fully own their own store.
|
||||||
|
Start right now at nextjs.org/commerce
|
||||||
|
|
||||||
## Features
|
Demo live at: [commerce-demo.vercel.app](https://commerce-demo.vercel.app/)
|
||||||
|
|
||||||
## Todo
|
This project is currently under development.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
Ecommerce is one of the most important uses of the web and together we can raise the standard for ecommerce sites.
|
||||||
|
|
||||||
|
## Goals and Features
|
||||||
|
- Performant by default
|
||||||
|
- SEO Ready
|
||||||
|
- Internationalization
|
||||||
|
- Responsive
|
||||||
|
- UI Components
|
||||||
|
- Theming
|
||||||
|
- Standarized Data Hooks
|
||||||
|
- Integrations - Integrate seamlessly with the most common ecommerce platforms.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
Our Commitment to Open Source can be found [here](https://vercel.com/oss).
|
||||||
|
|
||||||
|
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
|
||||||
|
2. Create a new branch `git checkout -b MY_BRANCH_NAME`
|
||||||
|
3. Install yarn: `npm install -g yarn`
|
||||||
|
4. Install the dependencies: `yarn`
|
||||||
|
5. Run `yarn dev` to build and watch for code changes
|
||||||
|
7. The development branch is `development` (this is the branch pull requests should be made against).
|
||||||
|
On a release, the relevant parts of the changes in the `staging` branch are rebased into `master`.
|
||||||
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { FC, useEffect, useState, useCallback } from 'react'
|
import { FC, useEffect, useState, useCallback } from 'react'
|
||||||
import { validate } from 'email-validator'
|
import { validate } from 'email-validator'
|
||||||
import { Info } from '@components/icons'
|
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import { Logo, Button, Input } from '@components/ui'
|
import { Logo, Button, Input } from '@components/ui'
|
||||||
import useSignup from '@lib/bigcommerce/use-signup'
|
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
@ -15,27 +13,15 @@ const ForgotPassword: FC<Props> = () => {
|
|||||||
const [dirty, setDirty] = useState(false)
|
const [dirty, setDirty] = useState(false)
|
||||||
const [disabled, setDisabled] = useState(false)
|
const [disabled, setDisabled] = useState(false)
|
||||||
|
|
||||||
const signup = useSignup()
|
|
||||||
const { setModalView, closeModal } = useUI()
|
const { setModalView, closeModal } = useUI()
|
||||||
|
|
||||||
const handleSignup = async () => {
|
const handleResetPassword = async (e: React.SyntheticEvent<EventTarget>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
if (!dirty && !disabled) {
|
if (!dirty && !disabled) {
|
||||||
setDirty(true)
|
setDirty(true)
|
||||||
handleValidation()
|
handleValidation()
|
||||||
}
|
}
|
||||||
|
|
||||||
// try {
|
|
||||||
// setLoading(true)
|
|
||||||
// setMessage('')
|
|
||||||
// await signup({
|
|
||||||
// email,
|
|
||||||
// })
|
|
||||||
// setLoading(false)
|
|
||||||
// closeModal()
|
|
||||||
// } catch ({ errors }) {
|
|
||||||
// setMessage(errors[0].message)
|
|
||||||
// setLoading(false)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleValidation = useCallback(() => {
|
const handleValidation = useCallback(() => {
|
||||||
@ -50,7 +36,10 @@ const ForgotPassword: FC<Props> = () => {
|
|||||||
}, [handleValidation])
|
}, [handleValidation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 flex flex-col justify-between p-3">
|
<form
|
||||||
|
onSubmit={handleResetPassword}
|
||||||
|
className="w-80 flex flex-col justify-between p-3"
|
||||||
|
>
|
||||||
<div className="flex justify-center pb-12 ">
|
<div className="flex justify-center pb-12 ">
|
||||||
<Logo width="64px" height="64px" />
|
<Logo width="64px" height="64px" />
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +52,7 @@ const ForgotPassword: FC<Props> = () => {
|
|||||||
<div className="pt-2 w-full flex flex-col">
|
<div className="pt-2 w-full flex flex-col">
|
||||||
<Button
|
<Button
|
||||||
variant="slim"
|
variant="slim"
|
||||||
onClick={() => handleSignup()}
|
type="submit"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@ -82,7 +71,7 @@ const ForgotPassword: FC<Props> = () => {
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FC, useEffect, useState, useCallback } from 'react'
|
import { FC, useEffect, useState, useCallback } from 'react'
|
||||||
import { Logo, Modal, Button, Input } from '@components/ui'
|
import { Logo, Modal, Button, Input } from '@components/ui'
|
||||||
import useLogin from '@lib/bigcommerce/use-login'
|
import useLogin from '@bigcommerce/storefront-data-hooks/use-login'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import { validate } from 'email-validator'
|
import { validate } from 'email-validator'
|
||||||
|
|
||||||
@ -18,7 +18,9 @@ const LoginView: FC<Props> = () => {
|
|||||||
|
|
||||||
const login = useLogin()
|
const login = useLogin()
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async (e: React.SyntheticEvent<EventTarget>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
if (!dirty && !disabled) {
|
if (!dirty && !disabled) {
|
||||||
setDirty(true)
|
setDirty(true)
|
||||||
handleValidation()
|
handleValidation()
|
||||||
@ -54,7 +56,10 @@ const LoginView: FC<Props> = () => {
|
|||||||
}, [handleValidation])
|
}, [handleValidation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 flex flex-col justify-between p-3">
|
<form
|
||||||
|
onSubmit={handleLogin}
|
||||||
|
className="w-80 flex flex-col justify-between p-3"
|
||||||
|
>
|
||||||
<div className="flex justify-center pb-12 ">
|
<div className="flex justify-center pb-12 ">
|
||||||
<Logo width="64px" height="64px" />
|
<Logo width="64px" height="64px" />
|
||||||
</div>
|
</div>
|
||||||
@ -75,7 +80,7 @@ const LoginView: FC<Props> = () => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="slim"
|
variant="slim"
|
||||||
onClick={() => handleLogin()}
|
type="submit"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@ -92,7 +97,7 @@ const LoginView: FC<Props> = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import { validate } from 'email-validator'
|
|||||||
import { Info } from '@components/icons'
|
import { Info } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import { Logo, Button, Input } from '@components/ui'
|
import { Logo, Button, Input } from '@components/ui'
|
||||||
import useSignup from '@lib/bigcommerce/use-signup'
|
import useSignup from '@bigcommerce/storefront-data-hooks/use-signup'
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
@ -21,7 +21,9 @@ const SignUpView: FC<Props> = () => {
|
|||||||
const signup = useSignup()
|
const signup = useSignup()
|
||||||
const { setModalView, closeModal } = useUI()
|
const { setModalView, closeModal } = useUI()
|
||||||
|
|
||||||
const handleSignup = async () => {
|
const handleSignup = async (e: React.SyntheticEvent<EventTarget>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
if (!dirty && !disabled) {
|
if (!dirty && !disabled) {
|
||||||
setDirty(true)
|
setDirty(true)
|
||||||
handleValidation()
|
handleValidation()
|
||||||
@ -59,7 +61,10 @@ const SignUpView: FC<Props> = () => {
|
|||||||
}, [handleValidation])
|
}, [handleValidation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-80 flex flex-col justify-between p-3">
|
<form
|
||||||
|
onSubmit={handleSignup}
|
||||||
|
className="w-80 flex flex-col justify-between p-3"
|
||||||
|
>
|
||||||
<div className="flex justify-center pb-12 ">
|
<div className="flex justify-center pb-12 ">
|
||||||
<Logo width="64px" height="64px" />
|
<Logo width="64px" height="64px" />
|
||||||
</div>
|
</div>
|
||||||
@ -83,7 +88,7 @@ const SignUpView: FC<Props> = () => {
|
|||||||
<div className="pt-2 w-full flex flex-col">
|
<div className="pt-2 w-full flex flex-col">
|
||||||
<Button
|
<Button
|
||||||
variant="slim"
|
variant="slim"
|
||||||
onClick={() => handleSignup()}
|
type="submit"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
@ -102,7 +107,7 @@ const SignUpView: FC<Props> = () => {
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,9 +3,9 @@ import cn from 'classnames'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Trash, Plus, Minus } from '@components/icons'
|
import { Trash, Plus, Minus } from '@components/icons'
|
||||||
import usePrice from '@lib/bigcommerce/use-price'
|
import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
|
||||||
import useUpdateItem from '@lib/bigcommerce/cart/use-update-item'
|
import useUpdateItem from '@bigcommerce/storefront-data-hooks/cart/use-update-item'
|
||||||
import useRemoveItem from '@lib/bigcommerce/cart/use-remove-item'
|
import useRemoveItem from '@bigcommerce/storefront-data-hooks/cart/use-remove-item'
|
||||||
import s from './CartItem.module.css'
|
import s from './CartItem.module.css'
|
||||||
|
|
||||||
const CartItem = ({
|
const CartItem = ({
|
||||||
|
@ -2,10 +2,10 @@ import { FC } from 'react'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { UserNav } from '@components/core'
|
import { UserNav } from '@components/core'
|
||||||
import { Button } from '@components/ui'
|
import { Button } from '@components/ui'
|
||||||
import { ArrowLeft, Bag, Cross, Check } from '@components/icons'
|
import { Bag, Cross, Check } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import useCart from '@lib/bigcommerce/cart/use-cart'
|
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
||||||
import usePrice from '@lib/bigcommerce/use-price'
|
import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
|
||||||
import CartItem from '../CartItem'
|
import CartItem from '../CartItem'
|
||||||
import s from './CartSidebarView.module.css'
|
import s from './CartSidebarView.module.css'
|
||||||
|
|
||||||
@ -47,11 +47,11 @@ const CartSidebarView: FC = () => {
|
|||||||
aria-label="Close panel"
|
aria-label="Close panel"
|
||||||
className="hover:text-gray-500 transition ease-in-out duration-150"
|
className="hover:text-gray-500 transition ease-in-out duration-150"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-6 w-6" />
|
<Cross className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<UserNav />
|
<UserNav className="" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
9
components/core/Footer/Footer.module.css
Normal file
9
components/core/Footer/Footer.module.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.link {
|
||||||
|
& > svg {
|
||||||
|
@apply transform duration-75 ease-linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover > svg {
|
||||||
|
@apply scale-110;
|
||||||
|
}
|
||||||
|
}
|
@ -2,12 +2,12 @@ import { FC } from 'react'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||||
import getSlug from '@utils/get-slug'
|
import getSlug from '@utils/get-slug'
|
||||||
import { Github } from '@components/icons'
|
import { Github } from '@components/icons'
|
||||||
import { Logo, Container } from '@components/ui'
|
import { Logo, Container } from '@components/ui'
|
||||||
import { I18nWidget } from '@components/core'
|
import { I18nWidget } from '@components/core'
|
||||||
|
import s from './Footer.module.css'
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
@ -83,7 +83,9 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 lg:col-span-6 flex items-start lg:justify-end text-primary">
|
<div className="col-span-1 lg:col-span-6 flex items-start lg:justify-end text-primary">
|
||||||
<div className="flex space-x-6 items-center h-10">
|
<div className="flex space-x-6 items-center h-10">
|
||||||
<Github />
|
<a href="https://github.com/vercel/commerce" className={s.link}>
|
||||||
|
<Github />
|
||||||
|
</a>
|
||||||
<I18nWidget />
|
<I18nWidget />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,20 +4,20 @@
|
|||||||
@screen md {
|
@screen md {
|
||||||
@apply flex-row;
|
@apply flex-row;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.asideWrapper {
|
& .asideWrapper {
|
||||||
@apply pr-3 w-full relative;
|
@apply pr-3 w-full relative;
|
||||||
|
|
||||||
@screen md {
|
@screen md {
|
||||||
@apply w-48;
|
@apply w-48;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.aside {
|
& .aside {
|
||||||
@apply flex flex-row w-full justify-around mb-12;
|
@apply flex flex-row w-full justify-around mb-12;
|
||||||
|
|
||||||
@screen md {
|
@screen md {
|
||||||
@apply mb-0 block sticky top-32;
|
@apply mb-0 block sticky top-32;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,21 +6,52 @@ import { Menu } from '@headlessui/react'
|
|||||||
import { DoubleChevron } from '@components/icons'
|
import { DoubleChevron } from '@components/icons'
|
||||||
import s from './I18nWidget.module.css'
|
import s from './I18nWidget.module.css'
|
||||||
|
|
||||||
const LOCALES_MAP: Record<string, string> = {
|
interface LOCALE_DATA {
|
||||||
es: 'Español',
|
name: string
|
||||||
'en-US': 'English',
|
img: {
|
||||||
|
filename: string
|
||||||
|
alt: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOCALES_MAP: Record<string, LOCALE_DATA> = {
|
||||||
|
es: {
|
||||||
|
name: 'Español',
|
||||||
|
img: {
|
||||||
|
filename: 'flag-es-co.svg',
|
||||||
|
alt: 'Bandera Colombiana',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'en-US': {
|
||||||
|
name: 'English',
|
||||||
|
img: {
|
||||||
|
filename: 'flag-en-us.svg',
|
||||||
|
alt: 'US Flag',
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const I18nWidget: FC = () => {
|
const I18nWidget: FC = () => {
|
||||||
const { locale, locales, defaultLocale = 'en-US' } = useRouter()
|
const {
|
||||||
|
locale,
|
||||||
|
locales,
|
||||||
|
defaultLocale = 'en-US',
|
||||||
|
asPath: currentPath,
|
||||||
|
} = useRouter()
|
||||||
const options = locales?.filter((val) => val !== locale)
|
const options = locales?.filter((val) => val !== locale)
|
||||||
|
|
||||||
|
const currentLocale = locale || defaultLocale
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={s.root}>
|
<nav className={s.root}>
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Button className={s.button} aria-label="Language selector">
|
<Menu.Button className={s.button} aria-label="Language selector">
|
||||||
<img className="mr-2" src="/flag-us.png" alt="US Flag" />
|
<img
|
||||||
<span className="mr-2">{LOCALES_MAP[locale || defaultLocale]}</span>
|
className="block mr-2 w-5"
|
||||||
|
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
|
||||||
|
alt={LOCALES_MAP[currentLocale].img.alt}
|
||||||
|
/>
|
||||||
|
<span className="mr-2">{LOCALES_MAP[currentLocale].name}</span>
|
||||||
{options && (
|
{options && (
|
||||||
<span>
|
<span>
|
||||||
<DoubleChevron />
|
<DoubleChevron />
|
||||||
@ -33,9 +64,9 @@ const I18nWidget: FC = () => {
|
|||||||
{options.map((locale) => (
|
{options.map((locale) => (
|
||||||
<Menu.Item key={locale}>
|
<Menu.Item key={locale}>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<Link href="/" locale={locale}>
|
<Link href={currentPath} locale={locale}>
|
||||||
<a className={cn(s.item, { [s.active]: active })}>
|
<a className={cn(s.item, { [s.active]: active })}>
|
||||||
{LOCALES_MAP[locale]}
|
{LOCALES_MAP[locale].name}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { FC, useCallback, useEffect, useState } from 'react'
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import type { Page } from '@lib/bigcommerce/api/operations/get-all-pages'
|
import type { Page } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||||
import { CommerceProvider } from '@lib/bigcommerce'
|
import { CommerceProvider } from '@bigcommerce/storefront-data-hooks'
|
||||||
import { CartSidebarView } from '@components/cart'
|
import { CartSidebarView } from '@components/cart'
|
||||||
import { Container, Sidebar, Button, Modal, Toast } from '@components/ui'
|
import { Container, Sidebar, Button, Modal, Toast } from '@components/ui'
|
||||||
import { Navbar, Featurebar, Footer } from '@components/core'
|
import { Navbar, Featurebar, Footer } from '@components/core'
|
||||||
|
@ -4,8 +4,9 @@ import { useTheme } from 'next-themes'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './DropdownMenu.module.css'
|
import s from './DropdownMenu.module.css'
|
||||||
import { Moon, Sun } from '@components/icons'
|
import { Moon, Sun } from '@components/icons'
|
||||||
|
import { useUI } from '@components/ui/context'
|
||||||
import { Menu, Transition } from '@headlessui/react'
|
import { Menu, Transition } from '@headlessui/react'
|
||||||
import useLogout from '@lib/bigcommerce/use-logout'
|
import useLogout from '@bigcommerce/storefront-data-hooks/use-logout'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
interface DropdownMenuProps {
|
interface DropdownMenuProps {
|
||||||
@ -32,6 +33,8 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
|||||||
const logout = useLogout()
|
const logout = useLogout()
|
||||||
const { pathname } = useRouter()
|
const { pathname } = useRouter()
|
||||||
|
|
||||||
|
const { closeSidebarIfPresent } = useUI()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
@ -51,6 +54,7 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
|||||||
className={cn(s.link, {
|
className={cn(s.link, {
|
||||||
[s.active]: pathname === href,
|
[s.active]: pathname === href,
|
||||||
})}
|
})}
|
||||||
|
onClick={closeSidebarIfPresent}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</a>
|
</a>
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import s from './UserNav.module.css'
|
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
|
||||||
import { FC } from 'react'
|
import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer'
|
||||||
|
import { Menu } from '@headlessui/react'
|
||||||
import { Heart, Bag } from '@components/icons'
|
import { Heart, Bag } from '@components/icons'
|
||||||
import { Avatar } from '@components/core'
|
import { Avatar } from '@components/core'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
import DropdownMenu from './DropdownMenu'
|
import DropdownMenu from './DropdownMenu'
|
||||||
import { Menu } from '@headlessui/react'
|
import s from './UserNav.module.css'
|
||||||
import useCart from '@lib/bigcommerce/cart/use-cart'
|
|
||||||
import useCustomer from '@lib/bigcommerce/use-customer'
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
@ -21,22 +22,19 @@ const UserNav: FC<Props> = ({ className, children, ...props }) => {
|
|||||||
const { data } = useCart()
|
const { data } = useCart()
|
||||||
const { data: customer } = useCustomer()
|
const { data: customer } = useCustomer()
|
||||||
|
|
||||||
const { openSidebar, closeSidebar, displaySidebar, openModal } = useUI()
|
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
|
||||||
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
|
||||||
return (
|
return (
|
||||||
<nav className={cn(s.root, className)}>
|
<nav className={cn(s.root, className)}>
|
||||||
<div className={s.mainContainer}>
|
<div className={s.mainContainer}>
|
||||||
<ul className={s.list}>
|
<ul className={s.list}>
|
||||||
<li
|
<li className={s.item} onClick={toggleSidebar}>
|
||||||
className={s.item}
|
|
||||||
onClick={(e) => (displaySidebar ? closeSidebar() : openSidebar())}
|
|
||||||
>
|
|
||||||
<Bag />
|
<Bag />
|
||||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||||
</li>
|
</li>
|
||||||
<li className={s.item}>
|
<li className={s.item}>
|
||||||
<Link href="/wishlist">
|
<Link href="/wishlist">
|
||||||
<a>
|
<a onClick={closeSidebarIfPresent}>
|
||||||
<Heart />
|
<Heart />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
.root {
|
.root {
|
||||||
@apply relative w-full box-border overflow-hidden bg-no-repeat bg-center bg-cover transition ease-linear cursor-pointer;
|
@apply relative max-h-full w-full box-border overflow-hidden bg-no-repeat bg-center bg-cover transition ease-linear cursor-pointer;
|
||||||
|
height: 100% !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
& .squareBg:before {
|
& .squareBg:before {
|
||||||
@ -119,17 +120,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wishlistButton {
|
.wishlistButton {
|
||||||
@apply w-10 h-10 flex ml-auto flex items-center justify-center bg-primary text-primary font-semibold text-xs leading-6 cursor-pointer z-10;
|
@apply w-10 h-10 flex ml-auto items-center justify-center bg-primary text-primary font-semibold text-xs leading-6 cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.imageContainer {
|
.imageContainer {
|
||||||
@apply absolute z-10 inset-0 flex items-center justify-center;
|
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
@apply h-full;
|
|
||||||
& > div {
|
& > div {
|
||||||
@apply h-full;
|
height: 100%;
|
||||||
padding-bottom: 0 !important;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-image {
|
||||||
|
height: 120% !important;
|
||||||
|
top: -10% !important;
|
||||||
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
|
import type { ProductNode } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products'
|
||||||
import usePrice from '@lib/bigcommerce/use-price'
|
import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
|
||||||
import { EnhancedImage } from '@components/core'
|
import { EnhancedImage } from '@components/core'
|
||||||
import s from './ProductCard.module.css'
|
import s from './ProductCard.module.css'
|
||||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||||
@ -31,56 +31,56 @@ const ProductCard: FC<Props> = ({
|
|||||||
currencyCode: p.prices?.price?.currencyCode!,
|
currencyCode: p.prices?.price?.currencyCode!,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (variant === 'slim') {
|
|
||||||
return (
|
|
||||||
<div className="relative overflow-hidden box-border">
|
|
||||||
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
|
|
||||||
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
|
||||||
{p.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<EnhancedImage
|
|
||||||
src={p.images.edges?.[0]?.node.urlOriginal!}
|
|
||||||
alt={p.images.edges?.[0]?.node.altText || 'Product Image'}
|
|
||||||
width={imgWidth}
|
|
||||||
height={imgHeight}
|
|
||||||
priority={priority}
|
|
||||||
quality="90"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/product${p.path}`}>
|
<Link href={`/product${p.path}`}>
|
||||||
<a
|
<a
|
||||||
className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}
|
className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}
|
||||||
>
|
>
|
||||||
<div className={s.squareBg} />
|
{variant === 'slim' ? (
|
||||||
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
|
<div className="relative overflow-hidden box-border">
|
||||||
<div className="absolute top-0 left-0 pr-16 max-w-full">
|
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
|
||||||
<h3 className={s.productTitle}>
|
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
|
||||||
<span>{p.name}</span>
|
{p.name}
|
||||||
</h3>
|
</span>
|
||||||
<span className={s.productPrice}>{price}</span>
|
</div>
|
||||||
|
<EnhancedImage
|
||||||
|
src={p.images.edges?.[0]?.node.urlOriginal!}
|
||||||
|
alt={p.images.edges?.[0]?.node.altText || 'Product Image'}
|
||||||
|
width={imgWidth}
|
||||||
|
height={imgHeight}
|
||||||
|
priority={priority}
|
||||||
|
quality="90"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<WishlistButton
|
) : (
|
||||||
className={s.wishlistButton}
|
<>
|
||||||
productId={p.entityId}
|
<div className={s.squareBg} />
|
||||||
variant={p.variants.edges?.[0]!}
|
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
|
||||||
/>
|
<div className="absolute top-0 left-0 pr-16 max-w-full">
|
||||||
</div>
|
<h3 className={s.productTitle}>
|
||||||
<div className={cn(s.imageContainer)}>
|
<span>{p.name}</span>
|
||||||
<EnhancedImage
|
</h3>
|
||||||
alt={p.name}
|
<span className={s.productPrice}>{price}</span>
|
||||||
className={cn('w-full object-cover', s['product-image'])}
|
</div>
|
||||||
src={src}
|
<WishlistButton
|
||||||
width={imgWidth}
|
className={s.wishlistButton}
|
||||||
height={imgHeight}
|
productId={p.entityId}
|
||||||
priority={priority}
|
variant={p.variants.edges?.[0]!}
|
||||||
quality="90"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
<div className={s.imageContainer}>
|
||||||
|
<EnhancedImage
|
||||||
|
alt={p.name}
|
||||||
|
className={cn('w-full object-cover', s['product-image'])}
|
||||||
|
src={src}
|
||||||
|
width={imgWidth}
|
||||||
|
height={imgHeight}
|
||||||
|
priority={priority}
|
||||||
|
quality="90"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
@ -9,8 +9,8 @@ import { Swatch, ProductSlider } from '@components/product'
|
|||||||
import { Button, Container } from '@components/ui'
|
import { Button, Container } from '@components/ui'
|
||||||
import { HTMLContent } from '@components/core'
|
import { HTMLContent } from '@components/core'
|
||||||
|
|
||||||
import useAddItem from '@lib/bigcommerce/cart/use-add-item'
|
import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item'
|
||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
import type { ProductNode } from '@bigcommerce/storefront-data-hooks/api/operations/get-product'
|
||||||
import {
|
import {
|
||||||
getCurrentVariant,
|
getCurrentVariant,
|
||||||
getProductOptions,
|
getProductOptions,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-product'
|
import type { ProductNode } from '@bigcommerce/storefront-data-hooks/api/operations/get-product'
|
||||||
|
|
||||||
export type SelectedOptions = {
|
export type SelectedOptions = {
|
||||||
size: string | null
|
size: string | null
|
||||||
|
@ -131,6 +131,12 @@ export const UIProvider: FC = (props) => {
|
|||||||
|
|
||||||
const openSidebar = () => dispatch({ type: 'OPEN_SIDEBAR' })
|
const openSidebar = () => dispatch({ type: 'OPEN_SIDEBAR' })
|
||||||
const closeSidebar = () => dispatch({ type: 'CLOSE_SIDEBAR' })
|
const closeSidebar = () => dispatch({ type: 'CLOSE_SIDEBAR' })
|
||||||
|
const toggleSidebar = () =>
|
||||||
|
state.displaySidebar
|
||||||
|
? dispatch({ type: 'CLOSE_SIDEBAR' })
|
||||||
|
: dispatch({ type: 'OPEN_SIDEBAR' })
|
||||||
|
const closeSidebarIfPresent = () =>
|
||||||
|
state.displaySidebar && dispatch({ type: 'CLOSE_SIDEBAR' })
|
||||||
|
|
||||||
const openDropdown = () => dispatch({ type: 'OPEN_DROPDOWN' })
|
const openDropdown = () => dispatch({ type: 'OPEN_DROPDOWN' })
|
||||||
const closeDropdown = () => dispatch({ type: 'CLOSE_DROPDOWN' })
|
const closeDropdown = () => dispatch({ type: 'CLOSE_DROPDOWN' })
|
||||||
@ -149,6 +155,8 @@ export const UIProvider: FC = (props) => {
|
|||||||
...state,
|
...state,
|
||||||
openSidebar,
|
openSidebar,
|
||||||
closeSidebar,
|
closeSidebar,
|
||||||
|
toggleSidebar,
|
||||||
|
closeSidebarIfPresent,
|
||||||
openDropdown,
|
openDropdown,
|
||||||
closeDropdown,
|
closeDropdown,
|
||||||
openModal,
|
openModal,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { FC, useState } from 'react'
|
import React, { FC, useState } from 'react'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import type { ProductNode } from '@lib/bigcommerce/api/operations/get-all-products'
|
import type { ProductNode } from '@bigcommerce/storefront-data-hooks/api/operations/get-all-products'
|
||||||
import useAddItem from '@lib/bigcommerce/wishlist/use-add-item'
|
import useAddItem from '@bigcommerce/storefront-data-hooks/wishlist/use-add-item'
|
||||||
import useRemoveItem from '@lib/bigcommerce/wishlist/use-remove-item'
|
import useRemoveItem from '@bigcommerce/storefront-data-hooks/wishlist/use-remove-item'
|
||||||
import useWishlist from '@lib/bigcommerce/wishlist/use-wishlist'
|
import useWishlist from '@bigcommerce/storefront-data-hooks/wishlist/use-wishlist'
|
||||||
import useCustomer from '@lib/bigcommerce/use-customer'
|
import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer'
|
||||||
import { Heart } from '@components/icons'
|
import { Heart } from '@components/icons'
|
||||||
import { useUI } from '@components/ui/context'
|
import { useUI } from '@components/ui/context'
|
||||||
|
|
||||||
@ -62,9 +62,10 @@ const WishlistButton: FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
{...props}
|
aria-label="Add to wishlist"
|
||||||
className={cn({ 'opacity-50': loading }, className)}
|
className={cn({ 'opacity-50': loading }, className)}
|
||||||
onClick={handleWishlistChange}
|
onClick={handleWishlistChange}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
|
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
import { BigcommerceApiError } from '../../utils/errors'
|
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { Cart, CartHandlers } from '..'
|
|
||||||
|
|
||||||
// Return current cart info
|
|
||||||
const getCart: CartHandlers['getCart'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
let result: { data?: Cart } = {}
|
|
||||||
|
|
||||||
if (cartId) {
|
|
||||||
try {
|
|
||||||
result = await config.storeApiFetch(`/v3/carts/${cartId}`)
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof BigcommerceApiError && error.status === 404) {
|
|
||||||
// Remove the cookie if it exists but the cart wasn't found
|
|
||||||
res.setHeader('Set-Cookie', getCartCookie(config.cartCookie))
|
|
||||||
} else {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({ data: result.data ?? null })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getCart
|
|
@ -1,34 +0,0 @@
|
|||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { CartHandlers } from '..'
|
|
||||||
|
|
||||||
// Return current cart info
|
|
||||||
const removeItem: CartHandlers['removeItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId, itemId },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
if (!cartId || !itemId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await config.storeApiFetch<{ data: any } | null>(
|
|
||||||
`/v3/carts/${cartId}/items/${itemId}`,
|
|
||||||
{ method: 'DELETE' }
|
|
||||||
)
|
|
||||||
const data = result?.data ?? null
|
|
||||||
|
|
||||||
res.setHeader(
|
|
||||||
'Set-Cookie',
|
|
||||||
data
|
|
||||||
? // Update the cart cookie
|
|
||||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
|
||||||
: // Remove the cart cookie if the cart was removed (empty items)
|
|
||||||
getCartCookie(config.cartCookie)
|
|
||||||
)
|
|
||||||
res.status(200).json({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default removeItem
|
|
@ -1,36 +0,0 @@
|
|||||||
import { parseCartItem } from '../../utils/parse-item'
|
|
||||||
import getCartCookie from '../../utils/get-cart-cookie'
|
|
||||||
import type { CartHandlers } from '..'
|
|
||||||
|
|
||||||
// Return current cart info
|
|
||||||
const updateItem: CartHandlers['updateItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { cartId, itemId, item },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
if (!cartId || !itemId || !item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await config.storeApiFetch(
|
|
||||||
`/v3/carts/${cartId}/items/${itemId}`,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({
|
|
||||||
line_item: parseCartItem(item),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update the cart cookie
|
|
||||||
res.setHeader(
|
|
||||||
'Set-Cookie',
|
|
||||||
getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
|
|
||||||
)
|
|
||||||
res.status(200).json({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateItem
|
|
@ -1,110 +0,0 @@
|
|||||||
import isAllowedMethod from '../utils/is-allowed-method'
|
|
||||||
import createApiHandler, {
|
|
||||||
BigcommerceApiHandler,
|
|
||||||
BigcommerceHandler,
|
|
||||||
} from '../utils/create-api-handler'
|
|
||||||
import { BigcommerceApiError } from '../utils/errors'
|
|
||||||
import getCart from './handlers/get-cart'
|
|
||||||
import addItem from './handlers/add-item'
|
|
||||||
import updateItem from './handlers/update-item'
|
|
||||||
import removeItem from './handlers/remove-item'
|
|
||||||
|
|
||||||
export type ItemBody = {
|
|
||||||
productId: number
|
|
||||||
variantId: number
|
|
||||||
quantity?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AddItemBody = { item: ItemBody }
|
|
||||||
|
|
||||||
export type UpdateItemBody = { itemId: string; item: ItemBody }
|
|
||||||
|
|
||||||
export type RemoveItemBody = { itemId: string }
|
|
||||||
|
|
||||||
// TODO: this type should match:
|
|
||||||
// https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
|
|
||||||
export type Cart = {
|
|
||||||
id: string
|
|
||||||
parent_id?: string
|
|
||||||
customer_id: number
|
|
||||||
email: string
|
|
||||||
currency: { code: string }
|
|
||||||
tax_included: boolean
|
|
||||||
base_amount: number
|
|
||||||
discount_amount: number
|
|
||||||
cart_amount: number
|
|
||||||
line_items: {
|
|
||||||
custom_items: any[]
|
|
||||||
digital_items: any[]
|
|
||||||
gift_certificates: any[]
|
|
||||||
physical_items: any[]
|
|
||||||
}
|
|
||||||
// TODO: add missing fields
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CartHandlers = {
|
|
||||||
getCart: BigcommerceHandler<Cart, { cartId?: string }>
|
|
||||||
addItem: BigcommerceHandler<Cart, { cartId?: string } & Partial<AddItemBody>>
|
|
||||||
updateItem: BigcommerceHandler<
|
|
||||||
Cart,
|
|
||||||
{ cartId?: string } & Partial<UpdateItemBody>
|
|
||||||
>
|
|
||||||
removeItem: BigcommerceHandler<
|
|
||||||
Cart,
|
|
||||||
{ cartId?: string } & Partial<RemoveItemBody>
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
|
|
||||||
|
|
||||||
// TODO: a complete implementation should have schema validation for `req.body`
|
|
||||||
const cartApi: BigcommerceApiHandler<Cart, CartHandlers> = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
config,
|
|
||||||
handlers
|
|
||||||
) => {
|
|
||||||
if (!isAllowedMethod(req, res, METHODS)) return
|
|
||||||
|
|
||||||
const { cookies } = req
|
|
||||||
const cartId = cookies[config.cartCookie]
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Return current cart info
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
const body = { cartId }
|
|
||||||
return await handlers['getCart']({ req, res, config, body })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create or add an item to the cart
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
const body = { ...req.body, cartId }
|
|
||||||
return await handlers['addItem']({ req, res, config, body })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update item in cart
|
|
||||||
if (req.method === 'PUT') {
|
|
||||||
const body = { ...req.body, cartId }
|
|
||||||
return await handlers['updateItem']({ req, res, config, body })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove an item from the cart
|
|
||||||
if (req.method === 'DELETE') {
|
|
||||||
const body = { ...req.body, cartId }
|
|
||||||
return await handlers['removeItem']({ req, res, config, body })
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof BigcommerceApiError
|
|
||||||
? 'An unexpected error ocurred with the Bigcommerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handlers = { getCart, addItem, updateItem, removeItem }
|
|
||||||
|
|
||||||
export default createApiHandler(cartApi, handlers, {})
|
|
@ -1,73 +0,0 @@
|
|||||||
import getAllProducts, { ProductEdge } from '../../operations/get-all-products'
|
|
||||||
import type { ProductsHandlers } from '../products'
|
|
||||||
|
|
||||||
const SORT: { [key: string]: string | undefined } = {
|
|
||||||
latest: 'id',
|
|
||||||
trending: 'total_sold',
|
|
||||||
price: 'price',
|
|
||||||
}
|
|
||||||
const LIMIT = 12
|
|
||||||
|
|
||||||
// Return current cart info
|
|
||||||
const getProducts: ProductsHandlers['getProducts'] = async ({
|
|
||||||
res,
|
|
||||||
body: { search, category, brand, sort },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
// Use a dummy base as we only care about the relative path
|
|
||||||
const url = new URL('/v3/catalog/products', 'http://a')
|
|
||||||
|
|
||||||
url.searchParams.set('is_visible', 'true')
|
|
||||||
url.searchParams.set('limit', String(LIMIT))
|
|
||||||
|
|
||||||
if (search) url.searchParams.set('keyword', search)
|
|
||||||
|
|
||||||
if (category && Number.isInteger(Number(category)))
|
|
||||||
url.searchParams.set('categories:in', category)
|
|
||||||
|
|
||||||
if (brand && Number.isInteger(Number(brand)))
|
|
||||||
url.searchParams.set('brand_id', brand)
|
|
||||||
|
|
||||||
if (sort) {
|
|
||||||
const [_sort, direction] = sort.split('-')
|
|
||||||
const sortValue = SORT[_sort]
|
|
||||||
|
|
||||||
if (sortValue && direction) {
|
|
||||||
url.searchParams.set('sort', sortValue)
|
|
||||||
url.searchParams.set('direction', direction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only want the id of each product
|
|
||||||
url.searchParams.set('include_fields', 'id')
|
|
||||||
|
|
||||||
const { data } = await config.storeApiFetch<{ data: { id: number }[] }>(
|
|
||||||
url.pathname + url.search
|
|
||||||
)
|
|
||||||
const entityIds = data.map((p) => p.id)
|
|
||||||
const found = entityIds.length > 0
|
|
||||||
// We want the GraphQL version of each product
|
|
||||||
const graphqlData = await getAllProducts({
|
|
||||||
variables: { first: LIMIT, entityIds },
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
// Put the products in an object that we can use to get them by id
|
|
||||||
const productsById = graphqlData.products.reduce<{
|
|
||||||
[k: number]: ProductEdge
|
|
||||||
}>((prods, p) => {
|
|
||||||
prods[p.node.entityId] = p
|
|
||||||
return prods
|
|
||||||
}, {})
|
|
||||||
const products: ProductEdge[] = found ? [] : graphqlData.products
|
|
||||||
|
|
||||||
// Populate the products array with the graphql products, in the order
|
|
||||||
// assigned by the list of entity ids
|
|
||||||
entityIds.forEach((id) => {
|
|
||||||
const product = productsById[id]
|
|
||||||
if (product) products.push(product)
|
|
||||||
})
|
|
||||||
|
|
||||||
res.status(200).json({ data: { products, found } })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getProducts
|
|
@ -1,48 +0,0 @@
|
|||||||
import isAllowedMethod from '../utils/is-allowed-method'
|
|
||||||
import createApiHandler, {
|
|
||||||
BigcommerceApiHandler,
|
|
||||||
BigcommerceHandler,
|
|
||||||
} from '../utils/create-api-handler'
|
|
||||||
import { BigcommerceApiError } from '../utils/errors'
|
|
||||||
import type { ProductEdge } from '../operations/get-all-products'
|
|
||||||
import getProducts from './handlers/get-products'
|
|
||||||
|
|
||||||
export type SearchProductsData = {
|
|
||||||
products: ProductEdge[]
|
|
||||||
found: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProductsHandlers = {
|
|
||||||
getProducts: BigcommerceHandler<
|
|
||||||
SearchProductsData,
|
|
||||||
{ search?: 'string'; category?: string; brand?: string; sort?: string }
|
|
||||||
>
|
|
||||||
}
|
|
||||||
|
|
||||||
const METHODS = ['GET']
|
|
||||||
|
|
||||||
// TODO: a complete implementation should have schema validation for `req.body`
|
|
||||||
const productsApi: BigcommerceApiHandler<
|
|
||||||
SearchProductsData,
|
|
||||||
ProductsHandlers
|
|
||||||
> = async (req, res, config, handlers) => {
|
|
||||||
if (!isAllowedMethod(req, res, METHODS)) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = req.query
|
|
||||||
return await handlers['getProducts']({ req, res, config, body })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof BigcommerceApiError
|
|
||||||
? 'An unexpected error ocurred with the Bigcommerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handlers = { getProducts }
|
|
||||||
|
|
||||||
export default createApiHandler(productsApi, handlers, {})
|
|
@ -1,59 +0,0 @@
|
|||||||
import type { GetLoggedInCustomerQuery } from '../../../schema'
|
|
||||||
import type { CustomersHandlers } from '..'
|
|
||||||
|
|
||||||
export const getLoggedInCustomerQuery = /* GraphQL */ `
|
|
||||||
query getLoggedInCustomer {
|
|
||||||
customer {
|
|
||||||
entityId
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
email
|
|
||||||
company
|
|
||||||
customerGroupId
|
|
||||||
notes
|
|
||||||
phone
|
|
||||||
addressCount
|
|
||||||
attributeCount
|
|
||||||
storeCredit {
|
|
||||||
value
|
|
||||||
currencyCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export type Customer = NonNullable<GetLoggedInCustomerQuery['customer']>
|
|
||||||
|
|
||||||
const getLoggedInCustomer: CustomersHandlers['getLoggedInCustomer'] = async ({
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
const token = req.cookies[config.customerCookie]
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
const { data } = await config.fetch<GetLoggedInCustomerQuery>(
|
|
||||||
getLoggedInCustomerQuery,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
cookie: `${config.customerCookie}=${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const { customer } = data
|
|
||||||
|
|
||||||
if (!customer) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Customer not found', code: 'not_found' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({ data: { customer } })
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({ data: null })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getLoggedInCustomer
|
|
@ -1,49 +0,0 @@
|
|||||||
import { FetcherError } from '../../../../commerce/utils/errors'
|
|
||||||
import login from '../../operations/login'
|
|
||||||
import type { LoginHandlers } from '../login'
|
|
||||||
|
|
||||||
const invalidCredentials = /invalid credentials/i
|
|
||||||
|
|
||||||
const loginHandler: LoginHandlers['login'] = async ({
|
|
||||||
res,
|
|
||||||
body: { email, password },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
// TODO: Add proper validations with something like Ajv
|
|
||||||
if (!(email && password)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// TODO: validate the password and email
|
|
||||||
// Passwords must be at least 7 characters and contain both alphabetic
|
|
||||||
// and numeric characters.
|
|
||||||
|
|
||||||
try {
|
|
||||||
await login({ variables: { email, password }, config, res })
|
|
||||||
} catch (error) {
|
|
||||||
// Check if the email and password didn't match an existing account
|
|
||||||
if (
|
|
||||||
error instanceof FetcherError &&
|
|
||||||
invalidCredentials.test(error.message)
|
|
||||||
) {
|
|
||||||
return res.status(401).json({
|
|
||||||
data: null,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
'Cannot find an account that matches the provided credentials',
|
|
||||||
code: 'invalid_credentials',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({ data: null })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default loginHandler
|
|
@ -1,23 +0,0 @@
|
|||||||
import { serialize } from 'cookie'
|
|
||||||
import { LogoutHandlers } from '../logout'
|
|
||||||
|
|
||||||
const logoutHandler: LogoutHandlers['logout'] = async ({
|
|
||||||
res,
|
|
||||||
body: { redirectTo },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
// Remove the cookie
|
|
||||||
res.setHeader(
|
|
||||||
'Set-Cookie',
|
|
||||||
serialize(config.customerCookie, '', { maxAge: -1, path: '/' })
|
|
||||||
)
|
|
||||||
|
|
||||||
// Only allow redirects to a relative URL
|
|
||||||
if (redirectTo?.startsWith('/')) {
|
|
||||||
res.redirect(redirectTo)
|
|
||||||
} else {
|
|
||||||
res.status(200).json({ data: null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default logoutHandler
|
|
@ -1,62 +0,0 @@
|
|||||||
import { BigcommerceApiError } from '../../utils/errors'
|
|
||||||
import login from '../../operations/login'
|
|
||||||
import { SignupHandlers } from '../signup'
|
|
||||||
|
|
||||||
const signup: SignupHandlers['signup'] = async ({
|
|
||||||
res,
|
|
||||||
body: { firstName, lastName, email, password },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
// TODO: Add proper validations with something like Ajv
|
|
||||||
if (!(firstName && lastName && email && password)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// TODO: validate the password and email
|
|
||||||
// Passwords must be at least 7 characters and contain both alphabetic
|
|
||||||
// and numeric characters.
|
|
||||||
|
|
||||||
try {
|
|
||||||
await config.storeApiFetch('/v3/customers', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify([
|
|
||||||
{
|
|
||||||
first_name: firstName,
|
|
||||||
last_name: lastName,
|
|
||||||
email,
|
|
||||||
authentication: {
|
|
||||||
new_password: password,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof BigcommerceApiError && error.status === 422) {
|
|
||||||
const hasEmailError = '0.email' in error.data?.errors
|
|
||||||
|
|
||||||
// If there's an error with the email, it most likely means it's duplicated
|
|
||||||
if (hasEmailError) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
message: 'The email is already in use',
|
|
||||||
code: 'duplicated_email',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login the customer right after creating it
|
|
||||||
await login({ variables: { email, password }, res, config })
|
|
||||||
|
|
||||||
res.status(200).json({ data: null })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default signup
|
|
@ -1,46 +0,0 @@
|
|||||||
import createApiHandler, {
|
|
||||||
BigcommerceApiHandler,
|
|
||||||
BigcommerceHandler,
|
|
||||||
} from '../utils/create-api-handler'
|
|
||||||
import isAllowedMethod from '../utils/is-allowed-method'
|
|
||||||
import { BigcommerceApiError } from '../utils/errors'
|
|
||||||
import getLoggedInCustomer, {
|
|
||||||
Customer,
|
|
||||||
} from './handlers/get-logged-in-customer'
|
|
||||||
|
|
||||||
export type { Customer }
|
|
||||||
|
|
||||||
export type CustomerData = {
|
|
||||||
customer: Customer
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CustomersHandlers = {
|
|
||||||
getLoggedInCustomer: BigcommerceHandler<CustomerData>
|
|
||||||
}
|
|
||||||
|
|
||||||
const METHODS = ['GET']
|
|
||||||
|
|
||||||
const customersApi: BigcommerceApiHandler<
|
|
||||||
CustomerData,
|
|
||||||
CustomersHandlers
|
|
||||||
> = async (req, res, config, handlers) => {
|
|
||||||
if (!isAllowedMethod(req, res, METHODS)) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = null
|
|
||||||
return await handlers['getLoggedInCustomer']({ req, res, config, body })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof BigcommerceApiError
|
|
||||||
? 'An unexpected error ocurred with the Bigcommerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlers = { getLoggedInCustomer }
|
|
||||||
|
|
||||||
export default createApiHandler(customersApi, handlers, {})
|
|
@ -1,45 +0,0 @@
|
|||||||
import createApiHandler, {
|
|
||||||
BigcommerceApiHandler,
|
|
||||||
BigcommerceHandler,
|
|
||||||
} from '../utils/create-api-handler'
|
|
||||||
import isAllowedMethod from '../utils/is-allowed-method'
|
|
||||||
import { BigcommerceApiError } from '../utils/errors'
|
|
||||||
import login from './handlers/login'
|
|
||||||
|
|
||||||
export type LoginBody = {
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LoginHandlers = {
|
|
||||||
login: BigcommerceHandler<null, Partial<LoginBody>>
|
|
||||||
}
|
|
||||||
|
|
||||||
const METHODS = ['POST']
|
|
||||||
|
|
||||||
const loginApi: BigcommerceApiHandler<null, LoginHandlers> = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
config,
|
|
||||||
handlers
|
|
||||||
) => {
|
|
||||||
if (!isAllowedMethod(req, res, METHODS)) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = req.body ?? {}
|
|
||||||
return await handlers['login']({ req, res, config, body })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof BigcommerceApiError
|
|
||||||
? 'An unexpected error ocurred with the Bigcommerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlers = { login }
|
|
||||||
|
|
||||||
export default createApiHandler(loginApi, handlers, {})
|
|
@ -1,42 +0,0 @@
|
|||||||
import createApiHandler, {
|
|
||||||
BigcommerceApiHandler,
|
|
||||||
BigcommerceHandler,
|
|
||||||
} from '../utils/create-api-handler'
|
|
||||||
import isAllowedMethod from '../utils/is-allowed-method'
|
|
||||||
import { BigcommerceApiError } from '../utils/errors'
|
|
||||||
import logout from './handlers/logout'
|
|
||||||
|
|
||||||
export type LogoutHandlers = {
|
|
||||||
logout: BigcommerceHandler<null, { redirectTo?: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
const METHODS = ['GET']
|
|
||||||
|
|
||||||
const logoutApi: BigcommerceApiHandler<null, LogoutHandlers> = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
config,
|
|
||||||
handlers
|
|
||||||
) => {
|
|
||||||
if (!isAllowedMethod(req, res, METHODS)) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const redirectTo = req.query.redirect_to
|
|
||||||
const body = typeof redirectTo === 'string' ? { redirectTo } : {}
|
|
||||||
|
|
||||||
return await handlers['logout']({ req, res, config, body })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof BigcommerceApiError
|
|
||||||
? 'An unexpected error ocurred with the Bigcommerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlers = { logout }
|
|
||||||
|
|
||||||
export default createApiHandler(logoutApi, handlers, {})
|
|
@ -1,50 +0,0 @@
|
|||||||
import createApiHandler, {
|
|
||||||
BigcommerceApiHandler,
|
|
||||||
BigcommerceHandler,
|
|
||||||
} from '../utils/create-api-handler'
|
|
||||||
import isAllowedMethod from '../utils/is-allowed-method'
|
|
||||||
import { BigcommerceApiError } from '../utils/errors'
|
|
||||||
import signup from './handlers/signup'
|
|
||||||
|
|
||||||
export type SignupBody = {
|
|
||||||
firstName: string
|
|
||||||
lastName: string
|
|
||||||
email: string
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SignupHandlers = {
|
|
||||||
signup: BigcommerceHandler<null, { cartId?: string } & Partial<SignupBody>>
|
|
||||||
}
|
|
||||||
|
|
||||||
const METHODS = ['POST']
|
|
||||||
|
|
||||||
const signupApi: BigcommerceApiHandler<null, SignupHandlers> = async (
|
|
||||||
req,
|
|
||||||
res,
|
|
||||||
config,
|
|
||||||
handlers
|
|
||||||
) => {
|
|
||||||
if (!isAllowedMethod(req, res, METHODS)) return
|
|
||||||
|
|
||||||
const { cookies } = req
|
|
||||||
const cartId = cookies[config.cartCookie]
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = { ...req.body, cartId }
|
|
||||||
return await handlers['signup']({ req, res, config, body })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error instanceof BigcommerceApiError
|
|
||||||
? 'An unexpected error ocurred with the Bigcommerce API'
|
|
||||||
: 'An unexpected error ocurred'
|
|
||||||
|
|
||||||
res.status(500).json({ data: null, errors: [{ message }] })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlers = { signup }
|
|
||||||
|
|
||||||
export default createApiHandler(signupApi, handlers, {})
|
|
File diff suppressed because it is too large
Load Diff
@ -1,329 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file was auto-generated by swagger-to-ts.
|
|
||||||
* Do not make direct changes to the file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface definitions {
|
|
||||||
blogPost_Full: {
|
|
||||||
/**
|
|
||||||
* ID of this blog post. (READ-ONLY)
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
} & definitions['blogPost_Base']
|
|
||||||
addresses: {
|
|
||||||
/**
|
|
||||||
* Full URL of where the resource is located.
|
|
||||||
*/
|
|
||||||
url?: string
|
|
||||||
/**
|
|
||||||
* Resource being accessed.
|
|
||||||
*/
|
|
||||||
resource?: string
|
|
||||||
}
|
|
||||||
formField: {
|
|
||||||
/**
|
|
||||||
* Name of the form field
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* Value of the form field
|
|
||||||
*/
|
|
||||||
value?: string
|
|
||||||
}
|
|
||||||
page_Full: {
|
|
||||||
/**
|
|
||||||
* ID of the page.
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
} & definitions['page_Base']
|
|
||||||
redirect: {
|
|
||||||
/**
|
|
||||||
* Numeric ID of the redirect.
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* The path from which to redirect.
|
|
||||||
*/
|
|
||||||
path: string
|
|
||||||
forward: definitions['forward']
|
|
||||||
/**
|
|
||||||
* URL of the redirect. READ-ONLY
|
|
||||||
*/
|
|
||||||
url?: string
|
|
||||||
}
|
|
||||||
forward: {
|
|
||||||
/**
|
|
||||||
* The type of redirect. If it is a `manual` redirect then type will always be manual. Dynamic redirects will have the type of the page. Such as product or category.
|
|
||||||
*/
|
|
||||||
type?: string
|
|
||||||
/**
|
|
||||||
* Reference of the redirect. Dynamic redirects will have the category or product number. Manual redirects will have the url that is being directed to.
|
|
||||||
*/
|
|
||||||
ref?: number
|
|
||||||
}
|
|
||||||
customer_Full: {
|
|
||||||
/**
|
|
||||||
* Unique numeric ID of this customer. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* Not returned in any responses, but accepts up to two fields allowing you to set the customer’s password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation.
|
|
||||||
*/
|
|
||||||
_authentication?: {
|
|
||||||
force_reset?: string
|
|
||||||
password?: string
|
|
||||||
password_confirmation?: string
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The name of the company for which the customer works.
|
|
||||||
*/
|
|
||||||
company?: string
|
|
||||||
/**
|
|
||||||
* First name of the customer.
|
|
||||||
*/
|
|
||||||
first_name: string
|
|
||||||
/**
|
|
||||||
* Last name of the customer.
|
|
||||||
*/
|
|
||||||
last_name: string
|
|
||||||
/**
|
|
||||||
* Email address of the customer.
|
|
||||||
*/
|
|
||||||
email: string
|
|
||||||
/**
|
|
||||||
* Phone number of the customer.
|
|
||||||
*/
|
|
||||||
phone?: string
|
|
||||||
/**
|
|
||||||
* Date on which the customer registered from the storefront or was created in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
date_created?: string
|
|
||||||
/**
|
|
||||||
* Date on which the customer updated their details in the storefront or was updated in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
date_modified?: string
|
|
||||||
/**
|
|
||||||
* The amount of credit the customer has. (Float, Float as String, Integer)
|
|
||||||
*/
|
|
||||||
store_credit?: string
|
|
||||||
/**
|
|
||||||
* The customer’s IP address when they signed up.
|
|
||||||
*/
|
|
||||||
registration_ip_address?: string
|
|
||||||
/**
|
|
||||||
* The group to which the customer belongs.
|
|
||||||
*/
|
|
||||||
customer_group_id?: number
|
|
||||||
/**
|
|
||||||
* Store-owner notes on the customer.
|
|
||||||
*/
|
|
||||||
notes?: string
|
|
||||||
/**
|
|
||||||
* Used to identify customers who fall into special sales-tax categories – in particular, those who are fully or partially exempt from paying sales tax. Can be blank, or can contain a single AvaTax code. (The codes are case-sensitive.) Stores that subscribe to BigCommerce’s Avalara Premium integration will use this code to determine how/whether to apply sales tax. Does not affect sales-tax calculations for stores that do not subscribe to Avalara Premium.
|
|
||||||
*/
|
|
||||||
tax_exempt_category?: string
|
|
||||||
/**
|
|
||||||
* Records whether the customer would like to receive marketing content from this store. READ-ONLY.This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
accepts_marketing?: boolean
|
|
||||||
addresses?: definitions['addresses']
|
|
||||||
/**
|
|
||||||
* Array of custom fields. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
|
|
||||||
*/
|
|
||||||
form_fields?: definitions['formField'][]
|
|
||||||
/**
|
|
||||||
* Force a password change on next login.
|
|
||||||
*/
|
|
||||||
reset_pass_on_login?: boolean
|
|
||||||
}
|
|
||||||
categoryAccessLevel: {
|
|
||||||
/**
|
|
||||||
* + `all` - Customers can access all categories
|
|
||||||
* + `specific` - Customers can access a specific list of categories
|
|
||||||
* + `none` - Customers are prevented from viewing any of the categories in this group.
|
|
||||||
*/
|
|
||||||
type?: 'all' | 'specific' | 'none'
|
|
||||||
/**
|
|
||||||
* Is an array of category IDs and should be supplied only if `type` is specific.
|
|
||||||
*/
|
|
||||||
categories?: string[]
|
|
||||||
}
|
|
||||||
timeZone: {
|
|
||||||
/**
|
|
||||||
* a string identifying the time zone, in the format: <Continent-name>/<City-name>.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* a negative or positive number, identifying the offset from UTC/GMT, in seconds, during winter/standard time.
|
|
||||||
*/
|
|
||||||
raw_offset?: number
|
|
||||||
/**
|
|
||||||
* "-/+" offset from UTC/GMT, in seconds, during summer/daylight saving time.
|
|
||||||
*/
|
|
||||||
dst_offset?: number
|
|
||||||
/**
|
|
||||||
* a boolean indicating whether this time zone observes daylight saving time.
|
|
||||||
*/
|
|
||||||
dst_correction?: boolean
|
|
||||||
date_format?: definitions['dateFormat']
|
|
||||||
}
|
|
||||||
count_Response: { count?: number }
|
|
||||||
dateFormat: {
|
|
||||||
/**
|
|
||||||
* string that defines dates’ display format, in the pattern: M jS Y
|
|
||||||
*/
|
|
||||||
display?: string
|
|
||||||
/**
|
|
||||||
* string that defines the CSV export format for orders, customers, and products, in the pattern: M jS Y
|
|
||||||
*/
|
|
||||||
export?: string
|
|
||||||
/**
|
|
||||||
* string that defines dates’ extended-display format, in the pattern: M jS Y @ g:i A.
|
|
||||||
*/
|
|
||||||
extended_display?: string
|
|
||||||
}
|
|
||||||
blogTags: { tag?: string; post_ids?: number[] }[]
|
|
||||||
blogPost_Base: {
|
|
||||||
/**
|
|
||||||
* Title of this blog post.
|
|
||||||
*/
|
|
||||||
title: string
|
|
||||||
/**
|
|
||||||
* URL for the public blog post.
|
|
||||||
*/
|
|
||||||
url?: string
|
|
||||||
/**
|
|
||||||
* URL to preview the blog post. (READ-ONLY)
|
|
||||||
*/
|
|
||||||
preview_url?: string
|
|
||||||
/**
|
|
||||||
* Text body of the blog post.
|
|
||||||
*/
|
|
||||||
body: string
|
|
||||||
/**
|
|
||||||
* Tags to characterize the blog post.
|
|
||||||
*/
|
|
||||||
tags?: string[]
|
|
||||||
/**
|
|
||||||
* Summary of the blog post. (READ-ONLY)
|
|
||||||
*/
|
|
||||||
summary?: string
|
|
||||||
/**
|
|
||||||
* Whether the blog post is published.
|
|
||||||
*/
|
|
||||||
is_published?: boolean
|
|
||||||
published_date?: definitions['publishedDate']
|
|
||||||
/**
|
|
||||||
* Published date in `ISO 8601` format.
|
|
||||||
*/
|
|
||||||
published_date_iso8601?: string
|
|
||||||
/**
|
|
||||||
* Description text for this blog post’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_description?: string
|
|
||||||
/**
|
|
||||||
* Keywords for this blog post’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_keywords?: string
|
|
||||||
/**
|
|
||||||
* Name of the blog post’s author.
|
|
||||||
*/
|
|
||||||
author?: string
|
|
||||||
/**
|
|
||||||
* Local path to a thumbnail uploaded to `product_images/` via [WebDav](https://support.bigcommerce.com/s/article/File-Access-WebDAV).
|
|
||||||
*/
|
|
||||||
thumbnail_path?: string
|
|
||||||
}
|
|
||||||
publishedDate: { timezone_type?: string; date?: string; timezone?: string }
|
|
||||||
/**
|
|
||||||
* Not returned in any responses, but accepts up to two fields allowing you to set the customer’s password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation.
|
|
||||||
*/
|
|
||||||
authentication: {
|
|
||||||
force_reset?: string
|
|
||||||
password?: string
|
|
||||||
password_confirmation?: string
|
|
||||||
}
|
|
||||||
customer_Base: { [key: string]: any }
|
|
||||||
page_Base: {
|
|
||||||
/**
|
|
||||||
* ID of any parent Web page.
|
|
||||||
*/
|
|
||||||
parent_id?: number
|
|
||||||
/**
|
|
||||||
* `page`: free-text page
|
|
||||||
* `link`: link to another web address
|
|
||||||
* `rss_feed`: syndicated content from an RSS feed
|
|
||||||
* `contact_form`: When the store's contact form is used.
|
|
||||||
*/
|
|
||||||
type: 'page' | 'rss_feed' | 'contact_form' | 'raw' | 'link'
|
|
||||||
/**
|
|
||||||
* Where the page’s type is a contact form: object whose members are the fields enabled (in the control panel) for storefront display. Possible members are:`fullname`: full name of the customer submitting the form; `phone`: customer’s phone number, as submitted on the form; `companyname`: customer’s submitted company name; `orderno`: customer’s submitted order number; `rma`: customer’s submitted RMA (Return Merchandise Authorization) number.
|
|
||||||
*/
|
|
||||||
contact_fields?: string
|
|
||||||
/**
|
|
||||||
* Where the page’s type is a contact form: email address that receives messages sent via the form.
|
|
||||||
*/
|
|
||||||
email?: string
|
|
||||||
/**
|
|
||||||
* Page name, as displayed on the storefront.
|
|
||||||
*/
|
|
||||||
name: string
|
|
||||||
/**
|
|
||||||
* Relative URL on the storefront for this page.
|
|
||||||
*/
|
|
||||||
url?: string
|
|
||||||
/**
|
|
||||||
* Description contained within this page’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_description?: string
|
|
||||||
/**
|
|
||||||
* HTML or variable that populates this page’s `<body>` element, in default/desktop view. Required in POST if page type is `raw`.
|
|
||||||
*/
|
|
||||||
body: string
|
|
||||||
/**
|
|
||||||
* HTML to use for this page's body when viewed in the mobile template (deprecated).
|
|
||||||
*/
|
|
||||||
mobile_body?: string
|
|
||||||
/**
|
|
||||||
* If true, this page has a mobile version.
|
|
||||||
*/
|
|
||||||
has_mobile_version?: boolean
|
|
||||||
/**
|
|
||||||
* If true, this page appears in the storefront’s navigation menu.
|
|
||||||
*/
|
|
||||||
is_visible?: boolean
|
|
||||||
/**
|
|
||||||
* If true, this page is the storefront’s home page.
|
|
||||||
*/
|
|
||||||
is_homepage?: boolean
|
|
||||||
/**
|
|
||||||
* Text specified for this page’s `<title>` element. (If empty, the value of the name property is used.)
|
|
||||||
*/
|
|
||||||
meta_title?: string
|
|
||||||
/**
|
|
||||||
* Layout template for this page. This field is writable only for stores with a Blueprint theme applied.
|
|
||||||
*/
|
|
||||||
layout_file?: string
|
|
||||||
/**
|
|
||||||
* Order in which this page should display on the storefront. (Lower integers specify earlier display.)
|
|
||||||
*/
|
|
||||||
sort_order?: number
|
|
||||||
/**
|
|
||||||
* Comma-separated list of keywords that shoppers can use to locate this page when searching the store.
|
|
||||||
*/
|
|
||||||
search_keywords?: string
|
|
||||||
/**
|
|
||||||
* Comma-separated list of SEO-relevant keywords to include in the page’s `<meta/>` element.
|
|
||||||
*/
|
|
||||||
meta_keywords?: string
|
|
||||||
/**
|
|
||||||
* If page type is `rss_feed` the n this field is visisble. Required in POST required for `rss page` type.
|
|
||||||
*/
|
|
||||||
feed: string
|
|
||||||
/**
|
|
||||||
* If page type is `link` this field is returned. Required in POST to create a `link` page.
|
|
||||||
*/
|
|
||||||
link: string
|
|
||||||
content_type?: 'application/json' | 'text/javascript' | 'text/html'
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file was auto-generated by swagger-to-ts.
|
|
||||||
* Do not make direct changes to the file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface definitions {
|
|
||||||
wishlist_Post: {
|
|
||||||
/**
|
|
||||||
* The customer id.
|
|
||||||
*/
|
|
||||||
customer_id: number
|
|
||||||
/**
|
|
||||||
* Whether the wishlist is available to the public.
|
|
||||||
*/
|
|
||||||
is_public?: boolean
|
|
||||||
/**
|
|
||||||
* The title of the wishlist.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* Array of Wishlist items.
|
|
||||||
*/
|
|
||||||
items?: {
|
|
||||||
/**
|
|
||||||
* The ID of the product.
|
|
||||||
*/
|
|
||||||
product_id?: number
|
|
||||||
/**
|
|
||||||
* The variant ID of the product.
|
|
||||||
*/
|
|
||||||
variant_id?: number
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
wishlist_Put: {
|
|
||||||
/**
|
|
||||||
* The customer id.
|
|
||||||
*/
|
|
||||||
customer_id: number
|
|
||||||
/**
|
|
||||||
* Whether the wishlist is available to the public.
|
|
||||||
*/
|
|
||||||
is_public?: boolean
|
|
||||||
/**
|
|
||||||
* The title of the wishlist.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* Array of Wishlist items.
|
|
||||||
*/
|
|
||||||
items?: {
|
|
||||||
/**
|
|
||||||
* The ID of the item
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* The ID of the product.
|
|
||||||
*/
|
|
||||||
product_id?: number
|
|
||||||
/**
|
|
||||||
* The variant ID of the item.
|
|
||||||
*/
|
|
||||||
variant_id?: number
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
wishlist_Full: {
|
|
||||||
/**
|
|
||||||
* Wishlist ID, provided after creating a wishlist with a POST.
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* The ID the customer to which the wishlist belongs.
|
|
||||||
*/
|
|
||||||
customer_id?: number
|
|
||||||
/**
|
|
||||||
* The Wishlist's name.
|
|
||||||
*/
|
|
||||||
name?: string
|
|
||||||
/**
|
|
||||||
* Whether the Wishlist is available to the public.
|
|
||||||
*/
|
|
||||||
is_public?: boolean
|
|
||||||
/**
|
|
||||||
* The token of the Wishlist. This is created internally within BigCommerce. The Wishlist ID is to be used for external apps. Read-Only
|
|
||||||
*/
|
|
||||||
token?: string
|
|
||||||
/**
|
|
||||||
* Array of Wishlist items
|
|
||||||
*/
|
|
||||||
items?: definitions['wishlistItem_Full'][]
|
|
||||||
}
|
|
||||||
wishlistItem_Full: {
|
|
||||||
/**
|
|
||||||
* The ID of the item
|
|
||||||
*/
|
|
||||||
id?: number
|
|
||||||
/**
|
|
||||||
* The ID of the product.
|
|
||||||
*/
|
|
||||||
product_id?: number
|
|
||||||
/**
|
|
||||||
* The variant ID of the item.
|
|
||||||
*/
|
|
||||||
variant_id?: number
|
|
||||||
}
|
|
||||||
wishlistItem_Post: {
|
|
||||||
/**
|
|
||||||
* The ID of the product.
|
|
||||||
*/
|
|
||||||
product_id?: number
|
|
||||||
/**
|
|
||||||
* The variant ID of the product.
|
|
||||||
*/
|
|
||||||
variant_id?: number
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Data about the response, including pagination and collection totals.
|
|
||||||
*/
|
|
||||||
pagination: {
|
|
||||||
/**
|
|
||||||
* Total number of items in the result set.
|
|
||||||
*/
|
|
||||||
total?: number
|
|
||||||
/**
|
|
||||||
* Total number of items in the collection response.
|
|
||||||
*/
|
|
||||||
count?: number
|
|
||||||
/**
|
|
||||||
* The amount of items returned in the collection per page, controlled by the limit parameter.
|
|
||||||
*/
|
|
||||||
per_page?: number
|
|
||||||
/**
|
|
||||||
* The page you are currently on within the collection.
|
|
||||||
*/
|
|
||||||
current_page?: number
|
|
||||||
/**
|
|
||||||
* The total number of pages in the collection.
|
|
||||||
*/
|
|
||||||
total_pages?: number
|
|
||||||
}
|
|
||||||
error: { status?: number; title?: string; type?: string }
|
|
||||||
metaCollection: { pagination?: definitions['pagination'] }
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
export const categoryTreeItemFragment = /* GraphQL */ `
|
|
||||||
fragment categoryTreeItem on CategoryTreeItem {
|
|
||||||
entityId
|
|
||||||
name
|
|
||||||
path
|
|
||||||
description
|
|
||||||
productCount
|
|
||||||
}
|
|
||||||
`
|
|
@ -1,43 +0,0 @@
|
|||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
|
||||||
import { definitions } from '../definitions/store-content'
|
|
||||||
|
|
||||||
export type Page = definitions['page_Full']
|
|
||||||
|
|
||||||
export type GetAllPagesResult<
|
|
||||||
T extends { pages: any[] } = { pages: Page[] }
|
|
||||||
> = T
|
|
||||||
|
|
||||||
async function getAllPages(opts?: {
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<GetAllPagesResult>
|
|
||||||
|
|
||||||
async function getAllPages<T extends { pages: any[] }>(opts: {
|
|
||||||
url: string
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<GetAllPagesResult<T>>
|
|
||||||
|
|
||||||
async function getAllPages({
|
|
||||||
config,
|
|
||||||
preview,
|
|
||||||
}: {
|
|
||||||
url?: string
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
preview?: boolean
|
|
||||||
} = {}): Promise<GetAllPagesResult> {
|
|
||||||
config = getConfig(config)
|
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
|
||||||
// required in case there's a custom `url`
|
|
||||||
const { data } = await config.storeApiFetch<
|
|
||||||
RecursivePartial<{ data: Page[] }>
|
|
||||||
>('/v3/content/pages')
|
|
||||||
const pages = (data as RecursiveRequired<typeof data>) ?? []
|
|
||||||
|
|
||||||
return {
|
|
||||||
pages: preview ? pages : pages.filter((p) => p.is_visible),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getAllPages
|
|
@ -1,71 +0,0 @@
|
|||||||
import type {
|
|
||||||
GetAllProductPathsQuery,
|
|
||||||
GetAllProductPathsQueryVariables,
|
|
||||||
} from '../../schema'
|
|
||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
|
||||||
import filterEdges from '../utils/filter-edges'
|
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
|
||||||
|
|
||||||
export const getAllProductPathsQuery = /* GraphQL */ `
|
|
||||||
query getAllProductPaths($first: Int = 100) {
|
|
||||||
site {
|
|
||||||
products(first: $first) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export type ProductPath = NonNullable<
|
|
||||||
NonNullable<GetAllProductPathsQuery['site']['products']['edges']>[0]
|
|
||||||
>
|
|
||||||
|
|
||||||
export type ProductPaths = ProductPath[]
|
|
||||||
|
|
||||||
export type { GetAllProductPathsQueryVariables }
|
|
||||||
|
|
||||||
export type GetAllProductPathsResult<
|
|
||||||
T extends { products: any[] } = { products: ProductPaths }
|
|
||||||
> = T
|
|
||||||
|
|
||||||
async function getAllProductPaths(opts?: {
|
|
||||||
variables?: GetAllProductPathsQueryVariables
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
}): Promise<GetAllProductPathsResult>
|
|
||||||
|
|
||||||
async function getAllProductPaths<
|
|
||||||
T extends { products: any[] },
|
|
||||||
V = any
|
|
||||||
>(opts: {
|
|
||||||
query: string
|
|
||||||
variables?: V
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
}): Promise<GetAllProductPathsResult<T>>
|
|
||||||
|
|
||||||
async function getAllProductPaths({
|
|
||||||
query = getAllProductPathsQuery,
|
|
||||||
variables,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
query?: string
|
|
||||||
variables?: GetAllProductPathsQueryVariables
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
} = {}): Promise<GetAllProductPathsResult> {
|
|
||||||
config = getConfig(config)
|
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
|
||||||
// required in case there's a custom `query`
|
|
||||||
const { data } = await config.fetch<
|
|
||||||
RecursivePartial<GetAllProductPathsQuery>
|
|
||||||
>(query, { variables })
|
|
||||||
const products = data.site?.products?.edges
|
|
||||||
|
|
||||||
return {
|
|
||||||
products: filterEdges(products as RecursiveRequired<typeof products>),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getAllProductPaths
|
|
@ -1,34 +0,0 @@
|
|||||||
import { GetCustomerIdQuery } from '../../schema'
|
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
|
||||||
|
|
||||||
export const getCustomerIdQuery = /* GraphQL */ `
|
|
||||||
query getCustomerId {
|
|
||||||
customer {
|
|
||||||
entityId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
async function getCustomerId({
|
|
||||||
customerToken,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
customerToken: string
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
}): Promise<number | undefined> {
|
|
||||||
config = getConfig(config)
|
|
||||||
|
|
||||||
const { data } = await config.fetch<GetCustomerIdQuery>(
|
|
||||||
getCustomerIdQuery,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
cookie: `${config.customerCookie}=${customerToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return data?.customer?.entityId
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getCustomerId
|
|
@ -1,53 +0,0 @@
|
|||||||
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
|
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
|
||||||
import { definitions } from '../definitions/store-content'
|
|
||||||
|
|
||||||
export type Page = definitions['page_Full']
|
|
||||||
|
|
||||||
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
|
|
||||||
|
|
||||||
export type PageVariables = {
|
|
||||||
id: number
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPage(opts: {
|
|
||||||
url?: string
|
|
||||||
variables: PageVariables
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<GetPageResult>
|
|
||||||
|
|
||||||
async function getPage<T extends { page?: any }, V = any>(opts: {
|
|
||||||
url: string
|
|
||||||
variables: V
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<GetPageResult<T>>
|
|
||||||
|
|
||||||
async function getPage({
|
|
||||||
url,
|
|
||||||
variables,
|
|
||||||
config,
|
|
||||||
preview,
|
|
||||||
}: {
|
|
||||||
url?: string
|
|
||||||
variables: PageVariables
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
preview?: boolean
|
|
||||||
}): Promise<GetPageResult> {
|
|
||||||
config = getConfig(config)
|
|
||||||
// RecursivePartial forces the method to check for every prop in the data, which is
|
|
||||||
// required in case there's a custom `url`
|
|
||||||
const { data } = await config.storeApiFetch<RecursivePartial<{ data: Page[] }>>(
|
|
||||||
url || `/v3/content/pages?id=${variables.id}&include=body`
|
|
||||||
)
|
|
||||||
const firstPage = data?.[0]
|
|
||||||
const page = firstPage as RecursiveRequired<typeof firstPage>
|
|
||||||
|
|
||||||
if (preview || page?.is_visible) {
|
|
||||||
return { page }
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getPage
|
|
@ -1,71 +0,0 @@
|
|||||||
import type { ServerResponse } from 'http'
|
|
||||||
import type { LoginMutation, LoginMutationVariables } from '../../schema'
|
|
||||||
import type { RecursivePartial } from '../utils/types'
|
|
||||||
import concatHeader from '../utils/concat-cookie'
|
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
|
||||||
|
|
||||||
export const loginMutation = /* GraphQL */ `
|
|
||||||
mutation login($email: String!, $password: String!) {
|
|
||||||
login(email: $email, password: $password) {
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export type LoginResult<T extends { result?: any } = { result?: string }> = T
|
|
||||||
|
|
||||||
export type LoginVariables = LoginMutationVariables
|
|
||||||
|
|
||||||
async function login(opts: {
|
|
||||||
variables: LoginVariables
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
res: ServerResponse
|
|
||||||
}): Promise<LoginResult>
|
|
||||||
|
|
||||||
async function login<T extends { result?: any }, V = any>(opts: {
|
|
||||||
query: string
|
|
||||||
variables: V
|
|
||||||
res: ServerResponse
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
}): Promise<LoginResult<T>>
|
|
||||||
|
|
||||||
async function login({
|
|
||||||
query = loginMutation,
|
|
||||||
variables,
|
|
||||||
res: response,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
query?: string
|
|
||||||
variables: LoginVariables
|
|
||||||
res: ServerResponse
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
}): Promise<LoginResult> {
|
|
||||||
config = getConfig(config)
|
|
||||||
|
|
||||||
const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>(
|
|
||||||
query,
|
|
||||||
{ variables }
|
|
||||||
)
|
|
||||||
// Bigcommerce returns a Set-Cookie header with the auth cookie
|
|
||||||
let cookie = res.headers.get('Set-Cookie')
|
|
||||||
|
|
||||||
if (cookie && typeof cookie === 'string') {
|
|
||||||
// In development, don't set a secure cookie or the browser will ignore it
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
cookie = cookie.replace('; Secure', '')
|
|
||||||
// SameSite=none can't be set unless the cookie is Secure
|
|
||||||
cookie = cookie.replace('; SameSite=none', '; SameSite=lax')
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setHeader(
|
|
||||||
'Set-Cookie',
|
|
||||||
concatHeader(response.getHeader('Set-Cookie'), cookie)!
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: data.login?.result,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default login
|
|
@ -1,14 +0,0 @@
|
|||||||
type Header = string | number | string[] | undefined
|
|
||||||
|
|
||||||
export default function concatHeader(prev: Header, val: Header) {
|
|
||||||
if (!val) return prev
|
|
||||||
if (!prev) return val
|
|
||||||
|
|
||||||
if (Array.isArray(prev)) return prev.concat(String(val))
|
|
||||||
|
|
||||||
prev = String(prev)
|
|
||||||
|
|
||||||
if (Array.isArray(val)) return [prev].concat(val)
|
|
||||||
|
|
||||||
return [prev, String(val)]
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { BigcommerceConfig, getConfig } from '..'
|
|
||||||
|
|
||||||
export type BigcommerceApiHandler<
|
|
||||||
T = any,
|
|
||||||
H extends BigcommerceHandlers = {},
|
|
||||||
Options extends {} = {}
|
|
||||||
> = (
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<BigcommerceApiResponse<T>>,
|
|
||||||
config: BigcommerceConfig,
|
|
||||||
handlers: H,
|
|
||||||
// Custom configs that may be used by a particular handler
|
|
||||||
options: Options
|
|
||||||
) => void | Promise<void>
|
|
||||||
|
|
||||||
export type BigcommerceHandler<T = any, Body = null> = (options: {
|
|
||||||
req: NextApiRequest
|
|
||||||
res: NextApiResponse<BigcommerceApiResponse<T>>
|
|
||||||
config: BigcommerceConfig
|
|
||||||
body: Body
|
|
||||||
}) => void | Promise<void>
|
|
||||||
|
|
||||||
export type BigcommerceHandlers<T = any> = {
|
|
||||||
[k: string]: BigcommerceHandler<T, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BigcommerceApiResponse<T> = {
|
|
||||||
data: T | null
|
|
||||||
errors?: { message: string; code?: string }[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function createApiHandler<
|
|
||||||
T = any,
|
|
||||||
H extends BigcommerceHandlers = {},
|
|
||||||
Options extends {} = {}
|
|
||||||
>(
|
|
||||||
handler: BigcommerceApiHandler<T, H, Options>,
|
|
||||||
handlers: H,
|
|
||||||
defaultOptions: Options
|
|
||||||
) {
|
|
||||||
return function getApiHandler({
|
|
||||||
config,
|
|
||||||
operations,
|
|
||||||
options,
|
|
||||||
}: {
|
|
||||||
config?: BigcommerceConfig
|
|
||||||
operations?: Partial<H>
|
|
||||||
options?: Options extends {} ? Partial<Options> : never
|
|
||||||
} = {}): NextApiHandler {
|
|
||||||
const ops = { ...operations, ...handlers }
|
|
||||||
const opts = { ...defaultOptions, ...options }
|
|
||||||
|
|
||||||
return function apiHandler(req, res) {
|
|
||||||
return handler(req, res, getConfig(config), ops, opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import type { Response } from '@vercel/fetch'
|
|
||||||
|
|
||||||
// Used for GraphQL errors
|
|
||||||
export class BigcommerceGraphQLError extends Error {}
|
|
||||||
|
|
||||||
export class BigcommerceApiError extends Error {
|
|
||||||
status: number
|
|
||||||
res: Response
|
|
||||||
data: any
|
|
||||||
|
|
||||||
constructor(msg: string, res: Response, data?: any) {
|
|
||||||
super(msg)
|
|
||||||
this.name = 'BigcommerceApiError'
|
|
||||||
this.status = res.status
|
|
||||||
this.res = res
|
|
||||||
this.data = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BigcommerceNetworkError extends Error {
|
|
||||||
constructor(msg: string) {
|
|
||||||
super(msg)
|
|
||||||
this.name = 'BigcommerceNetworkError'
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
import { FetcherError } from '../../../commerce/utils/errors'
|
|
||||||
import type { GraphQLFetcher } from '../../../commerce/api'
|
|
||||||
import { getConfig } from '..'
|
|
||||||
import fetch from './fetch'
|
|
||||||
|
|
||||||
const fetchGraphqlApi: GraphQLFetcher = async (
|
|
||||||
query: string,
|
|
||||||
{ variables, preview } = {},
|
|
||||||
fetchOptions
|
|
||||||
) => {
|
|
||||||
// log.warn(query)
|
|
||||||
const config = getConfig()
|
|
||||||
const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
|
|
||||||
...fetchOptions,
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${config.apiToken}`,
|
|
||||||
...fetchOptions?.headers,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query,
|
|
||||||
variables,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const json = await res.json()
|
|
||||||
if (json.errors) {
|
|
||||||
throw new FetcherError({
|
|
||||||
errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }],
|
|
||||||
status: res.status,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { data: json.data, res }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default fetchGraphqlApi
|
|
@ -1,71 +0,0 @@
|
|||||||
import type { RequestInit, Response } from '@vercel/fetch'
|
|
||||||
import { getConfig } from '..'
|
|
||||||
import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
|
|
||||||
import fetch from './fetch'
|
|
||||||
|
|
||||||
export default async function fetchStoreApi<T>(
|
|
||||||
endpoint: string,
|
|
||||||
options?: RequestInit
|
|
||||||
): Promise<T> {
|
|
||||||
const config = getConfig()
|
|
||||||
let res: Response
|
|
||||||
|
|
||||||
try {
|
|
||||||
res = await fetch(config.storeApiUrl + endpoint, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
...options?.headers,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Auth-Token': config.storeApiToken,
|
|
||||||
'X-Auth-Client': config.storeApiClientId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
throw new BigcommerceNetworkError(
|
|
||||||
`Fetch to Bigcommerce failed: ${error.message}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = res.headers.get('Content-Type')
|
|
||||||
const isJSON = contentType?.includes('application/json')
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
const data = isJSON ? await res.json() : await getTextOrNull(res)
|
|
||||||
const headers = getRawHeaders(res)
|
|
||||||
const msg = `Big Commerce API error (${
|
|
||||||
res.status
|
|
||||||
}) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${
|
|
||||||
typeof data === 'string' ? data : JSON.stringify(data, null, 2)
|
|
||||||
}`
|
|
||||||
|
|
||||||
throw new BigcommerceApiError(msg, res, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.status !== 204 && !isJSON) {
|
|
||||||
throw new BigcommerceApiError(
|
|
||||||
`Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
|
|
||||||
res
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If something was removed, the response will be empty
|
|
||||||
return res.status === 204 ? null : await res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRawHeaders(res: Response) {
|
|
||||||
const headers: { [key: string]: string } = {}
|
|
||||||
|
|
||||||
res.headers.forEach((value, key) => {
|
|
||||||
headers[key] = value
|
|
||||||
})
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTextOrNull(res: Response) {
|
|
||||||
try {
|
|
||||||
return res.text()
|
|
||||||
} catch (err) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
import zeitFetch from '@vercel/fetch'
|
|
||||||
|
|
||||||
export default zeitFetch()
|
|
@ -1,5 +0,0 @@
|
|||||||
export default function filterEdges<T>(
|
|
||||||
edges: (T | null | undefined)[] | null | undefined
|
|
||||||
) {
|
|
||||||
return edges?.filter((edge): edge is T => !!edge) ?? []
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import { serialize, CookieSerializeOptions } from 'cookie'
|
|
||||||
|
|
||||||
export default function getCartCookie(
|
|
||||||
name: string,
|
|
||||||
cartId?: string,
|
|
||||||
maxAge?: number
|
|
||||||
) {
|
|
||||||
const options: CookieSerializeOptions =
|
|
||||||
cartId && maxAge
|
|
||||||
? {
|
|
||||||
maxAge,
|
|
||||||
expires: new Date(Date.now() + maxAge * 1000),
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
path: '/',
|
|
||||||
sameSite: 'lax',
|
|
||||||
}
|
|
||||||
: { maxAge: -1, path: '/' } // Removes the cookie
|
|
||||||
|
|
||||||
return serialize(name, cartId || '', options)
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
|
|
||||||
export default function isAllowedMethod(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse,
|
|
||||||
allowedMethods: string[]
|
|
||||||
) {
|
|
||||||
const methods = allowedMethods.includes('OPTIONS')
|
|
||||||
? allowedMethods
|
|
||||||
: [...allowedMethods, 'OPTIONS']
|
|
||||||
|
|
||||||
if (!req.method || !methods.includes(req.method)) {
|
|
||||||
res.status(405)
|
|
||||||
res.setHeader('Allow', methods.join(', '))
|
|
||||||
res.end()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === 'OPTIONS') {
|
|
||||||
res.status(200)
|
|
||||||
res.setHeader('Allow', methods.join(', '))
|
|
||||||
res.setHeader('Content-Length', '0')
|
|
||||||
res.end()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import type { ItemBody as WishlistItemBody } from '../wishlist'
|
|
||||||
import type { ItemBody } from '../cart'
|
|
||||||
|
|
||||||
export const parseWishlistItem = (item: WishlistItemBody) => ({
|
|
||||||
product_id: item.productId,
|
|
||||||
variant_id: item.variantId,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const parseCartItem = (item: ItemBody) => ({
|
|
||||||
quantity: item.quantity,
|
|
||||||
product_id: item.productId,
|
|
||||||
variant_id: item.variantId,
|
|
||||||
})
|
|
@ -1,21 +0,0 @@
|
|||||||
import type { ProductNode } from '../operations/get-all-products'
|
|
||||||
import type { RecursivePartial } from './types'
|
|
||||||
|
|
||||||
export default function setProductLocaleMeta(
|
|
||||||
node: RecursivePartial<ProductNode>
|
|
||||||
) {
|
|
||||||
if (node.localeMeta?.edges) {
|
|
||||||
node.localeMeta.edges = node.localeMeta.edges.filter((edge) => {
|
|
||||||
const { key, value } = edge?.node ?? {}
|
|
||||||
if (key && key in node) {
|
|
||||||
;(node as any)[key] = value
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!node.localeMeta.edges.length) {
|
|
||||||
delete node.localeMeta
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export type RecursivePartial<T> = {
|
|
||||||
[P in keyof T]?: RecursivePartial<T[P]>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RecursiveRequired<T> = {
|
|
||||||
[P in keyof T]-?: RecursiveRequired<T[P]>
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
import type { WishlistHandlers } from '..'
|
|
||||||
import getCustomerId from '../../operations/get-customer-id'
|
|
||||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
|
||||||
import { parseWishlistItem } from '../../utils/parse-item'
|
|
||||||
|
|
||||||
// Returns the wishlist of the signed customer
|
|
||||||
const addItem: WishlistHandlers['addItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { customerToken, item },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
if (!item) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Missing item' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const customerId =
|
|
||||||
customerToken && (await getCustomerId({ customerToken, config }))
|
|
||||||
|
|
||||||
if (!customerId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { wishlist } = await getCustomerWishlist({
|
|
||||||
variables: { customerId },
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(
|
|
||||||
wishlist
|
|
||||||
? {
|
|
||||||
items: [parseWishlistItem(item)],
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
name: 'Wishlist',
|
|
||||||
customer_id: customerId,
|
|
||||||
items: [parseWishlistItem(item)],
|
|
||||||
is_public: false,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = wishlist
|
|
||||||
? await config.storeApiFetch(`/v3/wishlists/${wishlist.id}/items`, options)
|
|
||||||
: await config.storeApiFetch('/v3/wishlists', options)
|
|
||||||
|
|
||||||
res.status(200).json({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default addItem
|
|
@ -1,39 +0,0 @@
|
|||||||
import getCustomerId from '../../operations/get-customer-id'
|
|
||||||
import getCustomerWishlist, {
|
|
||||||
Wishlist,
|
|
||||||
} from '../../operations/get-customer-wishlist'
|
|
||||||
import type { WishlistHandlers } from '..'
|
|
||||||
|
|
||||||
// Return current wishlist info
|
|
||||||
const removeItem: WishlistHandlers['removeItem'] = async ({
|
|
||||||
res,
|
|
||||||
body: { customerToken, itemId },
|
|
||||||
config,
|
|
||||||
}) => {
|
|
||||||
const customerId =
|
|
||||||
customerToken && (await getCustomerId({ customerToken, config }))
|
|
||||||
const { wishlist } =
|
|
||||||
(customerId &&
|
|
||||||
(await getCustomerWishlist({
|
|
||||||
variables: { customerId },
|
|
||||||
config,
|
|
||||||
}))) ||
|
|
||||||
{}
|
|
||||||
|
|
||||||
if (!wishlist || !itemId) {
|
|
||||||
return res.status(400).json({
|
|
||||||
data: null,
|
|
||||||
errors: [{ message: 'Invalid request' }],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await config.storeApiFetch<{ data: Wishlist } | null>(
|
|
||||||
`/v3/wishlists/${wishlist.id}/items/${itemId}`,
|
|
||||||
{ method: 'DELETE' }
|
|
||||||
)
|
|
||||||
const data = result?.data ?? null
|
|
||||||
|
|
||||||
res.status(200).json({ data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export default removeItem
|
|
@ -1,56 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { HookFetcher } from '../../commerce/utils/types'
|
|
||||||
import { CommerceError } from '../../commerce/utils/errors'
|
|
||||||
import useCartAddItem from '../../commerce/cart/use-add-item'
|
|
||||||
import type { ItemBody, AddItemBody } from '../api/cart'
|
|
||||||
import useCart, { Cart } from './use-cart'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
|
||||||
url: '/api/bigcommerce/cart',
|
|
||||||
method: 'POST',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AddItemInput = ItemBody
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart, AddItemBody> = (
|
|
||||||
options,
|
|
||||||
{ item },
|
|
||||||
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',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
body: { item },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useAddItem = () => {
|
|
||||||
const { mutate } = useCart()
|
|
||||||
const fn = useCartAddItem(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async function addItem(input: AddItemInput) {
|
|
||||||
const data = await fn({ item: input })
|
|
||||||
await mutate(data, false)
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
[fn, mutate]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useAddItem.extend = extendHook
|
|
||||||
|
|
||||||
return useAddItem
|
|
||||||
}
|
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
@ -1,13 +0,0 @@
|
|||||||
import useAddItem from './use-add-item'
|
|
||||||
import useRemoveItem from './use-remove-item'
|
|
||||||
import useUpdateItem from './use-update-item'
|
|
||||||
|
|
||||||
// This hook is probably not going to be used, but it's here
|
|
||||||
// to show how a commerce should be structuring it
|
|
||||||
export default function useCartActions() {
|
|
||||||
const addItem = useAddItem()
|
|
||||||
const updateItem = useUpdateItem()
|
|
||||||
const removeItem = useRemoveItem()
|
|
||||||
|
|
||||||
return { addItem, updateItem, removeItem }
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
import type { HookFetcher } from '../../commerce/utils/types'
|
|
||||||
import type { SwrOptions } from '../../commerce/utils/use-data'
|
|
||||||
import useCommerceCart, { CartInput } from '../../commerce/cart/use-cart'
|
|
||||||
import type { Cart } from '../api/cart'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
|
||||||
url: '/api/bigcommerce/cart',
|
|
||||||
method: 'GET',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { Cart }
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart | null, CartInput> = (
|
|
||||||
options,
|
|
||||||
{ cartId },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
return cartId ? fetch({ ...defaultOpts, ...options }) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(
|
|
||||||
customFetcher: typeof fetcher,
|
|
||||||
swrOptions?: SwrOptions<Cart | null, CartInput>
|
|
||||||
) {
|
|
||||||
const useCart = () => {
|
|
||||||
const response = useCommerceCart(defaultOpts, [], customFetcher, {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
...swrOptions,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Uses a getter to only calculate the prop when required
|
|
||||||
// response.data is also a getter and it's better to not trigger it early
|
|
||||||
Object.defineProperty(response, 'isEmpty', {
|
|
||||||
get() {
|
|
||||||
return Object.values(response.data?.line_items ?? {}).every(
|
|
||||||
(items) => !items.length
|
|
||||||
)
|
|
||||||
},
|
|
||||||
set: (x) => x,
|
|
||||||
})
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
useCart.extend = extendHook
|
|
||||||
|
|
||||||
return useCart
|
|
||||||
}
|
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
@ -1,70 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import debounce from 'lodash.debounce'
|
|
||||||
import type { HookFetcher } from '../../commerce/utils/types'
|
|
||||||
import { CommerceError } from '../../commerce/utils/errors'
|
|
||||||
import useCartUpdateItem from '../../commerce/cart/use-update-item'
|
|
||||||
import type { ItemBody, UpdateItemBody } from '../api/cart'
|
|
||||||
import { fetcher as removeFetcher } from './use-remove-item'
|
|
||||||
import useCart, { Cart } from './use-cart'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
|
||||||
url: '/api/bigcommerce/cart',
|
|
||||||
method: 'PUT',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UpdateItemInput = Partial<{ id: string } & ItemBody>
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Cart | null, UpdateItemBody> = (
|
|
||||||
options,
|
|
||||||
{ itemId, item },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
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 removeFetcher(null, { itemId }, fetch)
|
|
||||||
}
|
|
||||||
} else if (item.quantity) {
|
|
||||||
throw new CommerceError({
|
|
||||||
message: 'The item quantity has to be a valid integer',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
body: { itemId, item },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
|
||||||
const useUpdateItem = (item?: any) => {
|
|
||||||
const { mutate } = useCart()
|
|
||||||
const fn = useCartUpdateItem<Cart | null, UpdateItemBody>(
|
|
||||||
defaultOpts,
|
|
||||||
customFetcher
|
|
||||||
)
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
debounce(async (input: UpdateItemInput) => {
|
|
||||||
const data = await fn({
|
|
||||||
itemId: input.id ?? item?.id,
|
|
||||||
item: {
|
|
||||||
productId: input.productId ?? item?.product_id,
|
|
||||||
variantId: input.productId ?? item?.variant_id,
|
|
||||||
quantity: input.quantity,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await mutate(data, false)
|
|
||||||
return data
|
|
||||||
}, cfg?.wait ?? 500),
|
|
||||||
[fn, mutate]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useUpdateItem.extend = extendHook
|
|
||||||
|
|
||||||
return useUpdateItem
|
|
||||||
}
|
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
@ -1,60 +0,0 @@
|
|||||||
import { ReactNode } from 'react'
|
|
||||||
import {
|
|
||||||
CommerceConfig,
|
|
||||||
CommerceProvider as CoreCommerceProvider,
|
|
||||||
useCommerce as useCoreCommerce,
|
|
||||||
} from '../commerce'
|
|
||||||
import { FetcherError } from '../commerce/utils/errors'
|
|
||||||
|
|
||||||
async function getText(res: Response) {
|
|
||||||
try {
|
|
||||||
return (await res.text()) || res.statusText
|
|
||||||
} catch (error) {
|
|
||||||
return res.statusText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getError(res: Response) {
|
|
||||||
if (res.headers.get('Content-Type')?.includes('application/json')) {
|
|
||||||
const data = await res.json()
|
|
||||||
return new FetcherError({ errors: data.errors, status: res.status })
|
|
||||||
}
|
|
||||||
return new FetcherError({ message: await getText(res), status: res.status })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const bigcommerceConfig: CommerceConfig = {
|
|
||||||
locale: 'en-us',
|
|
||||||
cartCookie: 'bc_cartId',
|
|
||||||
async fetcher({ url, method = 'GET', variables, body: bodyObj }) {
|
|
||||||
const hasBody = Boolean(variables || bodyObj)
|
|
||||||
const body = hasBody
|
|
||||||
? JSON.stringify(variables ? { variables } : bodyObj)
|
|
||||||
: undefined
|
|
||||||
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
|
|
||||||
const res = await fetch(url!, { method, body, headers })
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const { data } = await res.json()
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
throw await getError(res)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BigcommerceConfig = Partial<CommerceConfig>
|
|
||||||
|
|
||||||
export type BigcommerceProps = {
|
|
||||||
children?: ReactNode
|
|
||||||
locale: string
|
|
||||||
} & BigcommerceConfig
|
|
||||||
|
|
||||||
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
|
|
||||||
return (
|
|
||||||
<CoreCommerceProvider config={{ ...bigcommerceConfig, ...config }}>
|
|
||||||
{children}
|
|
||||||
</CoreCommerceProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCommerce = () => useCoreCommerce()
|
|
@ -1,64 +0,0 @@
|
|||||||
import type { HookFetcher } from '../../commerce/utils/types'
|
|
||||||
import type { SwrOptions } from '../../commerce/utils/use-data'
|
|
||||||
import useCommerceSearch from '../../commerce/products/use-search'
|
|
||||||
import type { SearchProductsData } from '../api/catalog/products'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
|
||||||
url: '/api/bigcommerce/catalog/products',
|
|
||||||
method: 'GET',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SearchProductsInput = {
|
|
||||||
search?: string
|
|
||||||
categoryId?: number
|
|
||||||
brandId?: number
|
|
||||||
sort?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<SearchProductsData, SearchProductsInput> = (
|
|
||||||
options,
|
|
||||||
{ search, categoryId, brandId, sort },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
// Use a dummy base as we only care about the relative path
|
|
||||||
const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
|
|
||||||
|
|
||||||
if (search) url.searchParams.set('search', search)
|
|
||||||
if (Number.isInteger(categoryId))
|
|
||||||
url.searchParams.set('category', String(categoryId))
|
|
||||||
if (Number.isInteger(categoryId))
|
|
||||||
url.searchParams.set('brand', String(brandId))
|
|
||||||
if (sort) url.searchParams.set('sort', sort)
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
url: url.pathname + url.search,
|
|
||||||
method: options?.method ?? defaultOpts.method,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(
|
|
||||||
customFetcher: typeof fetcher,
|
|
||||||
swrOptions?: SwrOptions<SearchProductsData, SearchProductsInput>
|
|
||||||
) {
|
|
||||||
const useSearch = (input: SearchProductsInput = {}) => {
|
|
||||||
const response = useCommerceSearch(
|
|
||||||
defaultOpts,
|
|
||||||
[
|
|
||||||
['search', input.search],
|
|
||||||
['categoryId', input.categoryId],
|
|
||||||
['brandId', input.brandId],
|
|
||||||
['sort', input.sort],
|
|
||||||
],
|
|
||||||
customFetcher,
|
|
||||||
{ revalidateOnFocus: false, ...swrOptions }
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
useSearch.extend = extendHook
|
|
||||||
|
|
||||||
return useSearch
|
|
||||||
}
|
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
File diff suppressed because it is too large
Load Diff
@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Generates definitions for REST API endpoints that are being
|
|
||||||
* used by ../api using https://github.com/drwpow/swagger-to-ts
|
|
||||||
*/
|
|
||||||
const { readFileSync, promises } = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const fetch = require('node-fetch')
|
|
||||||
const swaggerToTS = require('@manifoldco/swagger-to-ts').default
|
|
||||||
|
|
||||||
async function getSchema(filename) {
|
|
||||||
const url = `https://next-api.stoplight.io/projects/8433/files/${filename}`
|
|
||||||
const res = await fetch(url)
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Request failed with ${res.status}: ${res.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemas = Object.entries({
|
|
||||||
'../api/definitions/catalog.ts':
|
|
||||||
'BigCommerce_Catalog_API.oas2.yml?ref=version%2F20.930',
|
|
||||||
'../api/definitions/store-content.ts':
|
|
||||||
'BigCommerce_Store_Content_API.oas2.yml?ref=version%2F20.930',
|
|
||||||
'../api/definitions/wishlist.ts':
|
|
||||||
'BigCommerce_Wishlist_API.oas2.yml?ref=version%2F20.930',
|
|
||||||
// swagger-to-ts is not working for the schema of the cart API
|
|
||||||
// '../api/definitions/cart.ts':
|
|
||||||
// 'BigCommerce_Server_to_Server_Cart_API.oas2.yml',
|
|
||||||
})
|
|
||||||
|
|
||||||
async function writeDefinitions() {
|
|
||||||
const ops = schemas.map(async ([dest, filename]) => {
|
|
||||||
const destination = path.join(__dirname, dest)
|
|
||||||
const schema = await getSchema(filename)
|
|
||||||
const definition = swaggerToTS(schema.content, {
|
|
||||||
prettierConfig: 'package.json',
|
|
||||||
})
|
|
||||||
|
|
||||||
await promises.writeFile(destination, definition)
|
|
||||||
|
|
||||||
console.log(`✔️ Added definitions for: ${dest}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(ops)
|
|
||||||
}
|
|
||||||
|
|
||||||
writeDefinitions()
|
|
@ -1,38 +0,0 @@
|
|||||||
import type { HookFetcher } from '../commerce/utils/types'
|
|
||||||
import type { SwrOptions } from '../commerce/utils/use-data'
|
|
||||||
import useCommerceCustomer from '../commerce/use-customer'
|
|
||||||
import type { Customer, CustomerData } from './api/customers'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
|
||||||
url: '/api/bigcommerce/customers',
|
|
||||||
method: 'GET',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { Customer }
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<Customer | null> = async (
|
|
||||||
options,
|
|
||||||
_,
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
const data = await fetch<CustomerData | null>({ ...defaultOpts, ...options })
|
|
||||||
return data?.customer ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(
|
|
||||||
customFetcher: typeof fetcher,
|
|
||||||
swrOptions?: SwrOptions<Customer | null>
|
|
||||||
) {
|
|
||||||
const useCustomer = () => {
|
|
||||||
return useCommerceCustomer(defaultOpts, [], customFetcher, {
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
...swrOptions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useCustomer.extend = extendHook
|
|
||||||
|
|
||||||
return useCustomer
|
|
||||||
}
|
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
@ -1,54 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { HookFetcher } from '../commerce/utils/types'
|
|
||||||
import { CommerceError } from '../commerce/utils/errors'
|
|
||||||
import useCommerceLogin from '../commerce/use-login'
|
|
||||||
import type { LoginBody } from './api/customers/login'
|
|
||||||
import useCustomer from './use-customer'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
|
||||||
url: '/api/bigcommerce/customers/login',
|
|
||||||
method: 'POST',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LoginInput = LoginBody
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<null, LoginBody> = (
|
|
||||||
options,
|
|
||||||
{ email, password },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
if (!(email && password)) {
|
|
||||||
throw new CommerceError({
|
|
||||||
message:
|
|
||||||
'A first name, last name, email and password are required to login',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
body: { email, password },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useLogin = () => {
|
|
||||||
const { revalidate } = useCustomer()
|
|
||||||
const fn = useCommerceLogin<null, LoginInput>(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async function login(input: LoginInput) {
|
|
||||||
const data = await fn(input)
|
|
||||||
await revalidate()
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
[fn]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useLogin.extend = extendHook
|
|
||||||
|
|
||||||
return useLogin
|
|
||||||
}
|
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
@ -1,38 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { HookFetcher } from '../commerce/utils/types'
|
|
||||||
import useCommerceLogout from '../commerce/use-logout'
|
|
||||||
import useCustomer from './use-customer'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
|
||||||
url: '/api/bigcommerce/customers/logout',
|
|
||||||
method: 'GET',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<null> = (options, _, fetch) => {
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useLogout = () => {
|
|
||||||
const { mutate } = useCustomer()
|
|
||||||
const fn = useCommerceLogout<null>(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async function login() {
|
|
||||||
const data = await fn(null)
|
|
||||||
await mutate(null, false)
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
[fn]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useLogout.extend = extendHook
|
|
||||||
|
|
||||||
return useLogout
|
|
||||||
}
|
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from '../commerce/use-price'
|
|
||||||
export { default } from '../commerce/use-price'
|
|
@ -1,54 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { HookFetcher } from '../commerce/utils/types'
|
|
||||||
import { CommerceError } from '../commerce/utils/errors'
|
|
||||||
import useCommerceSignup from '../commerce/use-signup'
|
|
||||||
import type { SignupBody } from './api/customers/signup'
|
|
||||||
import useCustomer from './use-customer'
|
|
||||||
|
|
||||||
const defaultOpts = {
|
|
||||||
url: '/api/bigcommerce/customers/signup',
|
|
||||||
method: 'POST',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SignupInput = SignupBody
|
|
||||||
|
|
||||||
export const fetcher: HookFetcher<null, SignupBody> = (
|
|
||||||
options,
|
|
||||||
{ firstName, lastName, email, password },
|
|
||||||
fetch
|
|
||||||
) => {
|
|
||||||
if (!(firstName && lastName && email && password)) {
|
|
||||||
throw new CommerceError({
|
|
||||||
message:
|
|
||||||
'A first name, last name, email and password are required to signup',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch({
|
|
||||||
...defaultOpts,
|
|
||||||
...options,
|
|
||||||
body: { firstName, lastName, email, password },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extendHook(customFetcher: typeof fetcher) {
|
|
||||||
const useSignup = () => {
|
|
||||||
const { revalidate } = useCustomer()
|
|
||||||
const fn = useCommerceSignup<null, SignupInput>(defaultOpts, customFetcher)
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async function signup(input: SignupInput) {
|
|
||||||
const data = await fn(input)
|
|
||||||
await revalidate()
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
[fn]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useSignup.extend = extendHook
|
|
||||||
|
|
||||||
return useSignup
|
|
||||||
}
|
|
||||||
|
|
||||||
export default extendHook(fetcher)
|
|
@ -1,11 +0,0 @@
|
|||||||
import useAddItem from './use-add-item'
|
|
||||||
import useRemoveItem from './use-remove-item'
|
|
||||||
|
|
||||||
// This hook is probably not going to be used, but it's here
|
|
||||||
// to show how a commerce should be structuring it
|
|
||||||
export default function useWishlistActions() {
|
|
||||||
const addItem = useAddItem()
|
|
||||||
const removeItem = useRemoveItem()
|
|
||||||
|
|
||||||
return { addItem, removeItem }
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
import type { RequestInit, Response } from '@vercel/fetch'
|
|
||||||
|
|
||||||
export interface CommerceAPIConfig {
|
|
||||||
locale?: string
|
|
||||||
commerceUrl: string
|
|
||||||
apiToken: string
|
|
||||||
cartCookie: string
|
|
||||||
cartCookieMaxAge: number
|
|
||||||
customerCookie: string
|
|
||||||
fetch<Data = any, Variables = any>(
|
|
||||||
query: string,
|
|
||||||
queryData?: CommerceAPIFetchOptions<Variables>,
|
|
||||||
fetchOptions?: RequestInit
|
|
||||||
): Promise<GraphQLFetcherResult<Data>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GraphQLFetcher<
|
|
||||||
Data extends GraphQLFetcherResult = GraphQLFetcherResult,
|
|
||||||
Variables = any
|
|
||||||
> = (
|
|
||||||
query: string,
|
|
||||||
queryData?: CommerceAPIFetchOptions<Variables>,
|
|
||||||
fetchOptions?: RequestInit
|
|
||||||
) => Promise<Data>
|
|
||||||
|
|
||||||
export interface GraphQLFetcherResult<Data = any> {
|
|
||||||
data: Data
|
|
||||||
res: Response
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommerceAPIFetchOptions<Variables> {
|
|
||||||
variables?: Variables
|
|
||||||
preview?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: define interfaces for all the available operations and API endpoints
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from '../utils/use-action'
|
|
||||||
|
|
||||||
const useAddItem = useAction
|
|
||||||
|
|
||||||
export default useAddItem
|
|
@ -1,17 +0,0 @@
|
|||||||
import type { HookFetcher, HookFetcherOptions } from '../utils/types'
|
|
||||||
import useAddItem from './use-add-item'
|
|
||||||
import useRemoveItem from './use-remove-item'
|
|
||||||
import useUpdateItem from './use-update-item'
|
|
||||||
|
|
||||||
// This hook is probably not going to be used, but it's here
|
|
||||||
// to show how a commerce should be structuring it
|
|
||||||
export default function useCartActions<T, Input>(
|
|
||||||
options: HookFetcherOptions,
|
|
||||||
fetcher: HookFetcher<T, Input>
|
|
||||||
) {
|
|
||||||
const addItem = useAddItem<T, Input>(options, fetcher)
|
|
||||||
const updateItem = useUpdateItem<T, Input>(options, fetcher)
|
|
||||||
const removeItem = useRemoveItem<T, Input>(options, fetcher)
|
|
||||||
|
|
||||||
return { addItem, updateItem, removeItem }
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
import type { responseInterface } from 'swr'
|
|
||||||
import Cookies from 'js-cookie'
|
|
||||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
|
||||||
import useData, { SwrOptions } from '../utils/use-data'
|
|
||||||
import { useCommerce } from '..'
|
|
||||||
|
|
||||||
export type CartResponse<Result> = responseInterface<Result, Error> & {
|
|
||||||
isEmpty: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CartInput = {
|
|
||||||
cartId: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useCart<Result>(
|
|
||||||
options: HookFetcherOptions,
|
|
||||||
input: HookInput,
|
|
||||||
fetcherFn: HookFetcher<Result, CartInput>,
|
|
||||||
swrOptions?: SwrOptions<Result, CartInput>
|
|
||||||
) {
|
|
||||||
const { cartCookie } = useCommerce()
|
|
||||||
|
|
||||||
const fetcher: typeof fetcherFn = (options, input, fetch) => {
|
|
||||||
input.cartId = Cookies.get(cartCookie)
|
|
||||||
return fetcherFn(options, input, fetch)
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = useData(options, input, fetcher, swrOptions)
|
|
||||||
|
|
||||||
return Object.assign(response, { isEmpty: true }) as CartResponse<Result>
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from '../utils/use-action'
|
|
||||||
|
|
||||||
const useRemoveItem = useAction
|
|
||||||
|
|
||||||
export default useRemoveItem
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from '../utils/use-action'
|
|
||||||
|
|
||||||
const useUpdateItem = useAction
|
|
||||||
|
|
||||||
export default useUpdateItem
|
|
@ -1,51 +0,0 @@
|
|||||||
import {
|
|
||||||
ReactNode,
|
|
||||||
MutableRefObject,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
} from 'react'
|
|
||||||
import { Fetcher } from './utils/types'
|
|
||||||
|
|
||||||
const Commerce = createContext<CommerceContextValue | {}>({})
|
|
||||||
|
|
||||||
export type CommerceProps = {
|
|
||||||
children?: ReactNode
|
|
||||||
config: CommerceConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommerceConfig = { fetcher: Fetcher<any> } & Omit<
|
|
||||||
CommerceContextValue,
|
|
||||||
'fetcherRef'
|
|
||||||
>
|
|
||||||
|
|
||||||
export type CommerceContextValue = {
|
|
||||||
fetcherRef: MutableRefObject<Fetcher<any>>
|
|
||||||
locale: string
|
|
||||||
cartCookie: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CommerceProvider({ children, config }: CommerceProps) {
|
|
||||||
if (!config) {
|
|
||||||
throw new Error('CommerceProvider requires a valid config object')
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetcherRef = useRef(config.fetcher)
|
|
||||||
// Because the config is an object, if the parent re-renders this provider
|
|
||||||
// will re-render every consumer unless we memoize the config
|
|
||||||
const cfg = useMemo(
|
|
||||||
() => ({
|
|
||||||
fetcherRef,
|
|
||||||
locale: config.locale,
|
|
||||||
cartCookie: config.cartCookie,
|
|
||||||
}),
|
|
||||||
[config.locale, config.cartCookie]
|
|
||||||
)
|
|
||||||
|
|
||||||
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCommerce<T extends CommerceContextValue>() {
|
|
||||||
return useContext(Commerce) as T
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import useData from '../utils/use-data'
|
|
||||||
|
|
||||||
const useSearch = useData
|
|
||||||
|
|
||||||
export default useSearch
|
|
@ -1,5 +0,0 @@
|
|||||||
import useData from './utils/use-data'
|
|
||||||
|
|
||||||
const useCustomer = useData
|
|
||||||
|
|
||||||
export default useCustomer
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from './utils/use-action'
|
|
||||||
|
|
||||||
const useLogin = useAction
|
|
||||||
|
|
||||||
export default useLogin
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from './utils/use-action'
|
|
||||||
|
|
||||||
const useLogout = useAction
|
|
||||||
|
|
||||||
export default useLogout
|
|
@ -1,64 +0,0 @@
|
|||||||
import { useMemo } from 'react'
|
|
||||||
import { useCommerce } from '.'
|
|
||||||
|
|
||||||
export function formatPrice({
|
|
||||||
amount,
|
|
||||||
currencyCode,
|
|
||||||
locale,
|
|
||||||
}: {
|
|
||||||
amount: number
|
|
||||||
currencyCode: string
|
|
||||||
locale: string
|
|
||||||
}) {
|
|
||||||
const formatCurrency = new Intl.NumberFormat(locale, {
|
|
||||||
style: 'currency',
|
|
||||||
currency: currencyCode,
|
|
||||||
})
|
|
||||||
|
|
||||||
return formatCurrency.format(amount)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatVariantPrice({
|
|
||||||
amount,
|
|
||||||
baseAmount,
|
|
||||||
currencyCode,
|
|
||||||
locale,
|
|
||||||
}: {
|
|
||||||
baseAmount: number
|
|
||||||
amount: number
|
|
||||||
currencyCode: string
|
|
||||||
locale: string
|
|
||||||
}) {
|
|
||||||
const hasDiscount = baseAmount > amount
|
|
||||||
const formatDiscount = new Intl.NumberFormat(locale, { style: 'percent' })
|
|
||||||
const discount = hasDiscount
|
|
||||||
? formatDiscount.format((baseAmount - amount) / baseAmount)
|
|
||||||
: null
|
|
||||||
|
|
||||||
const price = formatPrice({ amount, currencyCode, locale })
|
|
||||||
const basePrice = hasDiscount
|
|
||||||
? formatPrice({ amount: baseAmount, currencyCode, locale })
|
|
||||||
: null
|
|
||||||
|
|
||||||
return { price, basePrice, discount }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function usePrice(
|
|
||||||
data?: {
|
|
||||||
amount: number
|
|
||||||
baseAmount?: number
|
|
||||||
currencyCode: string
|
|
||||||
} | null
|
|
||||||
) {
|
|
||||||
const { amount, baseAmount, currencyCode } = data ?? {}
|
|
||||||
const { locale } = useCommerce()
|
|
||||||
const value = useMemo(() => {
|
|
||||||
if (typeof amount !== 'number' || !currencyCode) return ''
|
|
||||||
|
|
||||||
return baseAmount
|
|
||||||
? formatVariantPrice({ amount, baseAmount, currencyCode, locale })
|
|
||||||
: formatPrice({ amount, currencyCode, locale })
|
|
||||||
}, [amount, baseAmount, currencyCode])
|
|
||||||
|
|
||||||
return typeof value === 'string' ? { price: value } : value
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from './utils/use-action'
|
|
||||||
|
|
||||||
const useSignup = useAction
|
|
||||||
|
|
||||||
export default useSignup
|
|
@ -1,40 +0,0 @@
|
|||||||
export type ErrorData = {
|
|
||||||
message: string
|
|
||||||
code?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ErrorProps = {
|
|
||||||
code?: string
|
|
||||||
} & (
|
|
||||||
| { message: string; errors?: never }
|
|
||||||
| { message?: never; errors: ErrorData[] }
|
|
||||||
)
|
|
||||||
|
|
||||||
export class CommerceError extends Error {
|
|
||||||
code?: string
|
|
||||||
errors: ErrorData[]
|
|
||||||
|
|
||||||
constructor({ message, code, errors }: ErrorProps) {
|
|
||||||
const error: ErrorData = message
|
|
||||||
? { message, ...(code ? { code } : {}) }
|
|
||||||
: errors![0]
|
|
||||||
|
|
||||||
super(error.message)
|
|
||||||
this.errors = message ? [error] : errors!
|
|
||||||
|
|
||||||
if (error.code) this.code = error.code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FetcherError extends CommerceError {
|
|
||||||
status: number
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
options: {
|
|
||||||
status: number
|
|
||||||
} & ErrorProps
|
|
||||||
) {
|
|
||||||
super(options)
|
|
||||||
this.status = options.status
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
import type { HookFetcher, HookFetcherOptions } from './types'
|
|
||||||
import { useCommerce } from '..'
|
|
||||||
|
|
||||||
export default function useAction<T, Input = null>(
|
|
||||||
options: HookFetcherOptions,
|
|
||||||
fetcher: HookFetcher<T, Input>
|
|
||||||
) {
|
|
||||||
const { fetcherRef } = useCommerce()
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
(input: Input) => fetcher(options, input, fetcherRef.current),
|
|
||||||
[fetcher]
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
import useSWR, { ConfigInterface, responseInterface } from 'swr'
|
|
||||||
import type { HookInput, HookFetcher, HookFetcherOptions } from './types'
|
|
||||||
import { CommerceError } from './errors'
|
|
||||||
import { useCommerce } from '..'
|
|
||||||
|
|
||||||
export type SwrOptions<Result, Input = null> = ConfigInterface<
|
|
||||||
Result,
|
|
||||||
CommerceError,
|
|
||||||
HookFetcher<Result, Input>
|
|
||||||
>
|
|
||||||
|
|
||||||
export type UseData = <Result = any, Input = null>(
|
|
||||||
options: HookFetcherOptions | (() => HookFetcherOptions | null),
|
|
||||||
input: HookInput,
|
|
||||||
fetcherFn: HookFetcher<Result, Input>,
|
|
||||||
swrOptions?: SwrOptions<Result, Input>
|
|
||||||
) => responseInterface<Result, CommerceError>
|
|
||||||
|
|
||||||
const useData: UseData = (options, input, fetcherFn, swrOptions) => {
|
|
||||||
const { fetcherRef } = useCommerce()
|
|
||||||
const fetcher = async (
|
|
||||||
url?: string,
|
|
||||||
query?: string,
|
|
||||||
method?: string,
|
|
||||||
...args: any[]
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
return await fetcherFn(
|
|
||||||
{ url, query, method },
|
|
||||||
// Transform the input array into an object
|
|
||||||
args.reduce((obj, val, i) => {
|
|
||||||
obj[input[i][0]!] = val
|
|
||||||
return obj
|
|
||||||
}, {}),
|
|
||||||
fetcherRef.current
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
// SWR will not log errors, but any error that's not an instance
|
|
||||||
// of CommerceError is not welcomed by this hook
|
|
||||||
if (!(error instanceof CommerceError)) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const response = useSWR(
|
|
||||||
() => {
|
|
||||||
const opts = typeof options === 'function' ? options() : options
|
|
||||||
return opts
|
|
||||||
? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])]
|
|
||||||
: null
|
|
||||||
},
|
|
||||||
fetcher,
|
|
||||||
swrOptions
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useData
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from '../utils/use-action'
|
|
||||||
|
|
||||||
const useAddItem = useAction
|
|
||||||
|
|
||||||
export default useAddItem
|
|
@ -1,5 +0,0 @@
|
|||||||
import useAction from '../utils/use-action'
|
|
||||||
|
|
||||||
const useRemoveItem = useAction
|
|
||||||
|
|
||||||
export default useRemoveItem
|
|
@ -1,17 +0,0 @@
|
|||||||
import type { responseInterface } from 'swr'
|
|
||||||
import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
|
|
||||||
import useData, { SwrOptions } from '../utils/use-data'
|
|
||||||
|
|
||||||
export type WishlistResponse<Result> = responseInterface<Result, Error> & {
|
|
||||||
isEmpty: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useWishlist<Result, Input = null>(
|
|
||||||
options: HookFetcherOptions,
|
|
||||||
input: HookInput,
|
|
||||||
fetcherFn: HookFetcher<Result, Input>,
|
|
||||||
swrOptions?: SwrOptions<Result, Input>
|
|
||||||
) {
|
|
||||||
const response = useData(options, input, fetcherFn, swrOptions)
|
|
||||||
return Object.assign(response, { isEmpty: true }) as WishlistResponse<Result>
|
|
||||||
}
|
|
@ -19,6 +19,7 @@
|
|||||||
"singleQuote": true
|
"singleQuote": true
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bigcommerce/storefront-data-hooks": "^1.0.0",
|
||||||
"@headlessui/react": "^0.2.0",
|
"@headlessui/react": "^0.2.0",
|
||||||
"@next/bundle-analyzer": "^9.5.5",
|
"@next/bundle-analyzer": "^9.5.5",
|
||||||
"@react-aria/overlays": "^3.4.0",
|
"@react-aria/overlays": "^3.4.0",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
||||||
import { getConfig } from '@lib/bigcommerce/api'
|
import { getConfig } from '@lib/bigcommerce/api'
|
||||||
import getPage from '@lib/bigcommerce/api/operations/get-page'
|
import getPage from '@bigcommerce/storefront-data-hooks/api/operations/get-page'
|
||||||
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||||
import getSlug from '@utils/get-slug'
|
import getSlug from '@utils/get-slug'
|
||||||
import { Layout, HTMLContent } from '@components/core'
|
import { Layout, HTMLContent } from '@components/core'
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import cartApi from '@lib/bigcommerce/api/cart'
|
import cartApi from '@bigcommerce/storefront-data-hooks/api/cart'
|
||||||
|
|
||||||
export default cartApi()
|
export default cartApi()
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import catalogProductsApi from '@lib/bigcommerce/api/catalog/products'
|
import catalogProductsApi from '@bigcommerce/storefront-data-hooks/api/catalog/products'
|
||||||
|
|
||||||
export default catalogProductsApi()
|
export default catalogProductsApi()
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import checkoutApi from '@lib/bigcommerce/api/checkout'
|
import checkoutApi from '@bigcommerce/storefront-data-hooks/api/checkout'
|
||||||
|
|
||||||
export default checkoutApi()
|
export default checkoutApi()
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import customersApi from '@lib/bigcommerce/api/customers'
|
import customersApi from '@bigcommerce/storefront-data-hooks/api/customers'
|
||||||
|
|
||||||
export default customersApi()
|
export default customersApi()
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import loginApi from '@lib/bigcommerce/api/customers/login'
|
import loginApi from '@bigcommerce/storefront-data-hooks/api/customers/login'
|
||||||
|
|
||||||
export default loginApi()
|
export default loginApi()
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import logoutApi from '@lib/bigcommerce/api/customers/logout'
|
import logoutApi from '@bigcommerce/storefront-data-hooks/api/customers/logout'
|
||||||
|
|
||||||
export default logoutApi()
|
export default logoutApi()
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import signupApi from '@lib/bigcommerce/api/customers/signup'
|
import signupApi from '@bigcommerce/storefront-data-hooks/api/customers/signup'
|
||||||
|
|
||||||
export default signupApi()
|
export default signupApi()
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
import wishlistApi from '@lib/bigcommerce/api/wishlist'
|
import wishlistApi from '@bigcommerce/storefront-data-hooks/api/wishlist'
|
||||||
|
|
||||||
export default wishlistApi()
|
export default wishlistApi()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { GetStaticPropsContext } from 'next'
|
import type { GetStaticPropsContext } from 'next'
|
||||||
import { getConfig } from '@lib/bigcommerce/api'
|
import { getConfig } from '@lib/bigcommerce/api'
|
||||||
import getAllPages from '@lib/bigcommerce/api/operations/get-all-pages'
|
import getAllPages from '@bigcommerce/storefront-data-hooks/api/operations/get-all-pages'
|
||||||
import { Layout } from '@components/core'
|
import { Layout } from '@components/core'
|
||||||
import { Container } from '@components/ui'
|
import { Container } from '@components/ui'
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user