
Добро пожаловать в The BSMNT Commerce Toolkit : пакеты, которые помогут вам отправить лучшие витрины, быстрее и с большей уверенностью.
Этот инструментарий помог нам - Basement.studio - поправляйте надежные витрины, которые могли бы справиться с сумасшедшим количеством трафика. Некоторые из них включают в себя: shopmrbeast.com, karljacobs.co, shopmrballen.com и runboo.fashion.
Если вы ищете пример с Next.js + Shopify, ознакомьтесь с нашим примером здесь.
Этот репозиторий в настоящее время содержит три пакета:
@bsmnt/storefront-hooks : React Hooks для управления Storefront Client Side.
@tanstack/react-query и localStorage @bsmnt/sdk-gen : CLI, который генерирует тип SAFE, GraphQL SDK.
graphql для производства @bsmnt/drop : Помощники для управления обратным отсчетом. Обычно используется для создания шумиха вокруг падения товара.
Они играют очень хорошо вместе, но также могут использоваться отдельно. Посмотрим, как они работают!
@bsmnt/storefront-hooks yarn add @bsmnt/storefront-hooks @tanstack/react-queryЭтот пакет экспорт:
createStorefrontHooks : функция , которая создает крючки, необходимые для взаимодействия с тележкой. import { createStorefrontHooks } from '@bsmnt/storefront-hooks'
export const hooks = createStorefrontHooks ( {
cartCookieKey : '' , // to save cart id in cookie
fetchers : { } , // hooks will use these internally
mutators : { } , // hooks will use these internally
createCartIfNotFound : false , // defaults to false. if true, will create a cart if none is found
queryClientConfig : { } // internal query client config
} )Взгляните на некоторые примеры:
localStorage import { createStorefrontHooks } from '@bsmnt/storefront-hooks'
type LineItem = {
merchandiseId : string
quantity : number
}
type Cart = {
id : string
lines : LineItem [ ]
}
export const {
QueryClientProvider ,
useCartQuery ,
useAddLineItemsToCartMutation ,
useOptimisticCartUpdate ,
useRemoveLineItemsFromCartMutation ,
useUpdateLineItemsInCartMutation
} = createStorefrontHooks < Cart > ( {
cartCookieKey : 'example-nextjs-localstorage' ,
fetchers : {
fetchCart : ( cartId : string ) => {
const cartFromLocalStorage = localStorage . getItem ( cartId )
if ( ! cartFromLocalStorage ) throw new Error ( 'Cart not found' )
const cart : Cart = JSON . parse ( cartFromLocalStorage )
return cart
}
} ,
mutators : {
addLineItemsToCart : ( cartId , lines ) => {
const cartFromLocalStorage = localStorage . getItem ( cartId )
if ( ! cartFromLocalStorage ) throw new Error ( 'Cart not found' )
const cart : Cart = JSON . parse ( cartFromLocalStorage )
// Add line if not exists, update quantity if exists
const updatedCart = lines . reduce ( ( cart , line ) => {
const lineIndex = cart . lines . findIndex (
( cartLine ) => cartLine . merchandiseId === line . merchandiseId
)
if ( lineIndex === - 1 ) {
cart . lines . push ( line )
} else {
cart . lines [ lineIndex ] ! . quantity += line . quantity
}
return cart
} , cart )
localStorage . setItem ( cartId , JSON . stringify ( updatedCart ) )
return {
data : updatedCart
}
} ,
createCart : ( ) => {
const cart : Cart = { id : 'cart' , lines : [ ] }
localStorage . setItem ( cart . id , JSON . stringify ( cart ) )
return { data : cart }
} ,
createCartWithLines : ( lines ) => {
const cart = { id : 'cart' , lines }
localStorage . setItem ( cart . id , JSON . stringify ( cart ) )
return { data : cart }
} ,
removeLineItemsFromCart : ( cartId , lineIds ) => {
const cartFromLocalStorage = localStorage . getItem ( cartId )
if ( ! cartFromLocalStorage ) throw new Error ( 'Cart not found' )
const cart : Cart = JSON . parse ( cartFromLocalStorage )
cart . lines = cart . lines . filter (
( line ) => ! lineIds . includes ( line . merchandiseId )
)
localStorage . setItem ( cart . id , JSON . stringify ( cart ) )
return {
data : cart
}
} ,
updateLineItemsInCart : ( cartId , lines ) => {
const cartFromLocalStorage = localStorage . getItem ( cartId )
if ( ! cartFromLocalStorage ) throw new Error ( 'Cart not found' )
const cart : Cart = JSON . parse ( cartFromLocalStorage )
cart . lines = lines
localStorage . setItem ( cart . id , JSON . stringify ( cart ) )
return {
data : cart
}
}
} ,
logging : {
onError ( type , error ) {
console . info ( { type , error } )
} ,
onSuccess ( type , data ) {
console . info ( { type , data } )
}
}
} )@bsmnt/sdk-gen # Given the following file tree:
.
└── storefront/
├── sdk-gen/
│ └── sdk.ts # generated with @bsmnt/sdk-gen
└── hooks.ts # <- we'll work hereЭтот пример зависит от @bsmnt/sdk-gen.
// ./storefront/hooks.ts
import { createStorefrontHooks } from '@bsmnt/storefront-hooks'
import { storefront } from '../sdk-gen/sdk'
import type {
CartGenqlSelection ,
CartUserErrorGenqlSelection ,
FieldsSelection ,
Cart as GenqlCart
} from '../sdk-gen/generated'
const cartFragment = {
id : true ,
checkoutUrl : true ,
createdAt : true ,
cost : { subtotalAmount : { amount : true , currencyCode : true } }
} satisfies CartGenqlSelection
export type Cart = FieldsSelection < GenqlCart , typeof cartFragment >
const userErrorFragment = {
message : true ,
code : true ,
field : true
} satisfies CartUserErrorGenqlSelection
export const {
QueryClientProvider ,
useCartQuery ,
useAddLineItemsToCartMutation ,
useOptimisticCartUpdate ,
useRemoveLineItemsFromCartMutation ,
useUpdateLineItemsInCartMutation
} = createStorefrontHooks ( {
cartCookieKey : 'example-nextjs-shopify' ,
fetchers : {
fetchCart : async ( cartId ) => {
const { cart } = await storefront . query ( {
cart : {
__args : { id : cartId } ,
... cartFragment
}
} )
if ( cart === undefined ) throw new Error ( 'Request failed' )
return cart
}
} ,
mutators : {
addLineItemsToCart : async ( cartId , lines ) => {
const { cartLinesAdd } = await storefront . mutation ( {
cartLinesAdd : {
__args : {
cartId ,
lines
} ,
cart : cartFragment ,
userErrors : userErrorFragment
}
} )
return {
data : cartLinesAdd ?. cart ,
userErrors : cartLinesAdd ?. userErrors
}
} ,
createCart : async ( ) => {
const { cartCreate } = await storefront . mutation ( {
cartCreate : {
cart : cartFragment ,
userErrors : userErrorFragment
}
} )
return {
data : cartCreate ?. cart ,
userErrors : cartCreate ?. userErrors
}
} ,
// TODO we could use the same mutation as createCart?
createCartWithLines : async ( lines ) => {
const { cartCreate } = await storefront . mutation ( {
cartCreate : {
__args : { input : { lines } } ,
cart : cartFragment ,
userErrors : userErrorFragment
}
} )
return {
data : cartCreate ?. cart ,
userErrors : cartCreate ?. userErrors
}
} ,
removeLineItemsFromCart : async ( cartId , lineIds ) => {
const { cartLinesRemove } = await storefront . mutation ( {
cartLinesRemove : {
__args : { cartId , lineIds } ,
cart : cartFragment ,
userErrors : userErrorFragment
}
} )
return {
data : cartLinesRemove ?. cart ,
userErrors : cartLinesRemove ?. userErrors
}
} ,
updateLineItemsInCart : async ( cartId , lines ) => {
const { cartLinesUpdate } = await storefront . mutation ( {
cartLinesUpdate : {
__args : {
cartId ,
lines : lines . map ( ( l ) => ( {
id : l . merchandiseId ,
quantity : l . quantity ,
attributes : l . attributes
} ) )
} ,
cart : cartFragment ,
userErrors : userErrorFragment
}
} )
return {
data : cartLinesUpdate ?. cart ,
userErrors : cartLinesUpdate ?. userErrors
}
}
} ,
createCartIfNotFound : true
} ) @bsmnt/sdk-gen yarn add @bsmnt/sdk-gen --dev Этот пакет устанавливает CLI с одной командой: generate . Запуск его наживут на вашу конечную точку GraphQL и генерирует типы типографий из ваших запросов и мутаций. Он основан на Genql, поэтому обязательно ознакомьтесь с их документами.
# By default, you can have a file tree like the following:
.
└── sdk-gen/
└── config.js // ./sdk-gen/config.js
/**
* @type {import("@bsmnt/sdk-gen").Config}
*/
module . exports = {
endpoint : '' ,
headers : { }
}И тогда вы можете запустить генератор:
yarn sdk-gen Это заглянет внутрь ./sdk-gen/ для файла config.js и для всех ваших файлов .{graphql,gql} в этом каталоге.
Если вы хотите использовать пользовательский каталог (а не по умолчанию, который является ./sdk-gen/ ), вы можете использовать аргумент --dir .
yarn sdk-gen --dir ./my-custom/directoryПосле запуска генератора вы должны получить следующий результат:
.
└── sdk-gen/
├── config.js
├── documents.gql
├── generated/ # <- generated
│ ├── index.ts
│ └── graphql.schema.json
└── sdk.ts # <- generated Внутри sdk.ts у вас будет экспорт bsmntSdk :
import config from './config'
import { createSdk } from './generated'
export const bsmntSdk = createSdk ( config )И это все. Вы должны быть в состоянии использовать это, чтобы нажать ваш API GraphQL безопасным образом.
Дополнительным преимуществом является то, что этот SDK не зависит от graphql . Многие клиенты GraphQL требуют этого в качестве зависимости со стороны сверстников (например, graphql-request ), которая добавляет важные KBS в пакет.
↳ Для стандартного способа использовать это с API Shopify Storefront, посмотрите на наш пример с помощью Next.js + Shopify.
@bsmnt/drop yarn add @bsmnt/dropЭтот пакет экспорт:
CountdownProvider : поставщик контекста для CountdownStoreuseCountdownStore : Hook , который потребляет контекст CountdownProvider и возвращает CountdownStorezeroPad : утилита , чтобы подать номер с нулями Чтобы использовать, просто оберните CountdownProvider , где бы вы ни хотели добавить свой обратный отсчет. Например, с следующей.js:
// _app.tsx
import type { AppProps } from 'next/app'
import { CountdownProvider } from '@bsmnt/drop'
import { Countdown } from '../components/countdown'
export default function App ( { Component , pageProps } : AppProps ) {
return (
< CountdownProvider
endDate = { Date . now ( ) + 1000 * 5 } // set this to 5 seconds from now just to test
countdownChildren = { < Countdown /> }
exitDelay = { 1000 } // optional, just to give some time to animate the countdown before finally unmounting it
startDate = { Date . now ( ) } // optional, just if you need some kind of progress UI
>
< Component { ... pageProps } />
</ CountdownProvider >
)
}И тогда ваш обратный отсчет может выглядеть как -то вроде:
import { useCountdownStore } from '@bsmnt/drop'
export const Countdown = ( ) => {
const humanTimeRemaining = useCountdownStore ( ) (
( state ) => state . humanTimeRemaining // keep in mind this is zustand, so you can slice this store
)
return (
< div >
< h1 > Countdown </ h1 >
< ul >
< li > Days: { humanTimeRemaining . days } </ li >
< li > Hours: { humanTimeRemaining . hours } </ li >
< li > Minutes: { humanTimeRemaining . minutes } </ li >
< li > Seconds: { humanTimeRemaining . seconds } </ li >
</ ul >
</ div >
)
} Если вы отображаете humanTimeRemaining.seconds , есть высокая вероятность того, что ваш сервер будет делать что -то отличное от вашего клиента, так как это значение будет изменяться каждую секунду.
В большинстве случаев вы можете безопасно suppressHydrationWarning (см. Выпуск № 21 для получения дополнительной информации):
import { useCountdownStore } from '@bsmnt/drop'
export const Countdown = ( ) => {
const humanTimeRemaining = useCountdownStore ( ) (
( state ) => state . humanTimeRemaining // keep in mind this is zustand, so you can slice this store
)
return (
< div >
< h1 > Countdown </ h1 >
< ul >
< li suppressHydrationWarning > Days: { humanTimeRemaining . days } </ li >
< li suppressHydrationWarning > Hours: { humanTimeRemaining . hours } </ li >
< li suppressHydrationWarning > Minutes: { humanTimeRemaining . minutes } </ li >
< li suppressHydrationWarning > Seconds: { humanTimeRemaining . seconds } </ li >
</ ul >
</ div >
)
}Если вы не хотите рисковать, то более безопасный вариант ждет, пока ваше приложение не будет увлажнено, прежде чем оставаться в реальном времени:
import { useEffect , useState } from 'react'
import { useCountdownStore } from '@bsmnt/drop'
const Countdown = ( ) => {
const humanTimeRemaining = useCountdownStore ( ) (
( state ) => state . humanTimeRemaining // keep in mind this is zustand, so you can slice this store
)
const [ hasRenderedOnce , setHasRenderedOnce ] = useState ( false )
useEffect ( ( ) => {
setHasRenderedOnce ( true )
} , [ ] )
return (
< div >
< h1 > Countdown </ h1 >
< ul >
< li > Days: { humanTimeRemaining . days } </ li >
< li > Hours: { humanTimeRemaining . hours } </ li >
< li > Minutes: { hasRenderedOnce ? humanTimeRemaining . minutes : '59' } </ li >
< li > Seconds: { hasRenderedOnce ? humanTimeRemaining . seconds : '59' } </ li >
</ ul >
</ div >
)
} Некоторые примеры, которые можно начать:
localStorage Приглашаются запросы. Проблемы приветствуются. Для серьезных изменений, пожалуйста, сначала откройте проблему, чтобы обсудить, что вы хотели бы изменить.
Грань