4
0
forked from crowetic/commerce

Merge branch 'master' into storefront-data-hooks-imports

This commit is contained in:
B 2020-10-27 01:27:18 -03:00 committed by GitHub
commit f23a2381fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 221 additions and 108 deletions

View File

@ -1,7 +1,32 @@
# Next.js Commerce
The all-in-one starter kit for high-performance e-commerce sites. With a few clicks, Next.js developers can clone, deploy and fully own their own store.
Start right now at nextjs.org/commerce
## Features
Demo live at: [commerce-demo.vercel.app](https://commerce-demo.vercel.app/)
## Todo
This project is currently under development.
## Why
Ecommerce is one of the most important uses of the web and together we can raise the standard for ecommerce sites.
## Goals and Features
- Performant by default
- SEO Ready
- Internationalization
- Responsive
- UI Components
- Theming
- Standarized Data Hooks
- Integrations - Integrate seamlessly with the most common ecommerce platforms.
## Contribute
Our Commitment to Open Source can be found [here](https://vercel.com/oss).
1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
2. Create a new branch `git checkout -b MY_BRANCH_NAME`
3. Install yarn: `npm install -g yarn`
4. Install the dependencies: `yarn`
5. Run `yarn dev` to build and watch for code changes
7. 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`.

View File

@ -1,9 +1,7 @@
import { FC, useEffect, useState, useCallback } from 'react'
import { validate } from 'email-validator'
import { Info } from '@components/icons'
import { useUI } from '@components/ui/context'
import { Logo, Button, Input } from '@components/ui'
import useSignup from '@bigcommerce/storefront-data-hooks/use-signup'
interface Props {}
@ -15,27 +13,15 @@ const ForgotPassword: FC<Props> = () => {
const [dirty, setDirty] = useState(false)
const [disabled, setDisabled] = useState(false)
const signup = useSignup()
const { setModalView, closeModal } = useUI()
const handleSignup = async () => {
const handleResetPassword = async (e: React.SyntheticEvent<EventTarget>) => {
e.preventDefault()
if (!dirty && !disabled) {
setDirty(true)
handleValidation()
}
// try {
// setLoading(true)
// setMessage('')
// await signup({
// email,
// })
// setLoading(false)
// closeModal()
// } catch ({ errors }) {
// setMessage(errors[0].message)
// setLoading(false)
// }
}
const handleValidation = useCallback(() => {
@ -50,7 +36,10 @@ const ForgotPassword: FC<Props> = () => {
}, [handleValidation])
return (
<div className="w-80 flex flex-col justify-between p-3">
<form
onSubmit={handleResetPassword}
className="w-80 flex flex-col justify-between p-3"
>
<div className="flex justify-center pb-12 ">
<Logo width="64px" height="64px" />
</div>
@ -63,7 +52,7 @@ const ForgotPassword: FC<Props> = () => {
<div className="pt-2 w-full flex flex-col">
<Button
variant="slim"
onClick={() => handleSignup()}
type="submit"
loading={loading}
disabled={disabled}
>
@ -82,7 +71,7 @@ const ForgotPassword: FC<Props> = () => {
</a>
</span>
</div>
</div>
</form>
)
}

View File

@ -18,7 +18,9 @@ const LoginView: FC<Props> = () => {
const login = useLogin()
const handleLogin = async () => {
const handleLogin = async (e: React.SyntheticEvent<EventTarget>) => {
e.preventDefault()
if (!dirty && !disabled) {
setDirty(true)
handleValidation()
@ -54,7 +56,10 @@ const LoginView: FC<Props> = () => {
}, [handleValidation])
return (
<div className="w-80 flex flex-col justify-between p-3">
<form
onSubmit={handleLogin}
className="w-80 flex flex-col justify-between p-3"
>
<div className="flex justify-center pb-12 ">
<Logo width="64px" height="64px" />
</div>
@ -75,7 +80,7 @@ const LoginView: FC<Props> = () => {
<Button
variant="slim"
onClick={() => handleLogin()}
type="submit"
loading={loading}
disabled={disabled}
>
@ -92,7 +97,7 @@ const LoginView: FC<Props> = () => {
</a>
</div>
</div>
</div>
</form>
)
}

View File

@ -21,7 +21,9 @@ const SignUpView: FC<Props> = () => {
const signup = useSignup()
const { setModalView, closeModal } = useUI()
const handleSignup = async () => {
const handleSignup = async (e: React.SyntheticEvent<EventTarget>) => {
e.preventDefault()
if (!dirty && !disabled) {
setDirty(true)
handleValidation()
@ -59,7 +61,10 @@ const SignUpView: FC<Props> = () => {
}, [handleValidation])
return (
<div className="w-80 flex flex-col justify-between p-3">
<form
onSubmit={handleSignup}
className="w-80 flex flex-col justify-between p-3"
>
<div className="flex justify-center pb-12 ">
<Logo width="64px" height="64px" />
</div>
@ -83,7 +88,7 @@ const SignUpView: FC<Props> = () => {
<div className="pt-2 w-full flex flex-col">
<Button
variant="slim"
onClick={() => handleSignup()}
type="submit"
loading={loading}
disabled={disabled}
>
@ -102,7 +107,7 @@ const SignUpView: FC<Props> = () => {
</a>
</span>
</div>
</div>
</form>
)
}

View File

@ -2,7 +2,7 @@ import { FC } from 'react'
import cn from 'classnames'
import { UserNav } from '@components/core'
import { Button } from '@components/ui'
import { ArrowLeft, Bag, Cross, Check } from '@components/icons'
import { Bag, Cross, Check } from '@components/icons'
import { useUI } from '@components/ui/context'
import useCart from '@bigcommerce/storefront-data-hooks/cart/use-cart'
import usePrice from '@bigcommerce/storefront-data-hooks/use-price'
@ -47,11 +47,11 @@ const CartSidebarView: FC = () => {
aria-label="Close panel"
className="hover:text-gray-500 transition ease-in-out duration-150"
>
<ArrowLeft className="h-6 w-6" />
<Cross className="h-6 w-6" />
</button>
</div>
<div className="space-y-1">
<UserNav />
<UserNav className="" />
</div>
</div>
</header>

View File

@ -0,0 +1,9 @@
.link {
& > svg {
@apply transform duration-75 ease-linear;
}
&:hover > svg {
@apply scale-110;
}
}

View File

@ -7,7 +7,7 @@ import getSlug from '@utils/get-slug'
import { Github } from '@components/icons'
import { Logo, Container } from '@components/ui'
import { I18nWidget } from '@components/core'
import s from './Footer.module.css'
interface Props {
className?: string
children?: any
@ -83,7 +83,9 @@ const Footer: FC<Props> = ({ className, pages }) => {
</div>
<div className="col-span-1 lg:col-span-6 flex items-start lg:justify-end text-primary">
<div className="flex space-x-6 items-center h-10">
<Github />
<a href="https://github.com/vercel/commerce" className={s.link}>
<Github />
</a>
<I18nWidget />
</div>
</div>

View File

@ -6,21 +6,52 @@ import { Menu } from '@headlessui/react'
import { DoubleChevron } from '@components/icons'
import s from './I18nWidget.module.css'
const LOCALES_MAP: Record<string, string> = {
es: 'Español',
'en-US': 'English',
interface LOCALE_DATA {
name: string
img: {
filename: string
alt: string
}
}
const LOCALES_MAP: Record<string, LOCALE_DATA> = {
es: {
name: 'Español',
img: {
filename: 'flag-es-co.svg',
alt: 'Bandera Colombiana',
},
},
'en-US': {
name: 'English',
img: {
filename: 'flag-en-us.svg',
alt: 'US Flag',
},
},
}
const I18nWidget: FC = () => {
const { locale, locales, defaultLocale = 'en-US' } = useRouter()
const {
locale,
locales,
defaultLocale = 'en-US',
asPath: currentPath,
} = useRouter()
const options = locales?.filter((val) => val !== locale)
const currentLocale = locale || defaultLocale
return (
<nav className={s.root}>
<Menu>
<Menu.Button className={s.button} aria-label="Language selector">
<img className="mr-2" src="/flag-us.png" alt="US Flag" />
<span className="mr-2">{LOCALES_MAP[locale || defaultLocale]}</span>
<img
className="block mr-2 w-5"
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
alt={LOCALES_MAP[currentLocale].img.alt}
/>
<span className="mr-2">{LOCALES_MAP[currentLocale].name}</span>
{options && (
<span>
<DoubleChevron />
@ -33,9 +64,9 @@ const I18nWidget: FC = () => {
{options.map((locale) => (
<Menu.Item key={locale}>
{({ active }) => (
<Link href="/" locale={locale}>
<Link href={currentPath} locale={locale}>
<a className={cn(s.item, { [s.active]: active })}>
{LOCALES_MAP[locale]}
{LOCALES_MAP[locale].name}
</a>
</Link>
)}

View File

@ -4,6 +4,7 @@ import { useTheme } from 'next-themes'
import cn from 'classnames'
import s from './DropdownMenu.module.css'
import { Moon, Sun } from '@components/icons'
import { useUI } from '@components/ui/context'
import { Menu, Transition } from '@headlessui/react'
import useLogout from '@bigcommerce/storefront-data-hooks/use-logout'
import { useRouter } from 'next/router'
@ -32,6 +33,8 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
const logout = useLogout()
const { pathname } = useRouter()
const { closeSidebarIfPresent } = useUI()
return (
<Transition
show={open}
@ -51,6 +54,7 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
className={cn(s.link, {
[s.active]: pathname === href,
})}
onClick={closeSidebarIfPresent}
>
{name}
</a>

View File

@ -21,21 +21,18 @@ const UserNav: FC<Props> = ({ className, children, ...props }) => {
const { data } = useCart()
const { data: customer } = useCustomer()
const { openSidebar, closeSidebar, displaySidebar, openModal } = useUI()
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
const itemsCount = Object.values(data?.line_items ?? {}).reduce(countItems, 0)
return (
<nav className={cn(s.root, className)}>
<div className={s.mainContainer}>
<ul className={s.list}>
<li
className={s.item}
onClick={(e) => (displaySidebar ? closeSidebar() : openSidebar())}
>
<li className={s.item} onClick={toggleSidebar}>
<Bag />
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
</li>
<Link href="/wishlist">
<li className={s.item}>
<li className={s.item} onClick={closeSidebarIfPresent}>
<Heart />
</li>
</Link>

View File

@ -31,56 +31,56 @@ const ProductCard: FC<Props> = ({
currencyCode: p.prices?.price?.currencyCode!,
})
if (variant === 'slim') {
return (
<div className="relative overflow-hidden box-border">
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
{p.name}
</span>
</div>
<EnhancedImage
src={p.images.edges?.[0]?.node.urlOriginal!}
alt={p.name}
width={imgWidth}
height={imgHeight}
priority={priority}
quality="90"
/>
</div>
)
}
return (
<Link href={`/product${p.path}`}>
<a
className={cn(s.root, { [s.simple]: variant === 'simple' }, className)}
>
<div className={s.squareBg} />
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
<div className="absolute top-0 left-0 pr-16 max-w-full">
<h3 className={s.productTitle}>
<span>{p.name}</span>
</h3>
<span className={s.productPrice}>{price}</span>
{variant === 'slim' ? (
<div className="relative overflow-hidden box-border">
<div className="absolute inset-0 flex items-center justify-end mr-8 z-20">
<span className="bg-black text-white inline-block p-3 font-bold text-xl break-words">
{p.name}
</span>
</div>
<EnhancedImage
src={p.images.edges?.[0]?.node.urlOriginal!}
alt={p.name}
width={imgWidth}
height={imgHeight}
priority={priority}
quality="90"
/>
</div>
<WishlistButton
className={s.wishlistButton}
productId={p.entityId}
variant={p.variants.edges?.[0]!}
/>
</div>
<div className={s.imageContainer}>
<EnhancedImage
alt={p.name}
className={cn('w-full object-cover', s['product-image'])}
src={src}
width={imgWidth}
height={imgHeight}
priority={priority}
quality="90"
/>
</div>
) : (
<>
<div className={s.squareBg} />
<div className="flex flex-row justify-between box-border w-full z-20 absolute">
<div className="absolute top-0 left-0 pr-16 max-w-full">
<h3 className={s.productTitle}>
<span>{p.name}</span>
</h3>
<span className={s.productPrice}>{price}</span>
</div>
<WishlistButton
className={s.wishlistButton}
productId={p.entityId}
variant={p.variants.edges?.[0]!}
/>
</div>
<div className={s.imageContainer}>
<EnhancedImage
alt={p.name}
className={cn('w-full object-cover', s['product-image'])}
src={src}
width={imgWidth}
height={imgHeight}
priority={priority}
quality="90"
/>
</div>
</>
)}
</a>
</Link>
)

View File

@ -13,6 +13,7 @@ import { HTMLContent } from '@components/core'
import useAddItem from '@bigcommerce/storefront-data-hooks/cart/use-add-item'
import type { ProductNode } from '@bigcommerce/storefront-data-hooks/api/operations/get-product'
import { getProductOptions } from '../helpers'
import WishlistButton from '@components/wishlist/WishlistButton'
interface Props {
className?: string
@ -143,9 +144,11 @@ const ProductView: FC<Props> = ({ product, className }) => {
</div>
{/* TODO make it work */}
<div className={s.wishlistButton}>
<Heart />
</div>
<WishlistButton
className={s.wishlistButton}
productId={product.entityId}
variant={product.variants.edges?.[0]!}
/>
</div>
</Container>
)

View File

@ -131,6 +131,12 @@ export const UIProvider: FC = (props) => {
const openSidebar = () => dispatch({ type: 'OPEN_SIDEBAR' })
const closeSidebar = () => dispatch({ type: 'CLOSE_SIDEBAR' })
const toggleSidebar = () =>
state.displaySidebar
? dispatch({ type: 'CLOSE_SIDEBAR' })
: dispatch({ type: 'OPEN_SIDEBAR' })
const closeSidebarIfPresent = () =>
state.displaySidebar && dispatch({ type: 'CLOSE_SIDEBAR' })
const openDropdown = () => dispatch({ type: 'OPEN_DROPDOWN' })
const closeDropdown = () => dispatch({ type: 'CLOSE_DROPDOWN' })
@ -149,6 +155,8 @@ export const UIProvider: FC = (props) => {
...state,
openSidebar,
closeSidebar,
toggleSidebar,
closeSidebarIfPresent,
openDropdown,
closeDropdown,
openModal,

View File

@ -62,9 +62,10 @@ const WishlistButton: FC<Props> = ({
return (
<button
{...props}
aria-label="Add to wishlist"
className={cn({ 'opacity-50': loading }, className)}
onClick={handleWishlistChange}
{...props}
>
<Heart fill={itemInWishlist ? 'var(--pink)' : 'none'} />
</button>

View File

@ -3,21 +3,23 @@ import { Container, Text } from '@components/ui'
import useCustomer from '@bigcommerce/storefront-data-hooks/use-customer'
export default function Profile() {
const { data } = useCustomer()
console.log(data)
return (
<Container>
<Text variant="pageHeading">My Profile</Text>
{data && (
<div className="max-w-2xl flex flex-col space-y-5">
<div>
<Text variant="sectionHeading">Full Name</Text>
<span>
{data.firstName} {data.lastName}
</span>
</div>
<div>
<Text variant="sectionHeading">Email</Text>
<span>{data.email}</span>
<div className="grid lg:grid-cols-12">
<div className="lg:col-span-8 pr-4">
<div>
<Text variant="sectionHeading">Full Name</Text>
<span>
{data.firstName} {data.lastName}
</span>
</div>
<div className="mt-5">
<Text variant="sectionHeading">Email</Text>
<span>{data.email}</span>
</div>
</div>
</div>
)}

1
public/flag-en-us.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><circle cx="256" cy="256" r="256" fill="#f0f0f0"/><g fill="#d80027"><path d="M244.87 256H512c0-23.106-3.08-45.49-8.819-66.783H244.87V256zM244.87 122.435h229.556a257.35 257.35 0 00-59.07-66.783H244.87v66.783zM256 512c60.249 0 115.626-20.824 159.356-55.652H96.644C140.374 491.176 195.751 512 256 512zM37.574 389.565h436.852a254.474 254.474 0 0028.755-66.783H8.819a254.474 254.474 0 0028.755 66.783z"/></g><path d="M118.584 39.978h23.329l-21.7 15.765 8.289 25.509-21.699-15.765-21.699 15.765 7.16-22.037a257.407 257.407 0 00-49.652 55.337h7.475l-13.813 10.035a255.58 255.58 0 00-6.194 10.938l6.596 20.301-12.306-8.941a253.567 253.567 0 00-8.372 19.873l7.267 22.368h26.822l-21.7 15.765 8.289 25.509-21.699-15.765-12.998 9.444A258.468 258.468 0 000 256h256V0c-50.572 0-97.715 14.67-137.416 39.978zm9.918 190.422l-21.699-15.765L85.104 230.4l8.289-25.509-21.7-15.765h26.822l8.288-25.509 8.288 25.509h26.822l-21.7 15.765 8.289 25.509zm-8.289-100.083l8.289 25.509-21.699-15.765-21.699 15.765 8.289-25.509-21.7-15.765h26.822l8.288-25.509 8.288 25.509h26.822l-21.7 15.765zM220.328 230.4l-21.699-15.765L176.93 230.4l8.289-25.509-21.7-15.765h26.822l8.288-25.509 8.288 25.509h26.822l-21.7 15.765 8.289 25.509zm-8.289-100.083l8.289 25.509-21.699-15.765-21.699 15.765 8.289-25.509-21.7-15.765h26.822l8.288-25.509 8.288 25.509h26.822l-21.7 15.765zm0-74.574l8.289 25.509-21.699-15.765-21.699 15.765 8.289-25.509-21.7-15.765h26.822l8.288-25.509 8.288 25.509h26.822l-21.7 15.765z" fill="#0052b4"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

20
public/flag-es-ar.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

10
public/flag-es-co.svg Normal file
View File

@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.5144 6C19.5212 2.84625 16.0064 0.75 11.9999 0.75C7.99335 0.75 4.47848 2.84625 2.48535 6H2.4855C1.38787 7.737 0.75 9.79313 0.75 12C0.75 14.2069 1.38787 16.263 2.4855 18H2.48535C4.47848 21.1537 7.99335 23.25 11.9999 23.25C16.0064 23.25 19.5212 21.1537 21.5144 18H21.5145C22.6121 16.263 23.25 14.2069 23.25 12C23.25 9.79313 22.6121 7.737 21.5145 6H21.5144Z" fill="#B4D7EE"/>
</mask>
<g mask="url(#mask0)">
<rect x="-2" y="-1" width="27" height="14" fill="#FCD116"/>
<rect x="-2" y="13" width="27" height="6" fill="#003893"/>
<rect x="-2" y="19" width="27" height="6" fill="#CE1126"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 840 B

1
public/flag-es.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 256c0 31.314 5.633 61.31 15.923 89.043L256 367.304l240.077-22.261C506.367 317.31 512 287.314 512 256s-5.633-61.31-15.923-89.043L256 144.696 15.923 166.957C5.633 194.69 0 224.686 0 256z" fill="#ffda44"/><g fill="#d80027"><path d="M496.077 166.957C459.906 69.473 366.071 0 256 0S52.094 69.473 15.923 166.957h480.154zM15.923 345.043C52.094 442.527 145.929 512 256 512s203.906-69.473 240.077-166.957H15.923z"/></g></svg>

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B