mirror of
https://github.com/vercel/commerce.git
synced 2025-07-03 19:51:22 +00:00
Merge remote-tracking branch 'origin-vercel/main' into spree-framework-poc
This commit is contained in:
commit
0ad4361369
@ -20,3 +20,6 @@ NEXT_PUBLIC_SWELL_PUBLIC_KEY=
|
||||
|
||||
NEXT_PUBLIC_SALEOR_API_URL=
|
||||
NEXT_PUBLIC_SALEOR_CHANNEL=
|
||||
|
||||
NEXT_PUBLIC_VENDURE_SHOP_API_URL=
|
||||
NEXT_PUBLIC_VENDURE_LOCAL_URL=
|
||||
|
6
.eslintrc
Normal file
6
.eslintrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["next", "prettier"],
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off"
|
||||
}
|
||||
}
|
@ -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.
|
||||
<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>
|
||||
|
@ -77,7 +77,6 @@ html {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
touch-action: manipulation;
|
||||
font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
@ -38,6 +38,7 @@ const LoginView: FC<Props> = () => {
|
||||
} catch ({ errors }) {
|
||||
setMessage(errors[0].message)
|
||||
setLoading(false)
|
||||
setDisabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,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)
|
||||
|
@ -7,6 +7,7 @@ import Image, { ImageProps } from 'next/image'
|
||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import ProductTag from '../ProductTag'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
product: Product
|
||||
@ -23,7 +24,6 @@ const ProductCard: FC<Props> = ({
|
||||
className,
|
||||
noNameTag = false,
|
||||
variant = 'default',
|
||||
...props
|
||||
}) => {
|
||||
const { price } = usePrice({
|
||||
amount: product.price.value,
|
||||
@ -38,7 +38,7 @@ const ProductCard: FC<Props> = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<Link href={`/product/${product.slug}`} {...props}>
|
||||
<Link href={`/product/${product.slug}`}>
|
||||
<a className={rootClassName}>
|
||||
{variant === 'slim' && (
|
||||
<>
|
||||
|
@ -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)
|
||||
|
@ -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,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)
|
||||
|
@ -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,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)
|
||||
|
@ -16,13 +16,14 @@ const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
|
||||
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
||||
const sidebar = ref.current
|
||||
|
||||
if (sidebar) {
|
||||
disableBodyScroll(sidebar, { reserveScrollBarGap: true })
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (ref && ref.current) {
|
||||
enableBodyScroll(ref.current)
|
||||
}
|
||||
if (sidebar) enableBodyScroll(sidebar)
|
||||
clearAllBodyScrollLocks()
|
||||
}
|
||||
}, [])
|
||||
|
@ -10,7 +10,7 @@ type BCCartItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
quantity?: number
|
||||
option_selections?: OptionSelections
|
||||
option_selections?: OptionSelections[]
|
||||
}
|
||||
|
||||
export const parseWishlistItem = (
|
||||
|
@ -16,7 +16,7 @@ export const handler: MutationHook<LoginHook> = {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
'An email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ function normalizeProductOption(productOption: any) {
|
||||
const {
|
||||
node: {
|
||||
entityId,
|
||||
values: { edges },
|
||||
values: { edges = [] } = {},
|
||||
...rest
|
||||
},
|
||||
} = productOption
|
||||
|
@ -22,7 +22,7 @@ export const handler: SWRHook<SearchProductsHook> = {
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
if (Number.isInteger(Number(categoryId)))
|
||||
url.searchParams.set('categoryId', String(categoryId))
|
||||
if (Number.isInteger(brandId))
|
||||
url.searchParams.set('brandId', String(brandId))
|
||||
|
@ -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 = {
|
||||
|
@ -1,19 +1,22 @@
|
||||
## 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
|
||||
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
|
||||
|
||||
Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
||||
|
||||
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_PROVIDER=saleor
|
||||
NEXT_PUBLIC_SALEOR_API_URL=https://vercel.saleor.cloud/graphql/
|
||||
NEXT_PUBLIC_SALEOR_CHANNEL=default-channel
|
||||
COMMERCE_IMAGE_HOST=vercel.saleor.cloud
|
||||
```
|
||||
|
@ -22,7 +22,7 @@ export const handler: MutationHook<LoginHook> = {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
'An email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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`.
|
||||
|
||||
**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
|
||||
|
||||
1. Vendure does not ship with built-in wishlist functionality.
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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",
|
||||
@ -67,6 +68,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",
|
||||
@ -82,6 +86,7 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,ts,tsx}": [
|
||||
"eslint",
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user