Fix auth & wishlist

This commit is contained in:
Catalin Pinte 2022-12-14 14:49:23 +02:00
parent 6d783eae35
commit 045f82bbe8
27 changed files with 139 additions and 126 deletions

View File

@ -34,7 +34,7 @@ const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] =
getLoggedInCustomerQuery, getLoggedInCustomerQuery,
undefined, undefined,
{ {
'Set-Cookie': `${config.customerCookie}=${token}`, cookie: `${config.customerCookie}=${token}`,
} }
) )
const { customer } = data const { customer } = data

View File

@ -11,12 +11,12 @@ const login: LoginEndpoint['handlers']['login'] = async ({
commerce, commerce,
}) => { }) => {
try { try {
const res = new Response() const response = await commerce.login({
await commerce.login({ variables: { email, password }, config, res }) variables: { email, password },
return { config,
status: res.status, })
headers: res.headers,
} return response
} catch (error) { } catch (error) {
// Check if the email and password didn't match an existing account // Check if the email and password didn't match an existing account
if (error instanceof FetcherError) { if (error instanceof FetcherError) {
@ -24,7 +24,7 @@ const login: LoginEndpoint['handlers']['login'] = async ({
invalidCredentials.test(error.message) invalidCredentials.test(error.message)
? 'Cannot find an account that matches the provided credentials' ? 'Cannot find an account that matches the provided credentials'
: error.message, : error.message,
{ status: error.status || 401 } { status: 401 }
) )
} else { } else {
throw error throw error

View File

@ -1,6 +1,5 @@
import type { SignupEndpoint } from '.' import type { SignupEndpoint } from '.'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors' import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { BigcommerceApiError } from '../../utils/errors' import { BigcommerceApiError } from '../../utils/errors'
const signup: SignupEndpoint['handlers']['signup'] = async ({ const signup: SignupEndpoint['handlers']['signup'] = async ({
@ -22,28 +21,25 @@ const signup: SignupEndpoint['handlers']['signup'] = async ({
}, },
]), ]),
}) })
} catch (error) {
if (error instanceof BigcommerceApiError && error.status === 422) {
const hasEmailError = '0.email' in error.data?.errors
// If there's an error with the email, it most likely means it's duplicated
if (hasEmailError) {
throw new CommerceAPIError('Email already in use', {
status: 400,
code: 'duplicated_email',
})
}
} else {
throw error
}
}
const res = new Response()
// Login the customer right after creating it // Login the customer right after creating it
await commerce.login({ variables: { email, password }, res, config }) const response = await commerce.login({
variables: { email, password },
config,
})
return { return response
headers: res.headers, } catch (error) {
// Display all validation errors from BigCommerce in a single error message
if (error instanceof BigcommerceApiError && error.status >= 400) {
const message = Object.values(error.data.errors).join('<br />')
throw new CommerceAPIError(message, {
status: 400,
code: 'invalid_request',
})
}
throw error
} }
} }

View File

@ -1,6 +1,7 @@
import { parseWishlistItem } from '../../utils/parse-item' import { parseWishlistItem } from '../../utils/parse-item'
import getCustomerId from '../../utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
import { normalizeWishlist } from '../../../lib/normalize'
const addItem: WishlistEndpoint['handlers']['addItem'] = async ({ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
body: { customerToken, item }, body: { customerToken, item },
@ -31,7 +32,7 @@ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
}), }),
}) })
return { return {
data, data: normalizeWishlist(data),
} }
} }
@ -47,7 +48,9 @@ const addItem: WishlistEndpoint['handlers']['addItem'] = async ({
) )
// Returns Wishlist // Returns Wishlist
return { data } return {
data: normalizeWishlist(data),
}
} }
export default addItem export default addItem

View File

@ -1,5 +1,4 @@
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors' import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import type { Wishlist } from '@vercel/commerce/types/wishlist'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
import getCustomerId from '../../utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
@ -9,8 +8,6 @@ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
config, config,
commerce, commerce,
}) => { }) => {
let result: { data?: Wishlist } = {}
if (customerToken) { if (customerToken) {
const customerId = const customerId =
customerToken && (await getCustomerId({ customerToken, config })) customerToken && (await getCustomerId({ customerToken, config }))
@ -25,10 +22,10 @@ const getWishlist: WishlistEndpoint['handlers']['getWishlist'] = async ({
config, config,
}) })
result = { data: wishlist } return { data: wishlist }
} }
return { data: result.data ?? null } return { data: null }
} }
export default getWishlist export default getWishlist

View File

@ -1,7 +1,9 @@
import type { Wishlist } from '@vercel/commerce/types/wishlist'
import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
import type { BCWishlist } from '../../utils/types'
import getCustomerId from '../../utils/get-customer-id'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors' import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { normalizeWishlist } from '../../../lib/normalize'
// Return wishlist info // Return wishlist info
const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
@ -11,6 +13,7 @@ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
}) => { }) => {
const customerId = const customerId =
customerToken && (await getCustomerId({ customerToken, config })) customerToken && (await getCustomerId({ customerToken, config }))
const { wishlist } = const { wishlist } =
(customerId && (customerId &&
(await commerce.getCustomerWishlist({ (await commerce.getCustomerWishlist({
@ -23,13 +26,12 @@ const removeItem: WishlistEndpoint['handlers']['removeItem'] = async ({
throw new CommerceAPIError('Wishlist not found', { status: 400 }) throw new CommerceAPIError('Wishlist not found', { status: 400 })
} }
const result = await config.storeApiFetch<{ data: Wishlist } | null>( const result = await config.storeApiFetch<{ data: BCWishlist } | null>(
`/v3/wishlists/${wishlist.id}/items/${itemId}`, `/v3/wishlists/${wishlist.id}/items/${itemId}`,
{ method: 'DELETE' } { method: 'DELETE' }
) )
const data = result?.data ?? null
return { data } return { data: result?.data ? normalizeWishlist(result.data) : null }
} }
export default removeItem export default removeItem

View File

@ -2,13 +2,11 @@ import type {
OperationContext, OperationContext,
OperationOptions, OperationOptions,
} from '@vercel/commerce/api/operations' } from '@vercel/commerce/api/operations'
import type { import type { GetCustomerWishlistOperation } from '@vercel/commerce/types/wishlist'
GetCustomerWishlistOperation, import type { RecursivePartial, BCWishlist } from '../utils/types'
Wishlist,
} from '@vercel/commerce/types/wishlist'
import type { RecursivePartial, RecursiveRequired } from '../utils/types'
import { BigcommerceConfig, Provider } from '..' import { BigcommerceConfig, Provider } from '..'
import getAllProducts, { ProductEdge } from './get-all-products' import { ProductEdge } from './get-all-products'
import { normalizeWishlist } from '../../lib/normalize'
export default function getCustomerWishlistOperation({ export default function getCustomerWishlistOperation({
commerce, commerce,
@ -41,18 +39,22 @@ export default function getCustomerWishlistOperation({
}): Promise<T['data']> { }): Promise<T['data']> {
config = commerce.getConfig(config) config = commerce.getConfig(config)
const { data = [] } = await config.storeApiFetch< const { data = [] } = await config.storeApiFetch<{ data: BCWishlist[] }>(
RecursivePartial<{ data: Wishlist[] }> `/v3/wishlists?customer_id=${variables.customerId}`
>(`/v3/wishlists?customer_id=${variables.customerId}`) )
const wishlist = data[0] const wishlist = data[0]
if (includeProducts && wishlist?.items?.length) { if (includeProducts && wishlist?.items?.length) {
const ids = wishlist.items const ids = []
?.map((item) => (item?.productId ? String(item?.productId) : null))
.filter((id): id is string => !!id)
if (ids?.length) { for (let i = 0; i < wishlist.items.length; i++) {
if (wishlist.items[i].product_id) {
ids.push(String(wishlist.items[i]?.product_id))
}
}
if (ids.length) {
const graphqlData = await commerce.getAllProducts({ const graphqlData = await commerce.getAllProducts({
variables: { first: 50, ids }, variables: { first: 50, ids },
config, config,
@ -66,7 +68,7 @@ export default function getCustomerWishlistOperation({
}, {}) }, {})
// Populate the wishlist items with the graphql products // Populate the wishlist items with the graphql products
wishlist.items.forEach((item) => { wishlist.items.forEach((item) => {
const product = item && productsById[Number(item.productId)] const product = item && productsById[Number(item.product_id)]
if (item && product) { if (item && product) {
// @ts-ignore Fix this type when the wishlist type is properly defined // @ts-ignore Fix this type when the wishlist type is properly defined
item.product = product item.product = product
@ -75,7 +77,7 @@ export default function getCustomerWishlistOperation({
} }
} }
return { wishlist: wishlist as RecursiveRequired<typeof wishlist> } return { wishlist: wishlist && normalizeWishlist(wishlist) }
} }
return getCustomerWishlist return getCustomerWishlist

View File

@ -22,26 +22,23 @@ export default function loginOperation({
async function login<T extends LoginOperation>(opts: { async function login<T extends LoginOperation>(opts: {
variables: T['variables'] variables: T['variables']
config?: BigcommerceConfig config?: BigcommerceConfig
res: Response
}): Promise<T['data']> }): Promise<T['data']>
async function login<T extends LoginOperation>( async function login<T extends LoginOperation>(
opts: { opts: {
variables: T['variables'] variables: T['variables']
config?: BigcommerceConfig config?: BigcommerceConfig
res: Response
} & OperationOptions } & OperationOptions
): Promise<T['data']> ): Promise<T['data']>
async function login<T extends LoginOperation>({ async function login<T extends LoginOperation>({
query = loginMutation, query = loginMutation,
variables, variables,
res: response,
config, config,
}: { }: {
query?: string query?: string
variables: T['variables'] variables: T['variables']
res: Response
config?: BigcommerceConfig config?: BigcommerceConfig
}): Promise<T['data']> { }): Promise<T['data']> {
config = commerce.getConfig(config) config = commerce.getConfig(config)
@ -50,6 +47,9 @@ export default function loginOperation({
query, query,
{ variables } { variables }
) )
const headers = new Headers()
// Bigcommerce returns a Set-Cookie header with the auth cookie // Bigcommerce returns a Set-Cookie header with the auth cookie
let cookie = res.headers.get('Set-Cookie') let cookie = res.headers.get('Set-Cookie')
@ -63,19 +63,13 @@ export default function loginOperation({
cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax') cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax')
} }
const prevCookie = response.headers.get('Set-Cookie') headers.set('Set-Cookie', cookie)
const newCookie = concatHeader(prevCookie, cookie)
if (newCookie) {
res.headers.set(
'Set-Cookie',
String(Array.isArray(newCookie) ? newCookie.join(',') : newCookie)
)
}
} }
return { return {
result: data.login?.result, result: data.login?.result,
headers,
status: res.status,
} }
} }

View File

@ -7,7 +7,7 @@ const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher =
async ( async (
query: string, query: string,
{ variables, preview } = {}, { variables, preview } = {},
options: { headers?: HeadersInit } = {} headers?: HeadersInit
): Promise<any> => { ): Promise<any> => {
// log.warn(query) // log.warn(query)
const config = getConfig() const config = getConfig()
@ -16,7 +16,7 @@ const fetchGraphqlApi: (getConfig: () => BigcommerceConfig) => GraphQLFetcher =
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${config.apiToken}`, Authorization: `Bearer ${config.apiToken}`,
...options.headers, ...headers,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({

View File

@ -20,7 +20,7 @@ async function getCustomerId({
getCustomerIdQuery, getCustomerIdQuery,
undefined, undefined,
{ {
'Set-Cookie': `${config.customerCookie}=${customerToken}`, cookie: `${config.customerCookie}=${customerToken}`,
} }
) )

View File

@ -5,3 +5,15 @@ export type RecursivePartial<T> = {
export type RecursiveRequired<T> = { export type RecursiveRequired<T> = {
[P in keyof T]-?: RecursiveRequired<T[P]> [P in keyof T]-?: RecursiveRequired<T[P]>
} }
export interface BCWishlist {
id: number
items: {
id: number
customer_id: number
is_public: boolean
product_id: number
variant_id: number
}[]
token: string
}

View File

@ -5,8 +5,10 @@ import type { Category, Brand } from '@vercel/commerce/types/site'
import type { BigcommerceCart, BCCategory, BCBrand } from '../types' import type { BigcommerceCart, BCCategory, BCBrand } from '../types'
import type { ProductNode } from '../api/operations/get-all-products' import type { ProductNode } from '../api/operations/get-all-products'
import type { definitions } from '../api/definitions/store-content' import type { definitions } from '../api/definitions/store-content'
import type { BCWishlist } from '../api/utils/types'
import getSlug from './get-slug' import getSlug from './get-slug'
import { Wishlist } from '@vercel/commerce/types/wishlist'
function normalizeProductOption(productOption: any) { function normalizeProductOption(productOption: any) {
const { const {
@ -137,3 +139,16 @@ export function normalizeBrand(brand: BCBrand): Brand {
path: `/${slug}`, path: `/${slug}`,
} }
} }
export function normalizeWishlist(wishlist: BCWishlist): Wishlist {
return {
id: String(wishlist.id),
token: wishlist.token,
items: wishlist.items.map((item: any) => ({
id: String(item.id),
productId: String(item.product_id),
variantId: String(item.variant_id),
product: item.product,
})),
}
}

View File

@ -35,7 +35,7 @@ const wishlistEndpoint: GetAPISchema<
if (req.method === 'GET') { if (req.method === 'GET') {
const body = getWishlistBodySchema.parse({ const body = getWishlistBodySchema.parse({
customerToken, customerToken,
includeProducts: !!products, includeProducts: input.includeProducts ?? !!products,
}) })
output = await handlers['getWishlist']({ ...ctx, body }) output = await handlers['getWishlist']({ ...ctx, body })
} }

View File

@ -44,7 +44,9 @@ export const transformRequest = (req: NextApiRequest, path: string) => {
body = JSON.stringify(req.body) body = JSON.stringify(req.body)
} }
return new NextRequest(`https://${req.headers.host}/api/commerce/${path}`, { const url = new URL(req.url || '/', `https://${req.headers.host}`)
return new NextRequest(url, {
headers, headers,
method: req.method, method: req.method,
body, body,

View File

@ -5,6 +5,7 @@ export const getCustomerAddressBodySchema = z.object({
}) })
export const customerSchema = z.object({ export const customerSchema = z.object({
customer: z.object({
id: z.string(), id: z.string(),
firstName: z.string(), firstName: z.string(),
lastName: z.string(), lastName: z.string(),
@ -13,6 +14,7 @@ export const customerSchema = z.object({
company: z.string().optional(), company: z.string().optional(),
notes: z.string().optional(), notes: z.string().optional(),
acceptsMarketing: z.boolean().optional(), acceptsMarketing: z.boolean().optional(),
}),
}) })
export const addressSchema = z.object({ export const addressSchema = z.object({

View File

@ -5,7 +5,7 @@ export const wishlistSchemaItem = z.object({
id: z.string(), id: z.string(),
productId: z.string(), productId: z.string(),
variantId: z.string(), variantId: z.string(),
product: productSchema, product: productSchema.optional(),
}) })
export const wishlistSchema = z.object({ export const wishlistSchema = z.object({
@ -15,7 +15,7 @@ export const wishlistSchema = z.object({
}) })
export const getWishlistBodySchema = z.object({ export const getWishlistBodySchema = z.object({
customerAccessToken: z.string(), customerToken: z.string().optional(),
includeProducts: z.boolean(), includeProducts: z.boolean(),
}) })
@ -25,17 +25,17 @@ export const wishlistItemBodySchema = z.object({
}) })
export const addItemBodySchema = z.object({ export const addItemBodySchema = z.object({
cartId: z.string().optional(), customerToken: z.string(),
item: wishlistItemBodySchema, item: wishlistItemBodySchema,
}) })
export const updateItemBodySchema = z.object({ export const updateItemBodySchema = z.object({
cartId: z.string(), customerToken: z.string(),
itemId: z.string(), itemId: z.string(),
item: wishlistItemBodySchema, item: wishlistItemBodySchema,
}) })
export const removeItemBodySchema = z.object({ export const removeItemBodySchema = z.object({
cartId: z.string(), customerToken: z.string(),
itemId: z.string(), itemId: z.string(),
}) })

View File

@ -26,6 +26,6 @@ export type LoginSchema = {
} }
export type LoginOperation = { export type LoginOperation = {
data: { result?: string } data: { result?: string; status?: number; headers?: Headers }
variables: unknown variables: unknown
} }

View File

@ -31,7 +31,6 @@ const LoginView: React.FC = () => {
email, email,
password, password,
}) })
setLoading(false)
closeModal() closeModal()
} catch ({ errors }) { } catch ({ errors }) {
if (errors instanceof Array) { if (errors instanceof Array) {
@ -39,15 +38,15 @@ const LoginView: React.FC = () => {
} else { } else {
setMessage('Unexpected error') setMessage('Unexpected error')
} }
setLoading(false)
setDisabled(false) setDisabled(false)
} finally {
setLoading(false)
} }
} }
const handleValidation = useCallback(() => { const handleValidation = useCallback(() => {
// Test for Alphanumeric password // Test for Alphanumeric password
const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password) const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password)
// Unable to send form unless fields are valid. // Unable to send form unless fields are valid.
if (dirty) { if (dirty) {
setDisabled(!validate(email) || password.length < 7 || !validPassword) setDisabled(!validate(email) || password.length < 7 || !validPassword)

View File

@ -38,7 +38,6 @@ const SignUpView: FC<Props> = () => {
lastName, lastName,
password, password,
}) })
setLoading(false)
closeModal() closeModal()
} catch ({ errors }) { } catch ({ errors }) {
console.error(errors) console.error(errors)
@ -47,8 +46,9 @@ const SignUpView: FC<Props> = () => {
} else { } else {
setMessage('Unexpected error') setMessage('Unexpected error')
} }
setLoading(false)
setDisabled(false) setDisabled(false)
} finally {
setLoading(false)
} }
} }

View File

@ -4,7 +4,6 @@
z-index: 10; z-index: 10;
height: 100vh; height: 100vh;
min-width: 100vw; min-width: 100vw;
transition: none;
} }
@media screen(lg) { @media screen(lg) {
@ -18,8 +17,7 @@
.link { .link {
@apply text-primary flex cursor-pointer px-6 py-3 @apply text-primary flex cursor-pointer px-6 py-3
transition ease-in-out duration-150 leading-6 transition ease-in-out duration-150 leading-6
font-medium items-center capitalize w-full box-border font-medium items-center capitalize w-full box-border outline-0;
outline-0;
} }
.link:hover { .link:hover {

View File

@ -35,13 +35,7 @@ export default function CustomerMenuContent() {
} }
return ( return (
<DropdownContent <DropdownContent sideOffset={10} id="CustomerMenuContent">
asChild
side="bottom"
sideOffset={10}
className={s.root}
id="CustomerMenuContent"
>
{LINKS.map(({ name, href }) => ( {LINKS.map(({ name, href }) => (
<DropdownMenuItem key={href}> <DropdownMenuItem key={href}>
<a <a

View File

@ -7,12 +7,15 @@
} }
.item { .item {
@apply ml-6 cursor-pointer relative transition ease-in-out @apply ml-6 flex items-center relative;
duration-100 flex items-center outline-none text-primary;
} }
.item:hover { .item > button {
@apply text-accent-6 transition scale-110 duration-100; @apply cursor-pointer transition ease-in-out duration-100 outline-none text-primary;
}
.item > button:hover {
@apply text-accent-6 transition scale-110 outline-none;
} }
.item:first-child { .item:first-child {

View File

@ -23,13 +23,8 @@ const UserNav: React.FC<{
}> = ({ className }) => { }> = ({ className }) => {
const { data } = useCart() const { data } = useCart()
const { data: isCustomerLoggedIn } = useCustomer() const { data: isCustomerLoggedIn } = useCustomer()
const { const { closeSidebarIfPresent, openModal, setSidebarView, openSidebar } =
toggleSidebar, useUI()
closeSidebarIfPresent,
openModal,
setSidebarView,
openSidebar,
} = useUI()
const itemsCount = data?.lineItems?.reduce(countItem, 0) ?? 0 const itemsCount = data?.lineItems?.reduce(countItem, 0) ?? 0
const DropdownTrigger = isCustomerLoggedIn const DropdownTrigger = isCustomerLoggedIn
@ -60,9 +55,9 @@ const UserNav: React.FC<{
{process.env.COMMERCE_WISHLIST_ENABLED && ( {process.env.COMMERCE_WISHLIST_ENABLED && (
<li className={s.item}> <li className={s.item}>
<Link href="/wishlist"> <Link href="/wishlist">
<a onClick={closeSidebarIfPresent} aria-label="Wishlist"> <button onClick={closeSidebarIfPresent} aria-label="Wishlist">
<Heart /> <Heart />
</a> </button>
</Link> </Link>
</li> </li>
)} )}

View File

@ -27,10 +27,8 @@ const WishlistButton: FC<Props> = ({
const { openModal, setModalView } = useUI() const { openModal, setModalView } = useUI()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
// @ts-ignore Wishlist is not always enabled
const itemInWishlist = data?.items?.find( const itemInWishlist = data?.items?.find(
// @ts-ignore Wishlist is not always enabled (item) => item.productId === productId && item.variantId === variant.id
(item) => item.product_id === productId && item.variant_id === variant.id
) )
const handleWishlistChange = async (e: any) => { const handleWishlistChange = async (e: any) => {

View File

@ -19,6 +19,7 @@ const WishlistCard: React.FC<{
item: WishlistItem item: WishlistItem
}> = ({ item }) => { }> = ({ item }) => {
const product: Product = item.product const product: Product = item.product
const { price } = usePrice({ const { price } = usePrice({
amount: product.price?.value, amount: product.price?.value,
baseAmount: product.price?.retailPrice, baseAmount: product.price?.retailPrice,

View File

@ -35,9 +35,9 @@ export async function getStaticProps({
} }
export default function Wishlist() { export default function Wishlist() {
const { data: customer } = useCustomer() const { data, isLoading, isEmpty } = useWishlist({
// @ts-ignore Shopify - Fix this types includeProducts: true,
const { data, isLoading, isEmpty } = useWishlist({ includeProducts: true }) })
return ( return (
<Container className="pt-4"> <Container className="pt-4">
@ -45,10 +45,10 @@ export default function Wishlist() {
<Text variant="pageHeading">My Wishlist</Text> <Text variant="pageHeading">My Wishlist</Text>
<div className="group flex flex-col"> <div className="group flex flex-col">
{isLoading ? ( {isLoading ? (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-6">
{rangeMap(12, (i) => ( {rangeMap(4, (i) => (
<Skeleton key={i}> <Skeleton key={i}>
<div className="w-60 h-60" /> <div className="w-full h-[279px]" />
</Skeleton> </Skeleton>
))} ))}
</div> </div>

View File

@ -23,8 +23,8 @@
"@components/*": ["components/*"], "@components/*": ["components/*"],
"@commerce": ["../packages/commerce/src"], "@commerce": ["../packages/commerce/src"],
"@commerce/*": ["../packages/commerce/src/*"], "@commerce/*": ["../packages/commerce/src/*"],
"@framework": ["../packages/local/src"], "@framework": ["../packages/shopify/src"],
"@framework/*": ["../packages/local/src/*"] "@framework/*": ["../packages/shopify/src/*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],