mirror of
https://github.com/vercel/commerce.git
synced 2025-07-04 20:21:21 +00:00
Merge branch 'main' into cart-a11y
This commit is contained in:
commit
db12719724
6
.eslintrc
Normal file
6
.eslintrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["next", "prettier"],
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off"
|
||||
}
|
||||
}
|
@ -70,6 +70,9 @@ const CartItem = ({
|
||||
if (item.quantity !== Number(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])
|
||||
|
||||
return (
|
||||
|
@ -73,7 +73,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
||||
<div className="flex items-center text-primary text-sm">
|
||||
<span className="text-primary">Created by</span>
|
||||
<a
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
href="https://vercel.com"
|
||||
aria-label="Vercel.com Link"
|
||||
target="_blank"
|
||||
|
@ -24,7 +24,7 @@ const Loading = () => (
|
||||
)
|
||||
|
||||
const dynamicProps = {
|
||||
loading: () => <Loading />,
|
||||
loading: Loading,
|
||||
}
|
||||
|
||||
const SignUpView = dynamic(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FC, InputHTMLAttributes, useEffect, useMemo } from 'react'
|
||||
import { FC, memo, useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './Searchbar.module.css'
|
||||
import { useRouter } from 'next/router'
|
||||
@ -13,7 +13,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
||||
|
||||
useEffect(() => {
|
||||
router.prefetch('/search')
|
||||
}, [])
|
||||
}, [router])
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
@ -32,8 +32,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
||||
}
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
return (
|
||||
<div className={cn(s.root, className)}>
|
||||
<label className="hidden" htmlFor={id}>
|
||||
Search
|
||||
@ -55,9 +54,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
export default Searchbar
|
||||
export default memo(Searchbar)
|
||||
|
@ -1,15 +1,19 @@
|
||||
import { memo } from 'react'
|
||||
import { Swatch } from '@components/product'
|
||||
import type { ProductOption } from '@commerce/types/product'
|
||||
import { SelectedOptions } from '../helpers'
|
||||
import React from 'react'
|
||||
|
||||
interface ProductOptionsProps {
|
||||
options: ProductOption[]
|
||||
selectedOptions: SelectedOptions
|
||||
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
||||
}
|
||||
|
||||
const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
|
||||
({ options, selectedOptions, setSelectedOptions }) => {
|
||||
const ProductOptions: React.FC<ProductOptionsProps> = ({
|
||||
options,
|
||||
selectedOptions,
|
||||
setSelectedOptions,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{options.map((opt) => (
|
||||
@ -31,8 +35,7 @@ const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
|
||||
setSelectedOptions((selectedOptions) => {
|
||||
return {
|
||||
...selectedOptions,
|
||||
[opt.displayName.toLowerCase()]:
|
||||
v.label.toLowerCase(),
|
||||
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
|
||||
}
|
||||
})
|
||||
}}
|
||||
@ -45,6 +48,5 @@ const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default ProductOptions
|
||||
export default memo(ProductOptions)
|
||||
|
@ -23,7 +23,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
||||
|
||||
useEffect(() => {
|
||||
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
||||
}, [])
|
||||
}, [product])
|
||||
|
||||
const variant = getProductVariant(product, selectedOptions)
|
||||
const addToCart = async () => {
|
||||
|
@ -66,17 +66,13 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
sliderContainerRef.current!.addEventListener(
|
||||
'touchstart',
|
||||
preventNavigation
|
||||
)
|
||||
const slider = sliderContainerRef.current!
|
||||
|
||||
slider.addEventListener('touchstart', preventNavigation)
|
||||
|
||||
return () => {
|
||||
if (sliderContainerRef.current) {
|
||||
sliderContainerRef.current!.removeEventListener(
|
||||
'touchstart',
|
||||
preventNavigation
|
||||
)
|
||||
if (slider) {
|
||||
slider.removeEventListener('touchstart', preventNavigation)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { FC, MouseEventHandler, memo } from 'react'
|
||||
import cn from 'classnames'
|
||||
import React from 'react'
|
||||
import s from './ProductSliderControl.module.css'
|
||||
import { ArrowLeft, ArrowRight } from '@components/icons'
|
||||
|
||||
interface ProductSliderControl {
|
||||
onPrev: React.MouseEventHandler<HTMLButtonElement>
|
||||
onNext: React.MouseEventHandler<HTMLButtonElement>
|
||||
onPrev: MouseEventHandler<HTMLButtonElement>
|
||||
onNext: MouseEventHandler<HTMLButtonElement>
|
||||
}
|
||||
|
||||
const ProductSliderControl: React.FC<ProductSliderControl> = React.memo(
|
||||
({ onPrev, onNext }) => (
|
||||
const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
|
||||
<div className={s.control}>
|
||||
<button
|
||||
className={cn(s.leftControl)}
|
||||
@ -27,5 +26,5 @@ const ProductSliderControl: React.FC<ProductSliderControl> = React.memo(
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
export default ProductSliderControl
|
||||
|
||||
export default memo(ProductSliderControl)
|
||||
|
@ -7,19 +7,19 @@ import { useRouter } from 'next/router'
|
||||
import { Layout } from '@components/common'
|
||||
import { ProductCard } from '@components/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 getSlug from '@lib/get-slug'
|
||||
import rangeMap from '@lib/range-map'
|
||||
|
||||
const SORT = Object.entries({
|
||||
const SORT = {
|
||||
'trending-desc': 'Trending',
|
||||
'latest-desc': 'Latest arrivals',
|
||||
'price-asc': 'Price: Low to high',
|
||||
'price-desc': 'Price: High to low',
|
||||
})
|
||||
}
|
||||
|
||||
import {
|
||||
filterQuery,
|
||||
@ -351,7 +351,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
|
||||
aria-haspopup="true"
|
||||
aria-expanded="true"
|
||||
>
|
||||
{sort ? `Sort: ${sort}` : 'Relevance'}
|
||||
{sort ? SORT[sort as keyof typeof SORT] : 'Relevance'}
|
||||
<svg
|
||||
className="-mr-1 ml-2 h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -398,7 +398,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
{SORT.map(([key, text]) => (
|
||||
{Object.entries(SORT).map(([key, text]) => (
|
||||
<li
|
||||
key={key}
|
||||
className={cn(
|
||||
|
@ -27,13 +27,15 @@ const Modal: FC<ModalProps> = ({ children, onClose }) => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
||||
const modal = ref.current
|
||||
|
||||
if (modal) {
|
||||
disableBodyScroll(modal, { reserveScrollBarGap: true })
|
||||
window.addEventListener('keydown', handleKey)
|
||||
}
|
||||
return () => {
|
||||
if (ref && ref.current) {
|
||||
enableBodyScroll(ref.current)
|
||||
if (modal) {
|
||||
enableBodyScroll(modal)
|
||||
}
|
||||
clearAllBodyScrollLocks()
|
||||
window.removeEventListener('keydown', handleKey)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC } from 'react'
|
||||
import { FC, memo } from 'react'
|
||||
import rangeMap from '@lib/range-map'
|
||||
import { Star } from '@components/icons'
|
||||
import cn from 'classnames'
|
||||
@ -7,8 +7,7 @@ export interface RatingProps {
|
||||
value: number
|
||||
}
|
||||
|
||||
const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
|
||||
return (
|
||||
const Quantity: FC<RatingProps> = ({ value = 5 }) => (
|
||||
<div className="flex flex-row py-6 text-accent-9">
|
||||
{rangeMap(5, (i) => (
|
||||
<span
|
||||
@ -22,6 +21,5 @@ const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default Quantity
|
||||
export default memo(Quantity)
|
||||
|
@ -13,8 +13,8 @@ interface SidebarProps {
|
||||
}
|
||||
|
||||
const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
|
||||
const innerRef = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||
const sidebarRef = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||
const contentRef = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||
|
||||
const onKeyDownSidebar = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.code === 'Escape') {
|
||||
@ -23,18 +23,18 @@ const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus()
|
||||
if (sidebarRef.current) {
|
||||
sidebarRef.current.focus()
|
||||
}
|
||||
|
||||
if (innerRef.current) {
|
||||
disableBodyScroll(innerRef.current, { reserveScrollBarGap: true })
|
||||
const contentElement = contentRef.current
|
||||
|
||||
if (contentElement) {
|
||||
disableBodyScroll(contentElement, { reserveScrollBarGap: true })
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (innerRef && innerRef.current) {
|
||||
enableBodyScroll(innerRef.current)
|
||||
}
|
||||
if (contentElement) enableBodyScroll(contentElement)
|
||||
clearAllBodyScrollLocks()
|
||||
}
|
||||
}, [])
|
||||
@ -42,7 +42,7 @@ const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(s.root)}
|
||||
ref={ref}
|
||||
ref={sidebarRef}
|
||||
onKeyDown={onKeyDownSidebar}
|
||||
tabIndex={1}
|
||||
>
|
||||
@ -50,7 +50,7 @@ const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
|
||||
<div className={s.backdrop} onClick={onClose} />
|
||||
<section className="absolute inset-y-0 right-0 max-w-full flex outline-none pl-10">
|
||||
<div className="h-full w-full md:w-screen md:max-w-md">
|
||||
<div className={s.sidebar} ref={innerRef}>
|
||||
<div className={s.sidebar} ref={contentRef}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,7 +10,7 @@ type BCCartItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
quantity?: number
|
||||
option_selections?: OptionSelections
|
||||
option_selections?: OptionSelections[]
|
||||
}
|
||||
|
||||
export const parseWishlistItem = (
|
||||
|
@ -10,7 +10,7 @@ function normalizeProductOption(productOption: any) {
|
||||
const {
|
||||
node: {
|
||||
entityId,
|
||||
values: { edges },
|
||||
values: { edges = [] } = {},
|
||||
...rest
|
||||
},
|
||||
} = productOption
|
||||
|
@ -40,7 +40,7 @@ export type OptionSelections = {
|
||||
|
||||
export type CartItemBody = Core.CartItemBody & {
|
||||
productId: string // The product id is always required for BC
|
||||
optionSelections?: OptionSelections
|
||||
optionSelections?: OptionSelections[]
|
||||
}
|
||||
|
||||
export type CartTypes = {
|
||||
|
@ -47,6 +47,12 @@ The app imports from the provider directly instead of the core commerce folder (
|
||||
|
||||
The provider folder should only depend on `framework/commerce` and dependencies in the main `package.json`. In the future we'll move the `framework` folder to a package that can be shared easily for multiple apps.
|
||||
|
||||
## Updating the list of known providers
|
||||
|
||||
Open [./config.js](./config.js) and add the provider name to the list in `PROVIDERS`.
|
||||
|
||||
Then, open [/.env.template](/.env.template) and add the provider name in the first line.
|
||||
|
||||
## Adding the provider hooks
|
||||
|
||||
Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks:
|
||||
|
@ -55,10 +55,13 @@ export default function FocusTrap({ children, focusFirst = false }: Props) {
|
||||
}
|
||||
}, [root, children])
|
||||
|
||||
return React.createElement('div', {
|
||||
return React.createElement(
|
||||
'div',
|
||||
{
|
||||
ref: root,
|
||||
children,
|
||||
className: 'outline-none focus-trap',
|
||||
tabIndex: -1,
|
||||
})
|
||||
},
|
||||
children
|
||||
)
|
||||
}
|
||||
|
@ -18,10 +18,10 @@ export function useSearchMeta(asPath: string) {
|
||||
c = parts[4]
|
||||
}
|
||||
|
||||
setPathname(path)
|
||||
if (path !== pathname) setPathname(path)
|
||||
if (c !== category) setCategory(c)
|
||||
if (b !== brand) setBrand(b)
|
||||
}, [asPath])
|
||||
}, [asPath, pathname, category, brand])
|
||||
|
||||
return { pathname, category, brand }
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"analyze": "BUNDLE_ANALYZE=both yarn build",
|
||||
"lint": "next lint",
|
||||
"prettier-fix": "prettier --write .",
|
||||
"find:unused": "npx next-unused",
|
||||
"generate": "graphql-codegen",
|
||||
@ -63,6 +64,9 @@
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/react": "^17.0.8",
|
||||
"deepmerge": "^4.2.2",
|
||||
"eslint": "^7.31.0",
|
||||
"eslint-config-next": "^11.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"graphql": "^15.5.1",
|
||||
"husky": "^6.0.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
@ -78,6 +82,7 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,ts,tsx}": [
|
||||
"eslint",
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user