forked from crowetic/commerce
Merge branch 'master' into agnostic
This commit is contained in:
commit
9ffc60ec95
24
README.md
24
README.md
@ -9,7 +9,7 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
|||||||
|
|
||||||
This project is currently <b>under development</b>.
|
This project is currently <b>under development</b>.
|
||||||
|
|
||||||
## Goals and Features
|
## Features
|
||||||
|
|
||||||
- Performant by default
|
- Performant by default
|
||||||
- SEO Ready
|
- SEO Ready
|
||||||
@ -22,14 +22,24 @@ This project is currently <b>under development</b>.
|
|||||||
- Dark Mode Support
|
- Dark Mode Support
|
||||||
|
|
||||||
## Work in progress
|
## Work in progress
|
||||||
|
|
||||||
We're using Github Projects to keep track of issues in progress and todo's. Here is our [Board](https://github.com/vercel/commerce/projects/1)
|
We're using Github Projects to keep track of issues in progress and todo's. Here is our [Board](https://github.com/vercel/commerce/projects/1)
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
Next.js Commerce integrates out-of-the-box with BigCommerce. We plan to support all major ecommerce backends.
|
Next.js Commerce integrates out-of-the-box with BigCommerce. We plan to support all major ecommerce backends.
|
||||||
|
|
||||||
If you wish to work on a new cms/headless ecommerce provider, please check this repo https://github.com/vercel/commerce-framework and open a PR with your functions and utilities under the standard mentioned in the README.
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
* **Next.js Commerce** should have a completely data **agnostic** UI
|
||||||
|
* **Aware of schema**: should ship with the right data schemas and types.
|
||||||
|
* All providers should return the right data types and schemas to blend correctly with Next.js Commerce.
|
||||||
|
* `@framework` will be the alias utilized in commerce and it will map to the ecommerce provider of preference- e.g BigCommerce, Shopify, Swell. All providers should expose the same standardized functions. _Note that the same applies for recipes using a CMS + an ecommerce provider._
|
||||||
|
|
||||||
|
There is a `framework` folder in the root folder that will contain multiple ecommerce providers.
|
||||||
|
|
||||||
|
Additionally, we need to ensure feature parity (not all providers have e.g. wishlist) we will also have to build a feature API to disable/enable features in the UI.
|
||||||
|
|
||||||
|
People actively working on this project: @okbel & @lfades.
|
||||||
|
|
||||||
## Troubleshoot
|
## Troubleshoot
|
||||||
|
|
||||||
@ -80,10 +90,10 @@ Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
|||||||
6. Add proper store values to `.env.local`.
|
6. Add proper store values to `.env.local`.
|
||||||
7. Run `yarn dev` to build and watch for code changes
|
7. Run `yarn dev` to build and watch for code changes
|
||||||
8. The development branch is `development` (this is the branch pull requests should be made against).
|
8. The development branch is `development` (this is the branch pull requests should be made against).
|
||||||
On a release, the relevant parts of the changes in the `staging` branch are rebased into `master`.
|
On a release, `develop` branch is rebased into `master`.
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
- **Next.js Commerce** should have a completely data **agnostic** UI
|
- **Next.js Commerce** should have a completely data **agnostic** UI
|
||||||
- **Aware of schema**: should ship with the right data schemas and types.
|
- **Aware of schema**: should ship with the right data schemas and types.
|
||||||
- All providers should return the right data types and schemas to blend correctly with Next.js Commerce.
|
- All providers should return the right data types and schemas to blend correctly with Next.js Commerce.
|
||||||
@ -94,6 +104,8 @@ There is a `framework` folder in the root folder that will contain multiple ecom
|
|||||||
Additionally, we need to ensure feature parity (not all providers have e.g. wishlist) we will also have to build a feature API to disable/enable features in the UI.
|
Additionally, we need to ensure feature parity (not all providers have e.g. wishlist) we will also have to build a feature API to disable/enable features in the UI.
|
||||||
|
|
||||||
People actively working on this project: @okbel & @lfades.
|
People actively working on this project: @okbel & @lfades.
|
||||||
|
=======
|
||||||
|
>>>>>>> master
|
||||||
|
|
||||||
## Framework
|
## Framework
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ const CartSidebarView: FC = () => {
|
|||||||
My Cart
|
My Cart
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-t border-accents-3">
|
<ul className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-t border-accents-3">
|
||||||
{items.map((item) => (
|
{items.map((item: any) => (
|
||||||
<CartItem
|
<CartItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
|
@ -102,6 +102,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
|||||||
<div className="flex items-center text-primary">
|
<div className="flex items-center text-primary">
|
||||||
<span className="text-primary">Crafted by</span>
|
<span className="text-primary">Crafted by</span>
|
||||||
<a
|
<a
|
||||||
|
rel="noopener"
|
||||||
href="https://vercel.com"
|
href="https://vercel.com"
|
||||||
aria-label="Vercel.com Link"
|
aria-label="Vercel.com Link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -51,6 +51,8 @@ const I18nWidget: FC = () => {
|
|||||||
>
|
>
|
||||||
<button className={s.button} aria-label="Language selector">
|
<button className={s.button} aria-label="Language selector">
|
||||||
<img
|
<img
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
className="block mr-2 w-5"
|
className="block mr-2 w-5"
|
||||||
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
|
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
|
||||||
alt={LOCALES_MAP[currentLocale].img.alt}
|
alt={LOCALES_MAP[currentLocale].img.alt}
|
||||||
|
@ -9,9 +9,11 @@ import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
|
|||||||
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
|
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
|
||||||
import { CartSidebarView } from '@components/cart'
|
import { CartSidebarView } from '@components/cart'
|
||||||
|
|
||||||
|
import LoginView from '@components/auth/LoginView'
|
||||||
import { CommerceProvider } from '@framework'
|
import { CommerceProvider } from '@framework'
|
||||||
import type { Page } from '@framework/api/operations/get-all-pages'
|
import type { Page } from '@framework/api/operations/get-all-pages'
|
||||||
|
|
||||||
|
|
||||||
const Loading = () => (
|
const Loading = () => (
|
||||||
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
|
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
|
||||||
<LoadingDots />
|
<LoadingDots />
|
||||||
@ -22,10 +24,6 @@ const dynamicProps = {
|
|||||||
loading: () => <Loading />,
|
loading: () => <Loading />,
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoginView = dynamic(
|
|
||||||
() => import('@components/auth/LoginView'),
|
|
||||||
dynamicProps
|
|
||||||
)
|
|
||||||
const SignUpView = dynamic(
|
const SignUpView = dynamic(
|
||||||
() => import('@components/auth/SignUpView'),
|
() => import('@components/auth/SignUpView'),
|
||||||
dynamicProps
|
dynamicProps
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
@apply bg-primary p-12 border border-accents-2;
|
@apply bg-primary p-12 border border-accents-2 relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal:focus {
|
.modal:focus {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC, useRef, useEffect } from 'react'
|
import { FC, useRef, useEffect, useCallback } from 'react'
|
||||||
import Portal from '@reach/portal'
|
import Portal from '@reach/portal'
|
||||||
import s from './Modal.module.css'
|
import s from './Modal.module.css'
|
||||||
import { Cross } from '@components/icons'
|
import { Cross } from '@components/icons'
|
||||||
@ -7,45 +7,55 @@ import {
|
|||||||
enableBodyScroll,
|
enableBodyScroll,
|
||||||
clearAllBodyScrollLocks,
|
clearAllBodyScrollLocks,
|
||||||
} from 'body-scroll-lock'
|
} from 'body-scroll-lock'
|
||||||
|
import FocusTrap from '@lib/focus-trap'
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
children?: any
|
children?: any
|
||||||
open?: boolean
|
open?: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onEnter?: () => void | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: FC<Props> = ({ children, open, onClose }) => {
|
const Modal: FC<Props> = ({ children, open, onClose, onEnter = null }) => {
|
||||||
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
const ref = useRef() as React.MutableRefObject<HTMLDivElement>
|
||||||
|
|
||||||
|
const handleKey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
return onClose()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
if (open) {
|
if (open) {
|
||||||
disableBodyScroll(ref.current)
|
disableBodyScroll(ref.current)
|
||||||
|
window.addEventListener('keydown', handleKey)
|
||||||
} else {
|
} else {
|
||||||
enableBodyScroll(ref.current)
|
enableBodyScroll(ref.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKey)
|
||||||
clearAllBodyScrollLocks()
|
clearAllBodyScrollLocks()
|
||||||
}
|
}
|
||||||
}, [open])
|
}, [open, handleKey])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
{open ? (
|
{open ? (
|
||||||
<div className={s.root} ref={ref}>
|
<div className={s.root}>
|
||||||
<div className={s.modal}>
|
<div className={s.modal} role="dialog" ref={ref}>
|
||||||
<div className="h-7 flex items-center justify-end w-full">
|
<button
|
||||||
<button
|
onClick={() => onClose()}
|
||||||
onClick={() => onClose()}
|
aria-label="Close panel"
|
||||||
aria-label="Close panel"
|
className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none absolute right-0 top-0 m-6"
|
||||||
className="hover:text-gray-500 transition ease-in-out duration-150 focus:outline-none"
|
>
|
||||||
>
|
<Cross className="h-6 w-6" />
|
||||||
<Cross className="h-6 w-6" />
|
</button>
|
||||||
</button>
|
<FocusTrap focusFirst>{children}</FocusTrap>
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Fallback to CMS Data
|
// Fallback to CMS Data
|
||||||
|
|
||||||
export const defatultPageProps = {
|
export const defaultPageProps = {
|
||||||
header: {
|
header: {
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
|
64
lib/focus-trap.tsx
Normal file
64
lib/focus-trap.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useEffect, RefObject } from 'react'
|
||||||
|
import { tabbable } from 'tabbable'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode | any
|
||||||
|
focusFirst?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FocusTrap({ children, focusFirst = false }: Props) {
|
||||||
|
const root: RefObject<any> = React.useRef()
|
||||||
|
const anchor: RefObject<any> = React.useRef(document.activeElement)
|
||||||
|
|
||||||
|
const returnFocus = () => {
|
||||||
|
// Returns focus to the last focused element prior to trap.
|
||||||
|
if (anchor) {
|
||||||
|
anchor.current.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trapFocus = () => {
|
||||||
|
// Focus the container element
|
||||||
|
if (root.current) {
|
||||||
|
root.current.focus()
|
||||||
|
if (focusFirst) {
|
||||||
|
selectFirstFocusableEl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectFirstFocusableEl = () => {
|
||||||
|
// Try to find focusable elements, if match then focus
|
||||||
|
// Up to 6 seconds of load time threshold
|
||||||
|
let match = false
|
||||||
|
let end = 60 // Try to find match at least n times
|
||||||
|
let i = 0
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
if (!match !== i > end) {
|
||||||
|
match = !!tabbable(root.current).length
|
||||||
|
if (match) {
|
||||||
|
// Attempt to focus the first el
|
||||||
|
tabbable(root.current)[0].focus()
|
||||||
|
}
|
||||||
|
i = i + 1
|
||||||
|
} else {
|
||||||
|
// Clear interval after n attempts
|
||||||
|
clearInterval(timer)
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(trapFocus, 20)
|
||||||
|
return () => {
|
||||||
|
returnFocus()
|
||||||
|
}
|
||||||
|
}, [root, children])
|
||||||
|
|
||||||
|
return React.createElement('div', {
|
||||||
|
ref: root,
|
||||||
|
children,
|
||||||
|
className: 'outline-none focus-trap',
|
||||||
|
tabIndex: -1,
|
||||||
|
})
|
||||||
|
}
|
@ -3,14 +3,14 @@ import type {
|
|||||||
GetStaticPropsContext,
|
GetStaticPropsContext,
|
||||||
InferGetStaticPropsType,
|
InferGetStaticPropsType,
|
||||||
} from 'next'
|
} from 'next'
|
||||||
|
import { Text } from '@components/ui'
|
||||||
|
import { Layout } from '@components/common'
|
||||||
import getSlug from '@lib/get-slug'
|
import getSlug from '@lib/get-slug'
|
||||||
import { missingLocaleInPages } from '@lib/usage-warns'
|
import { missingLocaleInPages } from '@lib/usage-warns'
|
||||||
import { Layout } from '@components/common'
|
|
||||||
import { Text } from '@components/ui'
|
|
||||||
import { getConfig } from '@framework/api'
|
import { getConfig } from '@framework/api'
|
||||||
import getPage from '@framework/api/operations/get-page'
|
import getPage from '@framework/api/operations/get-page'
|
||||||
import getAllPages from '@framework/api/operations/get-all-pages'
|
import getAllPages from '@framework/api/operations/get-all-pages'
|
||||||
import { defatultPageProps } from '@lib/defaults'
|
import { defaultPageProps } from '@lib/defaults'
|
||||||
|
|
||||||
export async function getStaticProps({
|
export async function getStaticProps({
|
||||||
preview,
|
preview,
|
||||||
@ -34,7 +34,7 @@ export async function getStaticProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: { ...defatultPageProps, pages, page },
|
props: { ...defaultPageProps, pages, page },
|
||||||
revalidate: 60 * 60, // Every hour
|
revalidate: 60 * 60, // Every hour
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
import Document, {
|
import Document, { Head, Html, Main, NextScript } from 'next/document'
|
||||||
DocumentContext,
|
|
||||||
Head,
|
|
||||||
Html,
|
|
||||||
Main,
|
|
||||||
NextScript,
|
|
||||||
} from 'next/document'
|
|
||||||
|
|
||||||
class MyDocument extends Document {
|
class MyDocument extends Document {
|
||||||
render() {
|
render() {
|
||||||
|
@ -6,8 +6,12 @@ import { Layout } from '@components/common'
|
|||||||
import { Heart } from '@components/icons'
|
import { Heart } from '@components/icons'
|
||||||
import { Text, Container } from '@components/ui'
|
import { Text, Container } from '@components/ui'
|
||||||
import { WishlistCard } from '@components/wishlist'
|
import { WishlistCard } from '@components/wishlist'
|
||||||
|
<<<<<<< HEAD
|
||||||
import { defatultPageProps } from '@lib/defaults'
|
import { defatultPageProps } from '@lib/defaults'
|
||||||
import { useCustomer } from '@framework/customer'
|
import { useCustomer } from '@framework/customer'
|
||||||
|
=======
|
||||||
|
import { defaultPageProps } from '@lib/defaults'
|
||||||
|
>>>>>>> master
|
||||||
|
|
||||||
export async function getStaticProps({
|
export async function getStaticProps({
|
||||||
preview,
|
preview,
|
||||||
@ -16,7 +20,7 @@ export async function getStaticProps({
|
|||||||
const config = getConfig({ locale })
|
const config = getConfig({ locale })
|
||||||
const { pages } = await getAllPages({ config, preview })
|
const { pages } = await getAllPages({ config, preview })
|
||||||
return {
|
return {
|
||||||
props: { ...defatultPageProps, pages },
|
props: { ...defaultPageProps, pages },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,11 @@ module.exports = {
|
|||||||
'./pages/**/*.{js,ts,jsx,tsx}',
|
'./pages/**/*.{js,ts,jsx,tsx}',
|
||||||
'./components/**/*.{js,ts,jsx,tsx}',
|
'./components/**/*.{js,ts,jsx,tsx}',
|
||||||
],
|
],
|
||||||
|
options: {
|
||||||
|
safelist: {
|
||||||
|
standard: ['outline-none'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user