Merge remote-tracking branch 'origin-vercel/main' into spree-framework-poc

This commit is contained in:
tniezg 2021-08-25 16:07:54 +02:00
commit 0ad4361369
30 changed files with 1993 additions and 1051 deletions

View File

@ -20,3 +20,6 @@ NEXT_PUBLIC_SWELL_PUBLIC_KEY=
NEXT_PUBLIC_SALEOR_API_URL= NEXT_PUBLIC_SALEOR_API_URL=
NEXT_PUBLIC_SALEOR_CHANNEL= NEXT_PUBLIC_SALEOR_CHANNEL=
NEXT_PUBLIC_VENDURE_SHOP_API_URL=
NEXT_PUBLIC_VENDURE_LOCAL_URL=

6
.eslintrc Normal file
View File

@ -0,0 +1,6 @@
{
"extends": ["next", "prettier"],
"rules": {
"react/no-unescaped-entities": "off"
}
}

View File

@ -151,5 +151,5 @@ Next, you're free to customize the starter. More updates coming soon. Stay tuned
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard. After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
<br> <br>
<br> <br>
BigCommerce team has been notified and they plan to add more detailed about this subject. BigCommerce team has been notified and they plan to add more details about this subject.
</details> </details>

View File

@ -77,7 +77,6 @@ html {
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
touch-action: manipulation; touch-action: manipulation;
font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;

View File

@ -38,6 +38,7 @@ const LoginView: FC<Props> = () => {
} catch ({ errors }) { } catch ({ errors }) {
setMessage(errors[0].message) setMessage(errors[0].message)
setLoading(false) setLoading(false)
setDisabled(false)
} }
} }

View File

@ -70,6 +70,9 @@ const CartItem = ({
if (item.quantity !== Number(quantity)) { if (item.quantity !== Number(quantity)) {
setQuantity(item.quantity) setQuantity(item.quantity)
} }
// TODO: currently not including quantity in deps is intended, but we should
// do this differently as it could break easily
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item.quantity]) }, [item.quantity])
return ( return (

View File

@ -73,7 +73,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
<div className="flex items-center text-primary text-sm"> <div className="flex items-center text-primary text-sm">
<span className="text-primary">Created by</span> <span className="text-primary">Created by</span>
<a <a
rel="noopener" rel="noopener noreferrer"
href="https://vercel.com" href="https://vercel.com"
aria-label="Vercel.com Link" aria-label="Vercel.com Link"
target="_blank" target="_blank"

View File

@ -24,7 +24,7 @@ const Loading = () => (
) )
const dynamicProps = { const dynamicProps = {
loading: () => <Loading />, loading: Loading,
} }
const SignUpView = dynamic( const SignUpView = dynamic(

View File

@ -1,4 +1,4 @@
import { FC, InputHTMLAttributes, useEffect, useMemo } from 'react' import { FC, memo, useEffect } from 'react'
import cn from 'classnames' import cn from 'classnames'
import s from './Searchbar.module.css' import s from './Searchbar.module.css'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@ -13,7 +13,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
useEffect(() => { useEffect(() => {
router.prefetch('/search') router.prefetch('/search')
}, []) }, [router])
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.preventDefault() e.preventDefault()
@ -32,8 +32,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
} }
} }
return useMemo( return (
() => (
<div className={cn(s.root, className)}> <div className={cn(s.root, className)}>
<label className="hidden" htmlFor={id}> <label className="hidden" htmlFor={id}>
Search Search
@ -55,9 +54,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
</svg> </svg>
</div> </div>
</div> </div>
),
[]
) )
} }
export default Searchbar export default memo(Searchbar)

View File

@ -7,6 +7,7 @@ import Image, { ImageProps } from 'next/image'
import WishlistButton from '@components/wishlist/WishlistButton' import WishlistButton from '@components/wishlist/WishlistButton'
import usePrice from '@framework/product/use-price' import usePrice from '@framework/product/use-price'
import ProductTag from '../ProductTag' import ProductTag from '../ProductTag'
interface Props { interface Props {
className?: string className?: string
product: Product product: Product
@ -23,7 +24,6 @@ const ProductCard: FC<Props> = ({
className, className,
noNameTag = false, noNameTag = false,
variant = 'default', variant = 'default',
...props
}) => { }) => {
const { price } = usePrice({ const { price } = usePrice({
amount: product.price.value, amount: product.price.value,
@ -38,7 +38,7 @@ const ProductCard: FC<Props> = ({
) )
return ( return (
<Link href={`/product/${product.slug}`} {...props}> <Link href={`/product/${product.slug}`}>
<a className={rootClassName}> <a className={rootClassName}>
{variant === 'slim' && ( {variant === 'slim' && (
<> <>

View File

@ -1,15 +1,19 @@
import { memo } from 'react'
import { Swatch } from '@components/product' import { Swatch } from '@components/product'
import type { ProductOption } from '@commerce/types/product' import type { ProductOption } from '@commerce/types/product'
import { SelectedOptions } from '../helpers' import { SelectedOptions } from '../helpers'
import React from 'react'
interface ProductOptionsProps { interface ProductOptionsProps {
options: ProductOption[] options: ProductOption[]
selectedOptions: SelectedOptions selectedOptions: SelectedOptions
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>> setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
} }
const ProductOptions: React.FC<ProductOptionsProps> = React.memo( const ProductOptions: React.FC<ProductOptionsProps> = ({
({ options, selectedOptions, setSelectedOptions }) => { options,
selectedOptions,
setSelectedOptions,
}) => {
return ( return (
<div> <div>
{options.map((opt) => ( {options.map((opt) => (
@ -31,8 +35,7 @@ const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
setSelectedOptions((selectedOptions) => { setSelectedOptions((selectedOptions) => {
return { return {
...selectedOptions, ...selectedOptions,
[opt.displayName.toLowerCase()]: [opt.displayName.toLowerCase()]: v.label.toLowerCase(),
v.label.toLowerCase(),
} }
}) })
}} }}
@ -44,7 +47,6 @@ const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
))} ))}
</div> </div>
) )
} }
)
export default ProductOptions export default memo(ProductOptions)

View File

@ -23,7 +23,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
useEffect(() => { useEffect(() => {
selectDefaultOptionFromProduct(product, setSelectedOptions) selectDefaultOptionFromProduct(product, setSelectedOptions)
}, []) }, [product])
const variant = getProductVariant(product, selectedOptions) const variant = getProductVariant(product, selectedOptions)
const addToCart = async () => { const addToCart = async () => {

View File

@ -66,17 +66,13 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
event.preventDefault() event.preventDefault()
} }
sliderContainerRef.current!.addEventListener( const slider = sliderContainerRef.current!
'touchstart',
preventNavigation slider.addEventListener('touchstart', preventNavigation)
)
return () => { return () => {
if (sliderContainerRef.current) { if (slider) {
sliderContainerRef.current!.removeEventListener( slider.removeEventListener('touchstart', preventNavigation)
'touchstart',
preventNavigation
)
} }
} }
}, []) }, [])

View File

@ -1,15 +1,14 @@
import { FC, MouseEventHandler, memo } from 'react'
import cn from 'classnames' import cn from 'classnames'
import React from 'react'
import s from './ProductSliderControl.module.css' import s from './ProductSliderControl.module.css'
import { ArrowLeft, ArrowRight } from '@components/icons' import { ArrowLeft, ArrowRight } from '@components/icons'
interface ProductSliderControl { interface ProductSliderControl {
onPrev: React.MouseEventHandler<HTMLButtonElement> onPrev: MouseEventHandler<HTMLButtonElement>
onNext: React.MouseEventHandler<HTMLButtonElement> onNext: MouseEventHandler<HTMLButtonElement>
} }
const ProductSliderControl: React.FC<ProductSliderControl> = React.memo( const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
({ onPrev, onNext }) => (
<div className={s.control}> <div className={s.control}>
<button <button
className={cn(s.leftControl)} className={cn(s.leftControl)}
@ -26,6 +25,6 @@ const ProductSliderControl: React.FC<ProductSliderControl> = React.memo(
<ArrowRight /> <ArrowRight />
</button> </button>
</div> </div>
)
) )
export default ProductSliderControl
export default memo(ProductSliderControl)

View File

@ -7,19 +7,19 @@ import { useRouter } from 'next/router'
import { Layout } from '@components/common' import { Layout } from '@components/common'
import { ProductCard } from '@components/product' import { ProductCard } from '@components/product'
import type { Product } from '@commerce/types/product' import type { Product } from '@commerce/types/product'
import { Container, Grid, Skeleton } from '@components/ui' import { Container, Skeleton } from '@components/ui'
import useSearch from '@framework/product/use-search' import useSearch from '@framework/product/use-search'
import getSlug from '@lib/get-slug' import getSlug from '@lib/get-slug'
import rangeMap from '@lib/range-map' import rangeMap from '@lib/range-map'
const SORT = Object.entries({ const SORT = {
'trending-desc': 'Trending', 'trending-desc': 'Trending',
'latest-desc': 'Latest arrivals', 'latest-desc': 'Latest arrivals',
'price-asc': 'Price: Low to high', 'price-asc': 'Price: Low to high',
'price-desc': 'Price: High to low', 'price-desc': 'Price: High to low',
}) }
import { import {
filterQuery, filterQuery,
@ -351,7 +351,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
aria-haspopup="true" aria-haspopup="true"
aria-expanded="true" aria-expanded="true"
> >
{sort ? `Sort: ${sort}` : 'Relevance'} {sort ? SORT[sort as keyof typeof SORT] : 'Relevance'}
<svg <svg
className="-mr-1 ml-2 h-5 w-5" className="-mr-1 ml-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -398,7 +398,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
</a> </a>
</Link> </Link>
</li> </li>
{SORT.map(([key, text]) => ( {Object.entries(SORT).map(([key, text]) => (
<li <li
key={key} key={key}
className={cn( className={cn(

View File

@ -27,13 +27,15 @@ const Modal: FC<ModalProps> = ({ children, onClose }) => {
) )
useEffect(() => { useEffect(() => {
if (ref.current) { const modal = ref.current
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
if (modal) {
disableBodyScroll(modal, { reserveScrollBarGap: true })
window.addEventListener('keydown', handleKey) window.addEventListener('keydown', handleKey)
} }
return () => { return () => {
if (ref && ref.current) { if (modal) {
enableBodyScroll(ref.current) enableBodyScroll(modal)
} }
clearAllBodyScrollLocks() clearAllBodyScrollLocks()
window.removeEventListener('keydown', handleKey) window.removeEventListener('keydown', handleKey)

View File

@ -1,4 +1,4 @@
import React, { FC } from 'react' import { FC, memo } from 'react'
import rangeMap from '@lib/range-map' import rangeMap from '@lib/range-map'
import { Star } from '@components/icons' import { Star } from '@components/icons'
import cn from 'classnames' import cn from 'classnames'
@ -7,8 +7,7 @@ export interface RatingProps {
value: number value: number
} }
const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => { const Quantity: FC<RatingProps> = ({ value = 5 }) => (
return (
<div className="flex flex-row py-6 text-accent-9"> <div className="flex flex-row py-6 text-accent-9">
{rangeMap(5, (i) => ( {rangeMap(5, (i) => (
<span <span
@ -21,7 +20,6 @@ const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
</span> </span>
))} ))}
</div> </div>
) )
})
export default Quantity export default memo(Quantity)

View File

@ -16,13 +16,14 @@ const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
const ref = useRef() as React.MutableRefObject<HTMLDivElement> const ref = useRef() as React.MutableRefObject<HTMLDivElement>
useEffect(() => { useEffect(() => {
if (ref.current) { const sidebar = ref.current
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
if (sidebar) {
disableBodyScroll(sidebar, { reserveScrollBarGap: true })
} }
return () => { return () => {
if (ref && ref.current) { if (sidebar) enableBodyScroll(sidebar)
enableBodyScroll(ref.current)
}
clearAllBodyScrollLocks() clearAllBodyScrollLocks()
} }
}, []) }, [])

View File

@ -10,7 +10,7 @@ type BCCartItemBody = {
product_id: number product_id: number
variant_id: number variant_id: number
quantity?: number quantity?: number
option_selections?: OptionSelections option_selections?: OptionSelections[]
} }
export const parseWishlistItem = ( export const parseWishlistItem = (

View File

@ -16,7 +16,7 @@ export const handler: MutationHook<LoginHook> = {
if (!(email && password)) { if (!(email && password)) {
throw new CommerceError({ throw new CommerceError({
message: message:
'A first name, last name, email and password are required to login', 'An email and password are required to login',
}) })
} }

View File

@ -10,7 +10,7 @@ function normalizeProductOption(productOption: any) {
const { const {
node: { node: {
entityId, entityId,
values: { edges }, values: { edges = [] } = {},
...rest ...rest
}, },
} = productOption } = productOption

View File

@ -22,7 +22,7 @@ export const handler: SWRHook<SearchProductsHook> = {
const url = new URL(options.url!, 'http://a') const url = new URL(options.url!, 'http://a')
if (search) url.searchParams.set('search', search) if (search) url.searchParams.set('search', search)
if (Number.isInteger(categoryId)) if (Number.isInteger(Number(categoryId)))
url.searchParams.set('categoryId', String(categoryId)) url.searchParams.set('categoryId', String(categoryId))
if (Number.isInteger(brandId)) if (Number.isInteger(brandId))
url.searchParams.set('brandId', String(brandId)) url.searchParams.set('brandId', String(brandId))

View File

@ -40,7 +40,7 @@ export type OptionSelections = {
export type CartItemBody = Core.CartItemBody & { export type CartItemBody = Core.CartItemBody & {
productId: string // The product id is always required for BC productId: string // The product id is always required for BC
optionSelections?: OptionSelections optionSelections?: OptionSelections[]
} }
export type CartTypes = { export type CartTypes = {

View File

@ -1,19 +1,22 @@
## Saleor Provider ## Saleor Provider
**Demo:** TBD **Demo:** https://saleor.vercel.store/
Before getting starter, a [Saleor](https://saleor.io/) account and store is required before using the provider. You need a [Saleor](https://saleor.io/) instance, either in the cloud or self-hosted.
Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git): This provider requires Saleor **3.x** or higher.
Copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
```bash ```bash
cp framework/saleor/.env.template .env.local cp framework/saleor/.env.template .env.local
``` ```
Then, set the environment variables in `.env.local` to match the ones from your store. Then, set the environment following variables in your `.env.local`. Both, `NEXT_PUBLIC_SALEOR_API_URL` and `COMMERCE_IMAGE_HOST` must point to your own Saleor instance.
## Contribute ```
COMMERCE_PROVIDER=saleor
Our commitment to Open Source can be found [here](https://vercel.com/oss). NEXT_PUBLIC_SALEOR_API_URL=https://vercel.saleor.cloud/graphql/
NEXT_PUBLIC_SALEOR_CHANNEL=default-channel
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues). COMMERCE_IMAGE_HOST=vercel.saleor.cloud
```

View File

@ -22,7 +22,7 @@ export const handler: MutationHook<LoginHook> = {
if (!(email && password)) { if (!(email && password)) {
throw new CommerceError({ throw new CommerceError({
message: message:
'A first name, last name, email and password are required to login', 'An email and password are required to login',
}) })
} }

View File

@ -13,6 +13,8 @@ UI hooks and data fetching methods built from the ground up for e-commerce appli
``` ```
3. With the Vendure server running, start this project using `yarn dev` or `npm run dev`. 3. With the Vendure server running, start this project using `yarn dev` or `npm run dev`.
**Note:** The Vendure server needs to be configured to use the "cookie" tokenMethod rather than "bearer" to work with this provider. For more information see the [Managing Sessions docs](https://www.vendure.io/docs/storefront/managing-sessions/).
## Known Limitations ## Known Limitations
1. Vendure does not ship with built-in wishlist functionality. 1. Vendure does not ship with built-in wishlist functionality.

View File

@ -55,10 +55,13 @@ export default function FocusTrap({ children, focusFirst = false }: Props) {
} }
}, [root, children]) }, [root, children])
return React.createElement('div', { return React.createElement(
'div',
{
ref: root, ref: root,
children,
className: 'outline-none focus-trap', className: 'outline-none focus-trap',
tabIndex: -1, tabIndex: -1,
}) },
children
)
} }

View File

@ -18,10 +18,10 @@ export function useSearchMeta(asPath: string) {
c = parts[4] c = parts[4]
} }
setPathname(path) if (path !== pathname) setPathname(path)
if (c !== category) setCategory(c) if (c !== category) setCategory(c)
if (b !== brand) setBrand(b) if (b !== brand) setBrand(b)
}, [asPath]) }, [asPath, pathname, category, brand])
return { pathname, category, brand } return { pathname, category, brand }
} }

View File

@ -6,6 +6,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"analyze": "BUNDLE_ANALYZE=both yarn build", "analyze": "BUNDLE_ANALYZE=both yarn build",
"lint": "next lint",
"prettier-fix": "prettier --write .", "prettier-fix": "prettier --write .",
"find:unused": "npx next-unused", "find:unused": "npx next-unused",
"generate": "graphql-codegen", "generate": "graphql-codegen",
@ -67,6 +68,9 @@
"@types/node": "^15.12.4", "@types/node": "^15.12.4",
"@types/react": "^17.0.8", "@types/react": "^17.0.8",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"eslint": "^7.31.0",
"eslint-config-next": "^11.0.1",
"eslint-config-prettier": "^8.3.0",
"graphql": "^15.5.1", "graphql": "^15.5.1",
"husky": "^6.0.0", "husky": "^6.0.0",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
@ -82,6 +86,7 @@
}, },
"lint-staged": { "lint-staged": {
"**/*.{js,jsx,ts,tsx}": [ "**/*.{js,jsx,ts,tsx}": [
"eslint",
"prettier --write", "prettier --write",
"git add" "git add"
], ],

2704
yarn.lock

File diff suppressed because it is too large Load Diff