mirror of
https://github.com/vercel/commerce.git
synced 2025-06-20 06:01:21 +00:00
login and signup features
This commit is contained in:
parent
716b540966
commit
cf742e8fd5
@ -39,9 +39,7 @@ const Navbar: FC<NavbarProps> = ({ links }) => (
|
||||
<Searchbar />
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end flex-1 space-x-8">
|
||||
{/* <UserNav /> */}
|
||||
</div>
|
||||
<div className="flex justify-end flex-1 space-x-8">{<UserNav />}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex pb-4 lg:px-6 lg:hidden">
|
||||
|
@ -28,15 +28,15 @@ const LINKS = [
|
||||
{
|
||||
name: 'My Profile',
|
||||
href: '/profile',
|
||||
},
|
||||
} /*,
|
||||
{
|
||||
name: 'My Cart',
|
||||
href: '/cart',
|
||||
},
|
||||
},*/,
|
||||
]
|
||||
|
||||
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
||||
const logout = useLogout()
|
||||
//const logout = useLogout()
|
||||
const { pathname } = useRouter()
|
||||
const { theme, setTheme } = useTheme()
|
||||
const [display, setDisplay] = useState(false)
|
||||
@ -107,14 +107,14 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{/*<li>
|
||||
<a
|
||||
className={cn(s.link, 'border-t border-accents-2 mt-4')}
|
||||
onClick={() => logout()}
|
||||
>
|
||||
Logout
|
||||
</a>
|
||||
</li>
|
||||
</li>*/}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
@ -17,20 +17,20 @@ interface Props {
|
||||
const countItem = (count: number, item: LineItem) => count + item.quantity
|
||||
|
||||
const UserNav: FC<Props> = ({ className }) => {
|
||||
const { data } = useCart()
|
||||
//const { data } = useCart()
|
||||
const { data: customer } = useCustomer()
|
||||
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
|
||||
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
|
||||
//const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
|
||||
|
||||
return (
|
||||
<nav className={cn(s.root, className)}>
|
||||
<div className={s.mainContainer}>
|
||||
<ul className={s.list}>
|
||||
<li className={s.item} onClick={toggleSidebar}>
|
||||
{/*<li className={s.item} onClick={toggleSidebar}>
|
||||
<Bag />
|
||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||
</li>
|
||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||
process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||
<li className={s.item}>
|
||||
<Link href="/wishlist">
|
||||
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
|
||||
@ -38,7 +38,7 @@ const UserNav: FC<Props> = ({ className }) => {
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
)*/}
|
||||
<li className={s.item}>
|
||||
{customer ? (
|
||||
<DropdownMenu />
|
||||
|
@ -0,0 +1,39 @@
|
||||
import type { CustomerEndpoint } from '.'
|
||||
import getCustomerQuery from '../../../utils/queries/get-customer-query'
|
||||
const jwt = require('jwt-simple')
|
||||
|
||||
export const getLoggedInCustomerQuery = getCustomerQuery
|
||||
export type Customer = NonNullable<any['customer']>
|
||||
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = async ({
|
||||
req,
|
||||
res,
|
||||
config,
|
||||
}) => {
|
||||
const { customerCookie } = config
|
||||
|
||||
// customerId
|
||||
if (req.cookies[config.customerCookie]) {
|
||||
const id = jwt.decode(req.cookies[config.customerCookie], customerCookie)
|
||||
|
||||
if (id) {
|
||||
const { data } = await config.fetch<any>(getCustomerQuery, {
|
||||
variables: { id },
|
||||
})
|
||||
|
||||
const { customer } = data
|
||||
|
||||
if (!customer) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Customer not found', code: 'not_found' }],
|
||||
})
|
||||
}
|
||||
|
||||
return res.status(200).json({ data: { customer } })
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ data: null })
|
||||
}
|
||||
|
||||
export default getLoggedInCustomer
|
18
framework/commercetools/api/endpoints/customer/index.ts
Normal file
18
framework/commercetools/api/endpoints/customer/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||
import customerEndpoint from '@commerce/api/endpoints/customer'
|
||||
import type { CustomerSchema } from '../../../types/customer'
|
||||
import type { CommercetoolsAPI } from '../..'
|
||||
import getLoggedInCustomer from './get-logged-in-customer'
|
||||
|
||||
export type CustomerAPI = GetAPISchema<CommercetoolsAPI, CustomerSchema>
|
||||
|
||||
export type CustomerEndpoint = CustomerAPI['endpoint']
|
||||
|
||||
export const handlers: CustomerEndpoint['handlers'] = { getLoggedInCustomer }
|
||||
|
||||
const customerApi = createEndpoint<CustomerAPI>({
|
||||
handler: customerEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default customerApi
|
18
framework/commercetools/api/endpoints/login/index.ts
Normal file
18
framework/commercetools/api/endpoints/login/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||
import loginEndpoint from '@commerce/api/endpoints/login'
|
||||
import type { LoginSchema } from '../../../types/login'
|
||||
import type { CommercetoolsAPI } from '../..'
|
||||
import login from './login'
|
||||
|
||||
export type LoginAPI = GetAPISchema<CommercetoolsAPI, LoginSchema>
|
||||
|
||||
export type LoginEndpoint = LoginAPI['endpoint']
|
||||
|
||||
export const handlers: LoginEndpoint['handlers'] = { login }
|
||||
|
||||
const loginApi = createEndpoint<LoginAPI>({
|
||||
handler: loginEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default loginApi
|
48
framework/commercetools/api/endpoints/login/login.ts
Normal file
48
framework/commercetools/api/endpoints/login/login.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { FetcherError } from '@commerce/utils/errors'
|
||||
import type { LoginEndpoint } from '.'
|
||||
|
||||
const invalidCredentials = /invalid credentials/i
|
||||
|
||||
const login: LoginEndpoint['handlers']['login'] = async ({
|
||||
res,
|
||||
body: { email, password },
|
||||
config,
|
||||
commerce,
|
||||
}) => {
|
||||
if (!(email && password)) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await commerce.login({
|
||||
variables: { data: { email, password } },
|
||||
config,
|
||||
res,
|
||||
})
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FetcherError &&
|
||||
invalidCredentials.test(error.message)
|
||||
) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'Cannot find an account that matches the provided credentials',
|
||||
code: 'invalid_credentials',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
res.status(200).json({ data: null })
|
||||
}
|
||||
|
||||
export default login
|
18
framework/commercetools/api/endpoints/signup/index.ts
Normal file
18
framework/commercetools/api/endpoints/signup/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||
import signupEndpoint from '@commerce/api/endpoints/signup'
|
||||
import type { SignupSchema } from '../../../types/signup'
|
||||
import type { CommercetoolsAPI } from '../..'
|
||||
import signup from './signup'
|
||||
|
||||
export type SignupAPI = GetAPISchema<CommercetoolsAPI, SignupSchema>
|
||||
|
||||
export type SignupEndpoint = SignupAPI['endpoint']
|
||||
|
||||
export const handlers: SignupEndpoint['handlers'] = { signup }
|
||||
|
||||
const singupApi = createEndpoint<SignupAPI>({
|
||||
handler: signupEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default singupApi
|
48
framework/commercetools/api/endpoints/signup/signup.ts
Normal file
48
framework/commercetools/api/endpoints/signup/signup.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { SignupEndpoint } from '.'
|
||||
import type { CommercetoolsCustomer } from '../../../types/customer'
|
||||
import { signupMutation } from '../../../utils/mutations/sign-up-mutation'
|
||||
const jwt = require('jwt-simple')
|
||||
import { serialize } from 'cookie'
|
||||
|
||||
const signup: SignupEndpoint['handlers']['signup'] = async ({
|
||||
res,
|
||||
body: { firstName, lastName, email, password },
|
||||
config,
|
||||
}) => {
|
||||
if (!(firstName && lastName && email && password)) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Invalid request' }],
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const newCustomer: CommercetoolsCustomer = (
|
||||
await config.fetch(signupMutation, {
|
||||
variables: { data: { firstName, lastName, email, password } },
|
||||
})
|
||||
).data?.customerSignUp.customer
|
||||
|
||||
const { customerCookie } = config
|
||||
const customerExpires = new Date(Date.now() + 30 * 30) // 1 month
|
||||
const customerToken = jwt.encode(newCustomer.id, customerCookie)
|
||||
|
||||
res.setHeader(
|
||||
'Set-Cookie',
|
||||
serialize(customerCookie, customerToken, {
|
||||
maxAge: 30,
|
||||
path: '/',
|
||||
expires: customerExpires,
|
||||
})
|
||||
)
|
||||
|
||||
res.status(200).json({ data: null })
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [{ message: 'Error on response' }],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default signup
|
@ -16,6 +16,9 @@ import getAllPages from '@framework/api/operations/get-all-pages'
|
||||
import login from '@framework/api/operations/login'
|
||||
import getCustomerWishlist from '@framework/api/operations/get-customer-wishlist'
|
||||
import getSiteInfo from '@framework/api/operations/get-site-info'
|
||||
import type { LoginAPI } from './endpoints/login'
|
||||
import type { CustomerAPI } from './endpoints/customer'
|
||||
import type { SignupAPI } from './endpoints/signup'
|
||||
|
||||
export interface CommercetoolsConfig extends CommerceAPIConfig {
|
||||
locale: string
|
||||
@ -39,6 +42,7 @@ const CLIENT_SECRET = process.env.CTP_CLIENT_SECRET || 'projectKey'
|
||||
const AUTH_URL = process.env.CTP_AUTH_URL || 'projectKey'
|
||||
const API_URL = process.env.CTP_API_URL || 'projectKey'
|
||||
const CONCURRENCY = process.env.CTP_CONCURRENCY || 0
|
||||
const CUSTOMER_COOKIE_NAME = process.env.CTP_CUSTOMER_COOKIE || 'projectKey'
|
||||
|
||||
if (!API_URL) {
|
||||
throw new Error(
|
||||
@ -72,7 +76,7 @@ const config: CommercetoolsConfig = {
|
||||
apiToken: '',
|
||||
cartCookie: '',
|
||||
cartCookieMaxAge: 0,
|
||||
customerCookie: '',
|
||||
customerCookie: CUSTOMER_COOKIE_NAME,
|
||||
fetch: fetchGraphql,
|
||||
fetchProducts: fetchProducts,
|
||||
}
|
||||
@ -88,6 +92,8 @@ const operations = {
|
||||
login,
|
||||
}
|
||||
|
||||
export type APIs = LoginAPI | CustomerAPI | SignupAPI
|
||||
|
||||
export const provider = { config, operations }
|
||||
|
||||
export type Provider = typeof provider
|
||||
|
@ -3,18 +3,22 @@ import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@commerce/api/operations'
|
||||
import { Provider, CommercetoolsConfig } from '@framework/api'
|
||||
import type { LoginOperation } from '../../types/login'
|
||||
import { Provider, CommercetoolsConfig } from '..'
|
||||
import { loginMutation } from '../../utils/mutations/log-in-mutation'
|
||||
import { serialize } from 'cookie'
|
||||
const jwt = require('jwt-simple')
|
||||
|
||||
export default function loginOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
async function login<T extends { variables: any; data: any }>(opts: {
|
||||
async function login<T extends LoginOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<CommercetoolsConfig>
|
||||
res: ServerResponse
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function login<T extends { variables: any; data: any }>(
|
||||
async function login<T extends LoginOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<CommercetoolsConfig>
|
||||
@ -22,11 +26,11 @@ export default function loginOperation({
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function login<T extends { variables: any; data: any }>({
|
||||
query = '',
|
||||
async function login<T extends LoginOperation>({
|
||||
query = loginMutation,
|
||||
variables,
|
||||
res: response,
|
||||
config: cfg,
|
||||
res: response,
|
||||
}: {
|
||||
query?: string
|
||||
variables: T['variables']
|
||||
@ -34,10 +38,29 @@ export default function loginOperation({
|
||||
config?: Partial<CommercetoolsConfig>
|
||||
}): Promise<T['data']> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
return {
|
||||
result: '',
|
||||
const expireTime = new Date(Date.now() + 30 * 30) // 1 month
|
||||
|
||||
const { data } = await config.fetch<any>(query, {
|
||||
variables,
|
||||
})
|
||||
|
||||
if (data) {
|
||||
const customerToken = jwt.encode(
|
||||
data.customerSignIn.customer.id,
|
||||
config.customerCookie
|
||||
)
|
||||
response.setHeader('Set-Cookie', [
|
||||
serialize(config.customerCookie, customerToken, {
|
||||
maxAge: 30,
|
||||
path: '/',
|
||||
expires: expireTime,
|
||||
}),
|
||||
])
|
||||
|
||||
return { result: data.customerSignIn.customer.id }
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
return login
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
// export { default as useLogin } from './use-login'
|
||||
// export { default as useLogout } from './use-logout'
|
||||
// export { default as useSignup } from './use-signup'
|
||||
export { default as useLogin } from './use-login'
|
||||
export { default as useLogout } from './use-logout'
|
||||
export { default as useSignup } from './use-signup'
|
||||
|
@ -0,0 +1,36 @@
|
||||
import { useCallback } from 'react'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import useLogin, { UseLogin } from '@commerce/auth/use-login'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
fetchOptions: {
|
||||
url: '/api/login',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: { email, password }, options, fetch }) {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message: 'An email and password are required to login',
|
||||
})
|
||||
}
|
||||
return fetch({
|
||||
...options,
|
||||
body: { email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
return useCallback(
|
||||
async function login(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
|
||||
import type { SignupHook } from '../types/signup'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export const handler: MutationHook<SignupHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/signup',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({
|
||||
input: { firstName, lastName, email, password },
|
||||
options,
|
||||
fetch,
|
||||
}) {
|
||||
if (!(firstName && lastName && email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to signup',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { firstName, lastName, email, password },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function signup(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
@ -1 +1 @@
|
||||
// export { default as useCustomer } from './use-customer'
|
||||
export { default as useCustomer } from './use-customer'
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { SWRHook } from '@commerce/utils/types'
|
||||
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
|
||||
import type { CustomerHook } from '../types/customer'
|
||||
export default useCustomer as UseCustomer<typeof handler>
|
||||
export const handler: SWRHook<CustomerHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/customer',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher({ options, fetch }) {
|
||||
const data = await fetch(options)
|
||||
return data?.customer ?? null
|
||||
},
|
||||
useHook: ({ useData }) => (input) => {
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input?.swrOptions,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
@ -3,11 +3,11 @@ import { Provider } from '@commerce'
|
||||
// import { handler as useAddItem } from './cart/use-add-item'
|
||||
// import { handler as useUpdateItem } from './cart/use-update-item'
|
||||
// import { handler as useRemoveItem } from './cart/use-remove-item'
|
||||
// import { handler as useCustomer } from './customer/use-customer'
|
||||
import { handler as useCustomer } from './customer/use-customer'
|
||||
import { handler as useSearch } from './product/use-search'
|
||||
// import { handler as useLogin } from './auth/use-login'
|
||||
import { handler as useLogin } from './auth/use-login'
|
||||
// import { handler as useLogout } from './auth/use-logout'
|
||||
// import { handler as useSignup } from './auth/use-signup'
|
||||
import { handler as useSignup } from './auth/use-signup'
|
||||
import fetcher from './fetcher'
|
||||
|
||||
// Export a provider with the CommerceHooks
|
||||
@ -16,9 +16,9 @@ export const commercetoolsProvider: Provider = {
|
||||
cartCookie: 'session',
|
||||
fetcher,
|
||||
// cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||
// customer: { useCustomer },
|
||||
customer: { useCustomer },
|
||||
products: { useSearch },
|
||||
// auth: { useLogin, useLogout, useSignup }
|
||||
auth: { useLogin, useSignup }, // useLogout
|
||||
}
|
||||
|
||||
export type CommercetoolsProvider = typeof commercetoolsProvider
|
||||
|
17
framework/commercetools/utils/mutations/log-in-mutation.ts
Normal file
17
framework/commercetools/utils/mutations/log-in-mutation.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/* GraphQL */
|
||||
export const loginMutation = `
|
||||
mutation customerSignInDraft($data: CustomerSignInDraft!) {
|
||||
customerSignIn(draft: $data) {
|
||||
customer {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
password
|
||||
}
|
||||
cart{
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
10
framework/commercetools/utils/mutations/sign-up-mutation.ts
Normal file
10
framework/commercetools/utils/mutations/sign-up-mutation.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/* GraphQL */
|
||||
export const signupMutation = `
|
||||
mutation createCustomer($data: CustomerSignUpDraft!) {
|
||||
customerSignUp (draft: $data) {
|
||||
customer {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
11
framework/commercetools/utils/queries/get-customer-query.ts
Normal file
11
framework/commercetools/utils/queries/get-customer-query.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/* GraphQL */
|
||||
const getCustomerQuery = `query ($id: String!) {
|
||||
customer(id: $id) {
|
||||
id
|
||||
firstName
|
||||
lastName
|
||||
email
|
||||
}
|
||||
}`
|
||||
|
||||
export default getCustomerQuery
|
@ -35,6 +35,7 @@
|
||||
"email-validator": "^2.0.4",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"js-cookie": "^2.2.1",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"keen-slider": "^5.2.4",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.random": "^3.2.0",
|
||||
|
@ -3827,6 +3827,11 @@ jws@^3.2.2:
|
||||
jwa "^1.4.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
jwt-simple@^0.5.6:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/jwt-simple/-/jwt-simple-0.5.6.tgz#3357adec55b26547114157be66748995b75b333a"
|
||||
integrity sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==
|
||||
|
||||
keen-slider@^5.2.4:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/keen-slider/-/keen-slider-5.4.0.tgz#e5a949e2bb237d7d6b068458dcda59ae9c415254"
|
||||
|
Loading…
x
Reference in New Issue
Block a user