Merge branch 'main' into cart-a11y

This commit is contained in:
Federico Orlandau 2021-08-30 13:53:05 +02:00
commit db12719724
22 changed files with 1138 additions and 167 deletions

6
.eslintrc Normal file
View File

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

View File

@ -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 (

View File

@ -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"

View File

@ -24,7 +24,7 @@ const Loading = () => (
)
const dynamicProps = {
loading: () => <Loading />,
loading: Loading,
}
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 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,32 +32,29 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
}
}
return useMemo(
() => (
<div className={cn(s.root, className)}>
<label className="hidden" htmlFor={id}>
Search
</label>
<input
id={id}
className={s.input}
placeholder="Search for products..."
defaultValue={router.query.q}
onKeyUp={handleKeyUp}
/>
<div className={s.iconContainer}>
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
/>
</svg>
</div>
return (
<div className={cn(s.root, className)}>
<label className="hidden" htmlFor={id}>
Search
</label>
<input
id={id}
className={s.input}
placeholder="Search for products..."
defaultValue={router.query.q}
onKeyUp={handleKeyUp}
/>
<div className={s.iconContainer}>
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
/>
</svg>
</div>
),
[]
</div>
)
}
export default Searchbar
export default memo(Searchbar)

View File

@ -1,50 +1,52 @@
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 }) => {
return (
<div>
{options.map((opt) => (
<div className="pb-4" key={opt.displayName}>
<h2 className="uppercase font-medium text-sm tracking-wide">
{opt.displayName}
</h2>
<div className="flex flex-row py-4">
{opt.values.map((v, i: number) => {
const active = selectedOptions[opt.displayName.toLowerCase()]
return (
<Swatch
key={`${opt.id}-${i}`}
active={v.label.toLowerCase() === active}
variant={opt.displayName}
color={v.hexColors ? v.hexColors[0] : ''}
label={v.label}
onClick={() => {
setSelectedOptions((selectedOptions) => {
return {
...selectedOptions,
[opt.displayName.toLowerCase()]:
v.label.toLowerCase(),
}
})
}}
/>
)
})}
</div>
const ProductOptions: React.FC<ProductOptionsProps> = ({
options,
selectedOptions,
setSelectedOptions,
}) => {
return (
<div>
{options.map((opt) => (
<div className="pb-4" key={opt.displayName}>
<h2 className="uppercase font-medium text-sm tracking-wide">
{opt.displayName}
</h2>
<div className="flex flex-row py-4">
{opt.values.map((v, i: number) => {
const active = selectedOptions[opt.displayName.toLowerCase()]
return (
<Swatch
key={`${opt.id}-${i}`}
active={v.label.toLowerCase() === active}
variant={opt.displayName}
color={v.hexColors ? v.hexColors[0] : ''}
label={v.label}
onClick={() => {
setSelectedOptions((selectedOptions) => {
return {
...selectedOptions,
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
}
})
}}
/>
)
})}
</div>
))}
</div>
)
}
)
</div>
))}
</div>
)
}
export default ProductOptions
export default memo(ProductOptions)

View File

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

View File

@ -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)
}
}
}, [])

View File

@ -1,31 +1,30 @@
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 }) => (
<div className={s.control}>
<button
className={cn(s.leftControl)}
onClick={onPrev}
aria-label="Previous Product Image"
>
<ArrowLeft />
</button>
<button
className={cn(s.rightControl)}
onClick={onNext}
aria-label="Next Product Image"
>
<ArrowRight />
</button>
</div>
)
const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
<div className={s.control}>
<button
className={cn(s.leftControl)}
onClick={onPrev}
aria-label="Previous Product Image"
>
<ArrowLeft />
</button>
<button
className={cn(s.rightControl)}
onClick={onNext}
aria-label="Next Product Image"
>
<ArrowRight />
</button>
</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 { 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(

View File

@ -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)

View File

@ -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,21 +7,19 @@ export interface RatingProps {
value: number
}
const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
return (
<div className="flex flex-row py-6 text-accent-9">
{rangeMap(5, (i) => (
<span
key={`star_${i}`}
className={cn('inline-block ml-1 ', {
'text-accent-5': i >= Math.floor(value),
})}
>
<Star />
</span>
))}
</div>
)
})
const Quantity: FC<RatingProps> = ({ value = 5 }) => (
<div className="flex flex-row py-6 text-accent-9">
{rangeMap(5, (i) => (
<span
key={`star_${i}`}
className={cn('inline-block ml-1 ', {
'text-accent-5': i >= Math.floor(value),
})}
>
<Star />
</span>
))}
</div>
)
export default Quantity
export default memo(Quantity)

View File

@ -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>

View File

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

View File

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

View File

@ -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 = {

View File

@ -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:

View File

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

View File

@ -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 }
}

View File

@ -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"
],

986
yarn.lock

File diff suppressed because it is too large Load Diff