forked from crowetic/commerce
Add Next.js ESLint (#425)
* Added Next.js eslint * added eslint to lint-staged * Added eslint config for prettier * Fixed eslint issues in multiple files * Fixed error in linter
This commit is contained in:
parent
0603b342be
commit
0e7e7b7d5f
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)) {
|
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 (
|
||||||
|
@ -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"
|
||||||
|
@ -24,7 +24,7 @@ const Loading = () => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const dynamicProps = {
|
const dynamicProps = {
|
||||||
loading: () => <Loading />,
|
loading: Loading,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignUpView = dynamic(
|
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 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,32 +32,29 @@ 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
|
</label>
|
||||||
</label>
|
<input
|
||||||
<input
|
id={id}
|
||||||
id={id}
|
className={s.input}
|
||||||
className={s.input}
|
placeholder="Search for products..."
|
||||||
placeholder="Search for products..."
|
defaultValue={router.query.q}
|
||||||
defaultValue={router.query.q}
|
onKeyUp={handleKeyUp}
|
||||||
onKeyUp={handleKeyUp}
|
/>
|
||||||
/>
|
<div className={s.iconContainer}>
|
||||||
<div className={s.iconContainer}>
|
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
||||||
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
<path
|
||||||
<path
|
fillRule="evenodd"
|
||||||
fillRule="evenodd"
|
clipRule="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"
|
||||||
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>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
</div>
|
||||||
[]
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Searchbar
|
export default memo(Searchbar)
|
||||||
|
@ -1,50 +1,52 @@
|
|||||||
|
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,
|
||||||
return (
|
selectedOptions,
|
||||||
<div>
|
setSelectedOptions,
|
||||||
{options.map((opt) => (
|
}) => {
|
||||||
<div className="pb-4" key={opt.displayName}>
|
return (
|
||||||
<h2 className="uppercase font-medium text-sm tracking-wide">
|
<div>
|
||||||
{opt.displayName}
|
{options.map((opt) => (
|
||||||
</h2>
|
<div className="pb-4" key={opt.displayName}>
|
||||||
<div className="flex flex-row py-4">
|
<h2 className="uppercase font-medium text-sm tracking-wide">
|
||||||
{opt.values.map((v, i: number) => {
|
{opt.displayName}
|
||||||
const active = selectedOptions[opt.displayName.toLowerCase()]
|
</h2>
|
||||||
return (
|
<div className="flex flex-row py-4">
|
||||||
<Swatch
|
{opt.values.map((v, i: number) => {
|
||||||
key={`${opt.id}-${i}`}
|
const active = selectedOptions[opt.displayName.toLowerCase()]
|
||||||
active={v.label.toLowerCase() === active}
|
return (
|
||||||
variant={opt.displayName}
|
<Swatch
|
||||||
color={v.hexColors ? v.hexColors[0] : ''}
|
key={`${opt.id}-${i}`}
|
||||||
label={v.label}
|
active={v.label.toLowerCase() === active}
|
||||||
onClick={() => {
|
variant={opt.displayName}
|
||||||
setSelectedOptions((selectedOptions) => {
|
color={v.hexColors ? v.hexColors[0] : ''}
|
||||||
return {
|
label={v.label}
|
||||||
...selectedOptions,
|
onClick={() => {
|
||||||
[opt.displayName.toLowerCase()]:
|
setSelectedOptions((selectedOptions) => {
|
||||||
v.label.toLowerCase(),
|
return {
|
||||||
}
|
...selectedOptions,
|
||||||
})
|
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
|
||||||
}}
|
}
|
||||||
/>
|
})
|
||||||
)
|
}}
|
||||||
})}
|
/>
|
||||||
</div>
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
)
|
</div>
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
export default ProductOptions
|
export default memo(ProductOptions)
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -1,31 +1,30 @@
|
|||||||
|
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)}
|
onClick={onPrev}
|
||||||
onClick={onPrev}
|
aria-label="Previous Product Image"
|
||||||
aria-label="Previous Product Image"
|
>
|
||||||
>
|
<ArrowLeft />
|
||||||
<ArrowLeft />
|
</button>
|
||||||
</button>
|
<button
|
||||||
<button
|
className={cn(s.rightControl)}
|
||||||
className={cn(s.rightControl)}
|
onClick={onNext}
|
||||||
onClick={onNext}
|
aria-label="Next Product Image"
|
||||||
aria-label="Next Product Image"
|
>
|
||||||
>
|
<ArrowRight />
|
||||||
<ArrowRight />
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
export default ProductSliderControl
|
|
||||||
|
export default memo(ProductSliderControl)
|
||||||
|
@ -7,7 +7,7 @@ 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'
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,21 +7,19 @@ 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
|
key={`star_${i}`}
|
||||||
key={`star_${i}`}
|
className={cn('inline-block ml-1 ', {
|
||||||
className={cn('inline-block ml-1 ', {
|
'text-accent-5': i >= Math.floor(value),
|
||||||
'text-accent-5': i >= Math.floor(value),
|
})}
|
||||||
})}
|
>
|
||||||
>
|
<Star />
|
||||||
<Star />
|
</span>
|
||||||
</span>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default Quantity
|
export default memo(Quantity)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -55,10 +55,13 @@ export default function FocusTrap({ children, focusFirst = false }: Props) {
|
|||||||
}
|
}
|
||||||
}, [root, children])
|
}, [root, children])
|
||||||
|
|
||||||
return React.createElement('div', {
|
return React.createElement(
|
||||||
ref: root,
|
'div',
|
||||||
children,
|
{
|
||||||
className: 'outline-none focus-trap',
|
ref: root,
|
||||||
tabIndex: -1,
|
className: 'outline-none focus-trap',
|
||||||
})
|
tabIndex: -1,
|
||||||
|
},
|
||||||
|
children
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
@ -63,6 +64,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",
|
||||||
@ -78,6 +82,7 @@
|
|||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*.{js,jsx,ts,tsx}": [
|
"**/*.{js,jsx,ts,tsx}": [
|
||||||
|
"eslint",
|
||||||
"prettier --write",
|
"prettier --write",
|
||||||
"git add"
|
"git add"
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user