This commit is contained in:
Bel Curcio 2021-05-25 15:16:17 -03:00
parent eb44455cde
commit 4b06e7c062
438 changed files with 40493 additions and 15387 deletions

23
.editorconfig Normal file
View File

@ -0,0 +1,23 @@
root = true
[*]
indent_style = space
indent_size = 2
tab_width = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.js]
quote_type = single
[{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}]
curly_bracket_next_line = false
spaces_around_operators = true
spaces_around_brackets = outside
# close enough to 1TB
indent_brace_style = K&R

View File

@ -1,7 +0,0 @@
BIGCOMMERCE_STOREFRONT_API_URL=https://your-site.mybigcommerce.com/graphql
BIGCOMMERCE_STOREFRONT_API_TOKEN=
BIGCOMMERCE_STORE_API_URL=https://api.bigcommerce.com/stores/xxxxxxxxxxx
BIGCOMMERCE_STORE_API_CLIENT_ID=
BIGCOMMERCE_STORE_API_SECRET=
BIGCOMMERCE_STORE_API_TOKEN=
BIGCOMMERCE_TOKEN_SECRET="this-is-a-secret-value-with-at-least-32-characters"

12
.env.template Normal file
View File

@ -0,0 +1,12 @@
# Available providers: bigcommerce, shopify
COMMERCE_PROVIDER=bigcommerce
BIGCOMMERCE_STOREFRONT_API_URL=
BIGCOMMERCE_STOREFRONT_API_TOKEN=
BIGCOMMERCE_STORE_API_URL=
BIGCOMMERCE_STORE_API_TOKEN=
BIGCOMMERCE_STORE_API_CLIENT_ID=
BIGCOMMERCE_CHANNEL_ID=
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
# local env files # local env files
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
.next
public

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false
}

View File

@ -1 +0,0 @@
lib

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode"]
}

124
README.md
View File

@ -1 +1,123 @@
# e-comm-example [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fcommerce&project-name=commerce&repo-name=commerce&demo-title=Next.js%20Commerce&demo-description=An%20all-in-one%20starter%20kit%20for%20high-performance%20e-commerce%20sites.&demo-url=https%3A%2F%2Fdemo.vercel.store&demo-image=https%3A%2F%2Fbigcommerce-demo-asset-ksvtgfvnd.vercel.app%2Fbigcommerce.png&integration-ids=oac_MuWZiE4jtmQ2ejZQaQ7ncuDT)
# 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 customize their own store.
Start right now at [nextjs.org/commerce](https://nextjs.org/commerce)
Demo live at: [demo.vercel.store](https://demo.vercel.store/)
- Shopify Demo: https://shopify.demo.vercel.store/
- BigCommerce Demo: https://bigcommerce.demo.vercel.store/
## Features
- Performant by default
- SEO Ready
- Internationalization
- Responsive
- UI Components
- Theming
- Standardized Data Hooks
- Integrations - Integrate seamlessly with the most common ecommerce platforms.
- Dark Mode Support
## Integrations
Next.js Commerce integrates out-of-the-box with BigCommerce and Shopify. We plan to support all major ecommerce backends.
## Considerations
- `framework/commerce` contains all types, helpers and functions to be used as base to build a new **provider**.
- **Providers** live under `framework`'s root folder and they will extend Next.js Commerce types and functionality (`framework/commerce`).
- We have a **Features API** to ensure feature parity between the UI and the Provider. The UI should update accordingly and no extra code should be bundled. All extra configuration for features will live under `features` in `commerce.config.json` and if needed it can also be accessed programatically.
- Each **provider** should add its corresponding `next.config.js` and `commerce.config.json` adding specific data related to the provider. For example in case of BigCommerce, the images CDN and additional API routes.
- **Providers don't depend on anything that's specific to the application they're used in**. They only depend on `framework/commerce`, on their own framework folder and on some dependencies included in `package.json`
## Configuration
### How to change providers
Open `.env.local` and change the value of `COMMERCE_PROVIDER` to the provider you would like to use, then set the environment variables for that provider (use `.env.template` as the base).
### Features
Every provider defines the features that it supports under `framework/{provider}/commerce.config.json`
#### How to turn Features on and off
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box)
- Open `commerce.config.json`
- You'll see a config file like this:
```json
{
"features": {
"wishlist": false
}
}
```
- Turn wishlist on by setting wishlist to true.
- Run the app and the wishlist functionality should be back on.
### How to create a new provider
Follow our docs for [Adding a new Commerce Provider](framework/commerce/new-provider.md).
If you succeeded building a provider, submit a PR with a valid demo and we'll review it asap.
## 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. Duplicate `.env.template` and rename it to `.env.local`
6. Add proper store values to `.env.local`
7. Run `yarn dev` to build and watch for code changes
## 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)
People actively working on this project: @okbel & @lfades.
## Troubleshoot
<details>
<summary>I already own a BigCommerce store. What should I do?</summary>
<br>
First thing you do is: <b>set your environment variables</b>
<br>
<br>
.env.local
```sh
BIGCOMMERCE_STOREFRONT_API_URL=<>
BIGCOMMERCE_STOREFRONT_API_TOKEN=<>
BIGCOMMERCE_STORE_API_URL=<>
BIGCOMMERCE_STORE_API_TOKEN=<>
BIGCOMMERCE_STORE_API_CLIENT_ID=<>
BIGCOMMERCE_CHANNEL_ID=<>
```
If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials.
1. Install Vercel CLI: `npm i -g vercel`
2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link`
3. Download your environment variables: `vercel env pull .env.local`
Next, you're free to customize the starter. More updates coming soon. Stay tuned.
</details>
<details>
<summary>BigCommerce shows a Coming Soon page and requests a Preview Code</summary>
<br>
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
<br>
<br>
BigCommerce team has been notified and they plan to add more detailed about this subject.
</details>

130
assets/base.css Normal file
View File

@ -0,0 +1,130 @@
:root {
--primary: #ffffff;
--primary-2: #f1f3f5;
--secondary: #000000;
--secondary-2: #111;
--selection: var(--cyan);
--text-base: #000000;
--text-primary: #000000;
--text-secondary: white;
--hover: rgba(0, 0, 0, 0.075);
--hover-1: rgba(0, 0, 0, 0.15);
--hover-2: rgba(0, 0, 0, 0.25);
--cyan: #22b8cf;
--green: #37b679;
--red: #da3c3c;
--pink: #e64980;
--purple: #f81ce5;
--blue: #0070f3;
--violet: #5f3dc4;
--violet-light: #7048e8;
--accents-0: #f8f9fa;
--accents-1: #f1f3f5;
--accents-2: #e9ecef;
--accents-3: #dee2e6;
--accents-4: #ced4da;
--accents-5: #adb5bd;
--accents-6: #868e96;
--accents-7: #495057;
--accents-8: #343a40;
--accents-9: #212529;
--font-sans: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue',
'Helvetica', sans-serif;
}
[data-theme='dark'] {
--primary: #000000;
--primary-2: #111;
--secondary: #ffffff;
--secondary-2: #f1f3f5;
--hover: rgba(255, 255, 255, 0.075);
--hover-1: rgba(255, 255, 255, 0.15);
--hover-2: rgba(255, 255, 255, 0.25);
--selection: var(--purple);
--text-base: white;
--text-primary: white;
--text-secondary: black;
--accents-0: #212529;
--accents-1: #343a40;
--accents-2: #495057;
--accents-3: #868e96;
--accents-4: #adb5bd;
--accents-5: #ced4da;
--accents-6: #dee2e6;
--accents-7: #e9ecef;
--accents-8: #f1f3f5;
--accents-9: #f8f9fa;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
height: 100%;
box-sizing: border-box;
touch-action: manipulation;
font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,
body {
font-family: var(--font-sans);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--primary);
color: var(--text-primary);
}
body {
position: relative;
min-height: 100%;
margin: 0;
}
a {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.fadeIn {
-webkit-animation-name: fadeIn;
animation-name: fadeIn;
}
@-webkit-keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

12
assets/chrome-bug.css Normal file
View File

@ -0,0 +1,12 @@
/**
* Chrome has a bug with transitions on load since 2012!
*
* To prevent a "pop" of content, you have to disable all transitions until
* the page is done loading.
*
* https://lab.laukstein.com/bug/input
* https://twitter.com/timer150/status/1345217126680899584
*/
body.loading * {
transition: none !important;
}

3
assets/components.css Normal file
View File

@ -0,0 +1,3 @@
.fit {
min-height: calc(100vh - 88px);
}

View File

@ -1,628 +0,0 @@
/* latin */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 200;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 300;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 800;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 900;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587415301/fonts/2/inter-var-latin.woff2)
format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
/* these other fonts are only used and downloaded when a special chars is shown
in most cases, they are not downloaded at all */
/* latin-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-latin-ext.woff2)
format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 200;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-latin-ext.woff2)
format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-latin-ext.woff2)
format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-latin-ext.woff2)
format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-latin-ext.woff2)
format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-latin-ext.woff2)
format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-latin-ext.woff2)
format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-latin-ext.woff2)
format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-latin-ext.woff2)
format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,
U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* cyrillic */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-cyrillic.woff2)
format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 200;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-cyrillic.woff2)
format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-cyrillic.woff2)
format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-cyrillic.woff2)
format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-cyrillic.woff2)
format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-cyrillic.woff2)
format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-cyrillic.woff2)
format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-cyrillic.woff2)
format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-cyrillic.woff2)
format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* cyrillic-ext */
@font-face {
font-family: "Inter";
font-style: swap;
font-weight: 100;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587418127/fonts/2/inter-var-cyrillic-ext.woff2)
format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: swap;
font-weight: 200;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587418127/fonts/2/inter-var-cyrillic-ext.woff2)
format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: swap;
font-weight: 300;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587418127/fonts/2/inter-var-cyrillic-ext.woff2)
format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: swap;
font-weight: 400;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587418127/fonts/2/inter-var-cyrillic-ext.woff2)
format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: swap;
font-weight: 500;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587418127/fonts/2/inter-var-cyrillic-ext.woff2)
format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: swap;
font-weight: 600;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587418127/fonts/2/inter-var-cyrillic-ext.woff2)
format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: swap;
font-weight: 700;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587418127/fonts/2/inter-var-cyrillic-ext.woff2)
format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: swap;
font-weight: 800;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587418127/fonts/2/inter-var-cyrillic-ext.woff2)
format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: swap;
font-weight: 900;
font-display: block;
src: url(https://assets.vercel.com/raw/upload/v1587418127/fonts/2/inter-var-cyrillic-ext.woff2)
format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
/* greek */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-greek.woff2)
format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 200;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-greek.woff2)
format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-greek.woff2)
format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-greek.woff2)
format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-greek.woff2)
format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-greek.woff2)
format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-greek.woff2)
format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-greek.woff2)
format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418276/fonts/2/inter-var-greek.woff2)
format("woff2");
unicode-range: U+0370-03FF;
}
/* greek-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-greek-ext.woff2)
format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 200;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-greek-ext.woff2)
format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-greek-ext.woff2)
format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-greek-ext.woff2)
format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-greek-ext.woff2)
format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-greek-ext.woff2)
format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-greek-ext.woff2)
format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-greek-ext.woff2)
format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-greek-ext.woff2)
format("woff2");
unicode-range: U+1F00-1FFF;
}
/* vietnamese */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-vietnamese.woff2)
format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 200;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-vietnamese.woff2)
format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-vietnamese.woff2)
format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-vietnamese.woff2)
format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-vietnamese.woff2)
format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-vietnamese.woff2)
format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-vietnamese.woff2)
format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-vietnamese.woff2)
format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 900;
font-display: swap;
src: url(https://assets.vercel.com/raw/upload/v1587418275/fonts/2/inter-var-vietnamese.woff2)
format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}

View File

@ -1,194 +0,0 @@
@import "./font.css";
@tailwind base;
@tailwind components;
:root {
/* Spacing variables */
--geist-space: 4px;
--geist-space-2x: 8px;
--geist-space-4x: 16px;
--geist-space-8x: 32px;
--geist-space-16x: 64px;
--geist-space-24x: 96px;
--geist-space-32x: 128px;
--geist-space-48x: 192px;
--geist-space-64x: 256px;
--geist-space-small: 32px;
--geist-space-medium: 40px;
--geist-space-large: 48px;
--geist-space-gap: 24px;
--geist-space-gap-half: 12px;
--geist-space-gap-quarter: var(--geist-space-2x);
--geist-gap: var(--geist-space-gap);
--geist-gap-half: var(--geist-space-gap-half);
--geist-gap-quarter: var(--geist-space-gap-quarter);
--geist-gap-double: var(--geist-space-large);
/* Negative values */
--geist-space-negative: -4px;
--geist-space-2x-negative: -8px;
--geist-space-4x-negative: -16px;
--geist-space-8x-negative: -32px;
--geist-space-16x-negative: -64px;
--geist-space-24x-negative: -96px;
--geist-space-32x-negative: -128px;
--geist-space-48x-negative: -192px;
--geist-space-64x-negative: -256px;
--geist-space-small-negative: -32px;
--geist-space-medium-negative: -40px;
--geist-space-large-negative: -48px;
--geist-space-gap-negative: -24px;
--geist-space-gap-half-negative: -12px;
--geist-space-gap-quarter-negative: var(--geist-space-2x-negative);
--geist-gap-negative: var(--geist-space-gap-negative);
--geist-gap-half-negative: var(--geist-space-gap-half-negative);
--geist-gap-quarter-negative: var(--geist-space-gap-quarter-negative);
--geist-gap-double-negative: var(--geist-space-large-negative);
/* Page values */
--geist-page-margin: var(--geist-space-gap);
--geist-page-width: 1000px;
--geist-page-width-with-margin: 1048px; /* 1000px + (2 * page margin) */
/* Appearance */
--geist-radius: 5px;
--geist-marketing-radius: 8px;
/* Fonts */
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
--font-mono: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
/* Header */
--header-height: 64px;
--header-border-bottom: inset 0 -1px 0 0 rgba(0, 0, 0, 0.1);
--header-background: rgba(255, 255, 255, 0.8);
/* Form sizing */
--geist-form-large-font: 1rem;
--geist-form-large-line-height: 1.5rem;
--geist-form-large-height: var(--geist-space-large);
--geist-form-small-font: 0.875rem;
--geist-form-small-line-height: 0.875rem;
--geist-form-small-height: var(--geist-space-small);
--geist-form-font: 0.875rem;
--geist-form-line-height: 1.25rem;
--geist-form-height: var(--geist-space-medium);
--geist-success-lighter: #d3e5ff;
--geist-success-light: #3291ff;
--geist-success: #0070f3;
--geist-success-dark: #0761d1;
--geist-error-lighter: #f7d4d6;
--geist-error-light: #ff1a1a;
--geist-error: #ee0000;
--geist-error-dark: #c50000;
--geist-warning-lighter: #ffefcf;
--geist-warning-light: #f7b955;
--geist-warning: #f5a623;
--geist-warning-dark: #ab570a;
--geist-violet-lighter: #e3d7fc;
--geist-violet-light: #8a63d2;
--geist-violet: #7928ca;
--geist-violet-dark: #4c2889;
--geist-cyan-lighter: #aaffec;
--geist-cyan-light: #79ffe1;
--geist-cyan: #50e3c2;
--geist-cyan-dark: #29bc9b;
--geist-highlight-purple: #f81ce5;
--geist-highlight-magenta: #eb367f;
--geist-highlight-pink: #ff0080;
--geist-foreground: #000;
--geist-background: #fff;
--geist-selection: var(--geist-cyan-light);
--accents-1: #fafafa;
--accents-2: #eaeaea;
--accents-3: #999999;
--accents-4: #888888;
--accents-5: #666666;
--accents-6: #444444;
--accents-7: #333333;
--accents-8: #111111;
--geist-link-color: var(--geist-success);
--geist-marketing-gray: #fafbfc;
--geist-code: var(--geist-highlight-purple);
/* Secondary (Gray) */
--geist-secondary-lighter: var(--accents-2);
--geist-secondary-light: var(--accents-3);
--geist-secondary: var(--accents-5);
--geist-secondary-dark: var(--accents-7);
/* Shadows and other values */
--dropdown-box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.02);
--dropdown-triangle-stroke: #fff;
--scroller-start: rgba(255, 255, 255, 1);
--scroller-end: rgba(255, 255, 255, 0);
--shadow-smallest: 0px 4px 8px rgba(0, 0, 0, 0.12);
--shadow-small: 0 5px 10px rgba(0, 0, 0, 0.12);
--shadow-medium: 0 8px 30px rgba(0, 0, 0, 0.12);
--shadow-large: 0 30px 60px rgba(0, 0, 0, 0.12);
--shadow-hover: 0 30px 60px rgba(0, 0, 0, 0.12);
--shadow-sticky: 0 12px 10px -10px rgba(0, 0, 0, 0.12);
--portal-opacity: 0.25;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
height: 100%;
box-sizing: border-box;
touch-action: manipulation;
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,
body {
font-family: var(--font-sans);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--geist-background);
color: var(--geist-foreground);
scroll-padding-top: var(--header-height);
}
body {
position: relative;
min-height: 100%;
margin: 0;
}
a {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
@tailwind utilities;

7
assets/main.css Normal file
View File

@ -0,0 +1,7 @@
@tailwind base;
@import './base.css';
@tailwind components;
@import './components.css';
@tailwind utilities;

View File

@ -2,23 +2,26 @@
"schema": { "schema": {
"https://buybutton.store/graphql": { "https://buybutton.store/graphql": {
"headers": { "headers": {
"Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlYXQiOjE3NjcxMzkyMDAsInN1Yl90eXBlIjoyLCJ0b2tlbl90eXBlIjoxLCJjb3JzIjpbImh0dHBzOi8vZGV2ZWxvcGVyLmJpZ2NvbW1lcmNlLmNvbSJdLCJjaWQiOjEsImlhdCI6MTU3NjI1MzgyNCwic3ViIjoiM3dtZThrcWtrNjQwNzZueWljMGkzamk0NG5wajQ2byIsInNpZCI6OTk5MzMxNzg0LCJpc3MiOiJCQyJ9.Rqt6hNI2W-XSOzHl4pqtfhAOygwka6atCIaIZ_WAa9v3dOctnBlZpBV5wzd3ICCy4sTCOZ9mJwcFH5_CHmJpNQ" "Authorization": "Bearer xzy"
} }
} }
}, },
"documents": [ "documents": [
{ {
"./lib/bigcommerce/api/queries/**/*.ts": { "./framework/bigcommerce/api/**/*.ts": {
"noRequire": true "noRequire": true
} }
} }
], ],
"generates": { "generates": {
"./lib/bigcommerce/schema.d.ts": { "./framework/bigcommerce/schema.d.ts": {
"plugins": ["typescript", "typescript-operations"] "plugins": ["typescript", "typescript-operations"]
}, },
"./lib/bigcommerce/schema.graphql": { "./framework/bigcommerce/schema.graphql": {
"plugins": ["schema-ast"] "plugins": ["schema-ast"]
} }
},
"hooks": {
"afterAllFileWrite": ["prettier --write"]
} }
} }

6
commerce.config.json Normal file
View File

@ -0,0 +1,6 @@
{
"features": {
"wishlist": true,
"checkout": true
}
}

View File

@ -0,0 +1,78 @@
import { FC, useEffect, useState, useCallback } from 'react'
import { validate } from 'email-validator'
import { useUI } from '@components/ui/context'
import { Logo, Button, Input } from '@components/ui'
interface Props {}
const ForgotPassword: FC<Props> = () => {
// Form State
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const [dirty, setDirty] = useState(false)
const [disabled, setDisabled] = useState(false)
const { setModalView, closeModal } = useUI()
const handleResetPassword = async (e: React.SyntheticEvent<EventTarget>) => {
e.preventDefault()
if (!dirty && !disabled) {
setDirty(true)
handleValidation()
}
}
const handleValidation = useCallback(() => {
// Unable to send form unless fields are valid.
if (dirty) {
setDisabled(!validate(email))
}
}, [email, dirty])
useEffect(() => {
handleValidation()
}, [handleValidation])
return (
<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>
<div className="flex flex-col space-y-4">
{message && (
<div className="text-red border border-red p-3">{message}</div>
)}
<Input placeholder="Email" onChange={setEmail} type="email" />
<div className="pt-2 w-full flex flex-col">
<Button
variant="slim"
type="submit"
loading={loading}
disabled={disabled}
>
Recover Password
</Button>
</div>
<span className="pt-3 text-center text-sm">
<span className="text-accents-7">Do you have an account?</span>
{` `}
<a
className="text-accent-9 font-bold hover:underline cursor-pointer"
onClick={() => setModalView('LOGIN_VIEW')}
>
Log In
</a>
</span>
</div>
</form>
)
}
export default ForgotPassword

View File

@ -0,0 +1,104 @@
import { FC, useEffect, useState, useCallback } from 'react'
import { Logo, Button, Input } from '@components/ui'
import useLogin from '@framework/auth/use-login'
import { useUI } from '@components/ui/context'
import { validate } from 'email-validator'
interface Props {}
const LoginView: FC<Props> = () => {
// Form State
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const [dirty, setDirty] = useState(false)
const [disabled, setDisabled] = useState(false)
const { setModalView, closeModal } = useUI()
const login = useLogin()
const handleLogin = async (e: React.SyntheticEvent<EventTarget>) => {
e.preventDefault()
if (!dirty && !disabled) {
setDirty(true)
handleValidation()
}
try {
setLoading(true)
setMessage('')
await login({
email,
password,
})
setLoading(false)
closeModal()
} catch ({ errors }) {
setMessage(errors[0].message)
setLoading(false)
}
}
const handleValidation = useCallback(() => {
// Test for Alphanumeric password
const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password)
// Unable to send form unless fields are valid.
if (dirty) {
setDisabled(!validate(email) || password.length < 7 || !validPassword)
}
}, [email, password, dirty])
useEffect(() => {
handleValidation()
}, [handleValidation])
return (
<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>
<div className="flex flex-col space-y-3">
{message && (
<div className="text-red border border-red p-3">
{message}. Did you {` `}
<a
className="text-accent-9 inline font-bold hover:underline cursor-pointer"
onClick={() => setModalView('FORGOT_VIEW')}
>
forgot your password?
</a>
</div>
)}
<Input type="email" placeholder="Email" onChange={setEmail} />
<Input type="password" placeholder="Password" onChange={setPassword} />
<Button
variant="slim"
type="submit"
loading={loading}
disabled={disabled}
>
Log In
</Button>
<div className="pt-1 text-center text-sm">
<span className="text-accents-7">Don't have an account?</span>
{` `}
<a
className="text-accent-9 font-bold hover:underline cursor-pointer"
onClick={() => setModalView('SIGNUP_VIEW')}
>
Sign Up
</a>
</div>
</div>
</form>
)
}
export default LoginView

View File

@ -0,0 +1,114 @@
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 '@framework/auth/use-signup'
interface Props {}
const SignUpView: FC<Props> = () => {
// Form State
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const [dirty, setDirty] = useState(false)
const [disabled, setDisabled] = useState(false)
const signup = useSignup()
const { setModalView, closeModal } = useUI()
const handleSignup = async (e: React.SyntheticEvent<EventTarget>) => {
e.preventDefault()
if (!dirty && !disabled) {
setDirty(true)
handleValidation()
}
try {
setLoading(true)
setMessage('')
await signup({
email,
firstName,
lastName,
password,
})
setLoading(false)
closeModal()
} catch ({ errors }) {
setMessage(errors[0].message)
setLoading(false)
}
}
const handleValidation = useCallback(() => {
// Test for Alphanumeric password
const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(password)
// Unable to send form unless fields are valid.
if (dirty) {
setDisabled(!validate(email) || password.length < 7 || !validPassword)
}
}, [email, password, dirty])
useEffect(() => {
handleValidation()
}, [handleValidation])
return (
<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>
<div className="flex flex-col space-y-4">
{message && (
<div className="text-red border border-red p-3">{message}</div>
)}
<Input placeholder="First Name" onChange={setFirstName} />
<Input placeholder="Last Name" onChange={setLastName} />
<Input type="email" placeholder="Email" onChange={setEmail} />
<Input type="password" placeholder="Password" onChange={setPassword} />
<span className="text-accents-8">
<span className="inline-block align-middle ">
<Info width="15" height="15" />
</span>{' '}
<span className="leading-6 text-sm">
<strong>Info</strong>: Passwords must be longer than 7 chars and
include numbers.{' '}
</span>
</span>
<div className="pt-2 w-full flex flex-col">
<Button
variant="slim"
type="submit"
loading={loading}
disabled={disabled}
>
Sign Up
</Button>
</div>
<span className="pt-1 text-center text-sm">
<span className="text-accents-7">Do you have an account?</span>
{` `}
<a
className="text-accent-9 font-bold hover:underline cursor-pointer"
onClick={() => setModalView('LOGIN_VIEW')}
>
Log In
</a>
</span>
</div>
</form>
)
}
export default SignUpView

3
components/auth/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { default as LoginView } from './LoginView'
export { default as SignUpView } from './SignUpView'
export { default as ForgotPassword } from './ForgotPassword'

View File

@ -0,0 +1,43 @@
.root {
@apply flex flex-col py-4;
}
.root:first-child {
padding-top: 0;
}
.actions {
@apply flex p-1 border-accents-3 border items-center justify-center w-12 text-accents-7;
transition-property: border-color, background, color, transform, box-shadow;
transition-duration: 0.15s;
transition-timing-function: ease;
}
.actions:hover {
@apply bg-accents-1 border-accents-4 text-accents-9;
transition: border-color;
z-index: 10;
}
.actions:focus {
@apply bg-accents-2 outline-none;
}
.quantity {
appearance: textfield;
@apply w-8 border-accents-2 border mx-3 rounded text-center text-sm text-black;
}
.quantity::-webkit-outer-spin-button,
.quantity::-webkit-inner-spin-button {
@apply appearance-none m-0;
}
.productImage {
position: absolute;
transform: scale(1.9);
width: 100%;
height: 100%;
left: 30% !important;
top: 30% !important;
}

View File

@ -0,0 +1,187 @@
import { ChangeEvent, useEffect, useState } from 'react'
import cn from 'classnames'
import Image from 'next/image'
import Link from 'next/link'
import s from './CartItem.module.css'
import { Trash, Plus, Minus, Cross } from '@components/icons'
import { useUI } from '@components/ui/context'
import type { LineItem } from '@framework/types'
import usePrice from '@framework/product/use-price'
import useUpdateItem from '@framework/cart/use-update-item'
import useRemoveItem from '@framework/cart/use-remove-item'
type ItemOption = {
name: string
nameId: number
value: string
valueId: number
}
const CartItem = ({
item,
currencyCode,
...rest
}: {
item: LineItem
currencyCode: string
}) => {
const { closeSidebarIfPresent } = useUI()
const { price } = usePrice({
amount: item.variant.price * item.quantity,
baseAmount: item.variant.listPrice * item.quantity,
currencyCode,
})
const updateItem = useUpdateItem({ item })
const removeItem = useRemoveItem()
const [quantity, setQuantity] = useState(item.quantity)
const [removing, setRemoving] = useState(false)
const updateQuantity = async (val: number) => {
await updateItem({ quantity: val })
}
const handleQuantity = (e: ChangeEvent<HTMLInputElement>) => {
const val = Number(e.target.value)
if (Number.isInteger(val) && val >= 0) {
setQuantity(Number(e.target.value))
}
}
const handleBlur = () => {
const val = Number(quantity)
if (val !== item.quantity) {
updateQuantity(val)
}
}
const increaseQuantity = (n = 1) => {
const val = Number(quantity) + n
if (Number.isInteger(val) && val >= 0) {
setQuantity(val)
updateQuantity(val)
}
}
const handleRemove = async () => {
setRemoving(true)
try {
// If this action succeeds then there's no need to do `setRemoving(true)`
// because the component will be removed from the view
await removeItem(item)
} catch (error) {
setRemoving(false)
}
}
// TODO: Add a type for this
const options = (item as any).options
useEffect(() => {
// Reset the quantity state if the item quantity changes
if (item.quantity !== Number(quantity)) {
setQuantity(item.quantity)
}
}, [item.quantity])
return (
<li
className={cn(s.root, {
'opacity-50 pointer-events-none': removing,
})}
{...rest}
>
<div className="flex flex-row space-x-4 py-4">
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer">
<Link href={`/product/${item.path}`}>
<Image
onClick={() => closeSidebarIfPresent()}
className={s.productImage}
width={150}
height={150}
src={item.variant.image!.url}
alt={item.variant.image!.altText}
unoptimized
/>
</Link>
</div>
<div className="flex-1 flex flex-col text-base">
<Link href={`/product/${item.path}`}>
<span
className="font-medium cursor-pointer leading-6"
onClick={() => closeSidebarIfPresent()}
>
{item.name}
</span>
</Link>
{options && options.length > 0 ? (
<div className="">
{options.map((option: ItemOption, i: number) => (
<div
key={`${item.id}-${option.name}`}
className="text-sm font-semibold text-accents-7 inline-flex items-center justify-center"
>
{option.name}
{option.name === 'Color' ? (
<span
className="mx-2 rounded-full bg-transparent border w-5 h-5 p-1 text-accents-9 inline-flex items-center justify-center overflow-hidden"
style={{
backgroundColor: `${option.value}`,
}}
></span>
) : (
<span className="mx-2 rounded-full bg-transparent border h-5 p-1 text-accents-9 inline-flex items-center justify-center overflow-hidden">
{option.value}
</span>
)}
{i === options.length - 1 ? '' : <span className="mr-3" />}
</div>
))}
</div>
) : null}
</div>
<div className="flex flex-col justify-between space-y-2 text-sm">
<span>{price}</span>
</div>
</div>
<div className="flex flex-row h-9">
<button className={s.actions} onClick={handleRemove}>
<Cross width={20} height={20} />
</button>
<label className="w-full border-accents-3 border ml-2">
<input
type="number"
max={99}
min={0}
className="bg-transparent px-4 w-full h-full focus:outline-none"
value={quantity}
onChange={handleQuantity}
onBlur={handleBlur}
/>
</label>
<button
type="button"
onClick={() => increaseQuantity(-1)}
className={s.actions}
style={{ marginLeft: '-1px' }}
>
<Minus width={18} height={18} />
</button>
<button
type="button"
onClick={() => increaseQuantity(1)}
className={cn(s.actions)}
style={{ marginLeft: '-1px' }}
>
<Plus width={18} height={18} />
</button>
</div>
</li>
)
}
export default CartItem

View File

@ -0,0 +1 @@
export { default } from './CartItem'

View File

@ -0,0 +1,16 @@
.root {
@apply h-full flex flex-col relative w-full;
height: calc(100vh + 300px);
}
.root.empty {
@apply bg-secondary text-secondary;
}
.root.success {
@apply bg-green text-white;
}
.root.error {
@apply bg-red text-white;
}

View File

@ -0,0 +1,156 @@
import cn from 'classnames'
import Link from 'next/link'
import { FC } from 'react'
import s from './CartSidebarView.module.css'
import CartItem from '../CartItem'
import { Button } from '@components/ui'
import { UserNav } from '@components/common'
import { useUI } from '@components/ui/context'
import { Bag, Cross, Check } from '@components/icons'
import useCart from '@framework/cart/use-cart'
import usePrice from '@framework/product/use-price'
const CartSidebarView: FC = () => {
const { closeSidebar, setSidebarView } = useUI()
const { data, isLoading, isEmpty } = useCart()
const { price: subTotal } = usePrice(
data && {
amount: Number(data.subtotalPrice),
currencyCode: data.currency.code,
}
)
const { price: total } = usePrice(
data && {
amount: Number(data.totalPrice),
currencyCode: data.currency.code,
}
)
const handleClose = () => closeSidebar()
const goToCheckout = () => setSidebarView('CHECKOUT_VIEW')
const error = null
const success = null
return (
<div
className={cn(s.root, {
[s.empty]: error || success || isLoading || isEmpty,
})}
>
<header className="px-4 pt-6 pb-4 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="h-7 flex items-center">
<button
onClick={handleClose}
aria-label="Close panel"
className="hover:text-gray-500 transition ease-in-out duration-150 flex items-center focus:outline-none"
>
<Cross className="h-6 w-6" />
<span className="ml-2 text-accents-7 text-xs hover:text-gray-500">
Close
</span>
</button>
</div>
<div className="space-y-1">
<UserNav />
</div>
</div>
</header>
{isLoading || isEmpty ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-dashed border-primary rounded-full flex items-center justify-center w-16 h-16 p-12 bg-secondary text-secondary">
<Bag className="absolute" />
</span>
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
Your cart is empty
</h2>
<p className="text-accents-3 px-10 text-center pt-2">
Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
</p>
</div>
) : error ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
<Cross width={24} height={24} />
</span>
<h2 className="pt-6 text-xl font-light text-center">
We couldnt process the purchase. Please check your card information
and try again.
</h2>
</div>
) : success ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
<Check />
</span>
<h2 className="pt-6 text-xl font-light text-center">
Thank you for your order.
</h2>
</div>
) : (
<>
<div className="px-4 sm:px-6 flex-1">
<Link href="/cart">
<h2
className="pt-1 pb-2 text-sm uppercase font-semibold tracking-wide cursor-pointer inline-block"
onClick={handleClose}
>
My Cart
</h2>
</Link>
<ul className="py-4 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-accents-3">
{data!.lineItems.map((item: any) => (
<CartItem
key={item.id}
item={item}
currencyCode={data!.currency.code}
/>
))}
</ul>
</div>
<div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accents-0 shadow-outline-normal text-sm">
<ul className="pb-2">
<li className="flex justify-between py-1">
<span>Subtotal</span>
<span>{subTotal}</span>
</li>
<li className="flex justify-between py-1">
<span>Taxes</span>
<span>Calculated at checkout</span>
</li>
<li className="flex justify-between py-1">
<span>Shipping</span>
<span className="font-bold tracking-wide">FREE</span>
</li>
</ul>
<div className="flex justify-between border-t border-accents-3 py-3 font-bold mb-2">
<span>Total</span>
<span>{total}</span>
</div>
<div>
{process.env.COMMERCE_CHECKOUT_ENABLED ? (
<Button
Component="a"
width="100%"
variant="ghost"
onClick={goToCheckout}
>
Continue to Checkout
</Button>
) : (
<Button href="/checkout" Component="a" width="100%">
Proceed to Checkout
</Button>
)}
</div>
</div>
</>
)}
</div>
)
}
export default CartSidebarView

View File

@ -0,0 +1 @@
export { default } from './CartSidebarView'

2
components/cart/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { default as CartSidebarView } from './CartSidebarView'
export { default as CartItem } from './CartItem'

View File

@ -0,0 +1,16 @@
.root {
@apply h-full flex flex-col relative w-full;
height: calc(100vh + 300px);
}
.root.empty {
@apply bg-secondary text-secondary;
}
.root.success {
@apply bg-green text-white;
}
.root.error {
@apply bg-red text-white;
}

View File

@ -0,0 +1,185 @@
import cn from 'classnames'
import Link from 'next/link'
import { FC } from 'react'
import s from './CheckoutSidebarView.module.css'
import CartItem from '@components/cart/CartItem'
import { Button } from '@components/ui'
import { UserNav } from '@components/common'
import { useUI } from '@components/ui/context'
import {
Bag,
Cross,
Check,
MapPin,
CreditCard,
ChevronLeft,
} from '@components/icons'
import useCart from '@framework/cart/use-cart'
import usePrice from '@framework/product/use-price'
const CheckoutSidebarView: FC = () => {
const { closeSidebar, setSidebarView } = useUI()
const { data, isLoading, isEmpty } = useCart()
const { price: subTotal } = usePrice(
data && {
amount: Number(data.subtotalPrice),
currencyCode: data.currency.code,
}
)
const { price: total } = usePrice(
data && {
amount: Number(data.totalPrice),
currencyCode: data.currency.code,
}
)
const handleClose = () => closeSidebar()
const error = null
const success = null
return (
<div
className={cn(s.root, {
[s.empty]: error || success || isLoading || isEmpty,
})}
>
<header className="px-4 pt-6 pb-4 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="h-7 flex items-center">
<button
onClick={() => setSidebarView('CART_VIEW')}
aria-label="Close panel"
className="hover:text-gray-500 transition ease-in-out duration-150 flex items-center focus:outline-none"
>
<ChevronLeft className="h-6 w-6" />
<span className="ml-2 text-accents-7 text-xs hover:text-gray-500">
Back
</span>
</button>
</div>
<div className="space-y-1">
<UserNav />
</div>
</div>
</header>
{isLoading || isEmpty ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-dashed border-primary rounded-full flex items-center justify-center w-16 h-16 p-12 bg-secondary text-secondary">
<Bag className="absolute" />
</span>
<h2 className="pt-6 text-2xl font-bold tracking-wide text-center">
Your cart is empty
</h2>
<p className="text-accents-3 px-10 text-center pt-2">
Biscuit oat cake wafer icing ice cream tiramisu pudding cupcake.
</p>
</div>
) : error ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
<Cross width={24} height={24} />
</span>
<h2 className="pt-6 text-xl font-light text-center">
We couldnt process the purchase. Please check your card information
and try again.
</h2>
</div>
) : success ? (
<div className="flex-1 px-4 flex flex-col justify-center items-center">
<span className="border border-white rounded-full flex items-center justify-center w-16 h-16">
<Check />
</span>
<h2 className="pt-6 text-xl font-light text-center">
Thank you for your order.
</h2>
</div>
) : (
<>
<div className="px-4 sm:px-6 flex-1">
<Link href="/cart">
<h2
className="pt-1 pb-2 text-sm uppercase font-semibold tracking-wide cursor-pointer inline-block"
onClick={handleClose}
>
Checkout
</h2>
</Link>
{/* Payment Method */}
{/* Only available with checkout set to true - Meaning that the provider does offer checkout functionality. */}
<div
onClick={() => setSidebarView('PAYMENT_VIEW')}
className="rounded-md border border-accents-2 px-6 py-6 mb-4 text-center flex items-center justify-center cursor-pointer hover:border-accents-4"
>
<div className="mr-5">
<CreditCard />
</div>
<div className="text-sm text-center font-medium">
<span>Add Payment Method</span>
{/* <span>VISA #### #### #### 2345</span> */}
</div>
</div>
{/* Shipping Address */}
{/* Only available with checkout set to true - Meaning that the provider does offer checkout functionality. */}
<div
onClick={() => setSidebarView('SHIPPING_VIEW')}
className="rounded-md border border-accents-2 px-6 py-6 mb-4 text-center flex items-center justify-center cursor-pointer hover:border-accents-4"
>
<div className="mr-5">
<MapPin />
</div>
<div className="text-sm text-center font-medium">
<span>Add Shipping Address</span>
{/* <span>
1046 Kearny Street.<br/>
San Franssisco, California
</span> */}
</div>
</div>
<ul className="py-4 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-accents-3 border-accents-3">
{data!.lineItems.map((item: any) => (
<CartItem
key={item.id}
item={item}
currencyCode={data!.currency.code}
/>
))}
</ul>
</div>
<div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accents-0 shadow-outline-normal text-sm">
<ul className="pb-2">
<li className="flex justify-between py-1">
<span>Subtotal</span>
<span>{subTotal}</span>
</li>
<li className="flex justify-between py-1">
<span>Taxes</span>
<span>Calculated at checkout</span>
</li>
<li className="flex justify-between py-1">
<span>Shipping</span>
<span className="font-bold tracking-wide">FREE</span>
</li>
</ul>
<div className="flex justify-between border-t border-accents-3 py-3 font-bold mb-2">
<span>Total</span>
<span>{total}</span>
</div>
<div>
<Button href="/checkout" Component="a" width="100%">
Confirm Purchase
</Button>
</div>
</div>
</>
)}
</div>
)
}
export default CheckoutSidebarView

View File

@ -0,0 +1 @@
export { default } from './CheckoutSidebarView'

View File

@ -0,0 +1,40 @@
import { FC } from 'react'
import s from './PaymentMethodView.module.css'
import { ChevronLeft } from '@components/icons'
import { UserNav } from '@components/common'
import { useUI } from '@components/ui/context'
const PaymentMethodView: FC = () => {
const { setSidebarView } = useUI()
return (
<div className={s.root}>
<header className="px-4 pt-6 pb-4 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="h-7 flex items-center">
<button
onClick={() => setSidebarView('CHECKOUT_VIEW')}
aria-label="Close panel"
className="hover:text-gray-500 transition ease-in-out duration-150 flex items-center focus:outline-none"
>
<ChevronLeft className="h-6 w-6" />
<span className="ml-2 text-accents-7 text-xs hover:text-gray-500">
Back
</span>
</button>
</div>
<div className="space-y-1">
<UserNav />
</div>
</div>
</header>
<div className="px-4 sm:px-6 flex-1">
<h2 className="pt-1 pb-2 text-sm uppercase font-semibold tracking-wide cursor-pointer inline-block">
Payment Method
</h2>
</div>
</div>
)
}
export default PaymentMethodView

View File

@ -0,0 +1 @@
export { default } from './PaymentMethodView'

View File

@ -0,0 +1,12 @@
# Checkout
Checkout is only enabled in those providers that specify Custom Checkout to be enabled.
This feature enables:
- Shipping
- Payment
```
COMMERCE_CHECKOUT_ENABLED
```

View File

@ -0,0 +1,2 @@
.root {
}

View File

@ -0,0 +1,40 @@
import { FC } from 'react'
import s from './ShippingView.module.css'
import { ChevronLeft } from '@components/icons'
import { UserNav } from '@components/common'
import { useUI } from '@components/ui/context'
const PaymentMethodView: FC = () => {
const { setSidebarView } = useUI()
return (
<div className={s.root}>
<header className="px-4 pt-6 pb-4 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="h-7 flex items-center">
<button
onClick={() => setSidebarView('CHECKOUT_VIEW')}
aria-label="Close panel"
className="hover:text-gray-500 transition ease-in-out duration-150 flex items-center focus:outline-none"
>
<ChevronLeft className="h-6 w-6" />
<span className="ml-2 text-accents-7 text-xs hover:text-gray-500">
Back
</span>
</button>
</div>
<div className="space-y-1">
<UserNav />
</div>
</div>
</header>
<div className="px-4 sm:px-6 flex-1">
<h2 className="pt-1 pb-2 text-sm uppercase font-semibold tracking-wide cursor-pointer inline-block">
Shipping
</h2>
</div>
</div>
)
}
export default PaymentMethodView

View File

@ -0,0 +1 @@
export { default } from './ShippingView'

View File

@ -0,0 +1,24 @@
import { FC, useRef, useEffect } from 'react'
import { useUserAvatar } from '@lib/hooks/useUserAvatar'
interface Props {
className?: string
children?: any
}
const Avatar: FC<Props> = ({}) => {
let ref = useRef() as React.MutableRefObject<HTMLInputElement>
let { userAvatar } = useUserAvatar()
return (
<div
ref={ref}
style={{ backgroundImage: userAvatar }}
className="inline-block h-8 w-8 rounded-full border-2 border-primary hover:border-secondary focus:border-secondary transition linear-out duration-150"
>
{/* Add an image - We're generating a gradient as placeholder <img></img> */}
</div>
)
}
export default Avatar

View File

@ -0,0 +1 @@
export { default } from './Avatar'

View File

@ -0,0 +1,7 @@
.root {
@apply text-center p-6 bg-primary text-sm flex-row justify-center items-center font-medium fixed bottom-0 w-full z-30 transition-all duration-300 ease-out;
@screen md {
@apply flex text-left;
}
}

View File

@ -0,0 +1,39 @@
import cn from 'classnames'
import s from './FeatureBar.module.css'
interface FeatureBarProps {
className?: string
title: string
description?: string
hide?: boolean
action?: React.ReactNode
}
const FeatureBar: React.FC<FeatureBarProps> = ({
title,
description,
className,
action,
hide,
}) => {
const rootClassName = cn(
s.root,
{
transform: true,
'translate-y-0 opacity-100': !hide,
'translate-y-full opacity-0': hide,
},
className
)
return (
<div className={rootClassName}>
<span className="block md:inline">{title}</span>
<span className="block mb-6 md:inline md:mb-0 md:ml-2">
{description}
</span>
{action && action}
</div>
)
}
export default FeatureBar

View File

@ -0,0 +1 @@
export { default } from './FeatureBar'

View File

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

View File

@ -0,0 +1,159 @@
import { FC } from 'react'
import cn from 'classnames'
import Link from 'next/link'
import { useRouter } from 'next/router'
import type { Page } from '@framework/common/get-all-pages'
import getSlug from '@lib/get-slug'
import { Github, Vercel } from '@components/icons'
import { Logo, Container } from '@components/ui'
import { I18nWidget } from '@components/common'
import s from './Footer.module.css'
interface Props {
className?: string
children?: any
pages?: Page[]
}
const LEGAL_PAGES = ['terms-of-use', 'shipping-returns', 'privacy-policy']
const Footer: FC<Props> = ({ className, pages }) => {
const { sitePages, legalPages } = usePages(pages)
const rootClassName = cn(className)
return (
<footer className={rootClassName}>
<Container>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-b border-accents-2 py-12 text-primary bg-primary transition-colors duration-150">
<div className="col-span-1 lg:col-span-2">
<Link href="/">
<a className="flex flex-initial items-center font-bold md:mr-24">
<span className="rounded-full border border-gray-700 mr-2">
<Logo />
</span>
<span>ACME</span>
</a>
</Link>
</div>
<div className="col-span-1 lg:col-span-2">
<ul className="flex flex-initial flex-col md:flex-1">
<li className="py-3 md:py-0 md:pb-4">
<Link href="/">
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
Home
</a>
</Link>
</li>
<li className="py-3 md:py-0 md:pb-4">
<Link href="/">
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
Careers
</a>
</Link>
</li>
<li className="py-3 md:py-0 md:pb-4">
<Link href="/blog">
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
Blog
</a>
</Link>
</li>
{sitePages.map((page) => (
<li key={page.url} className="py-3 md:py-0 md:pb-4">
<Link href={page.url!}>
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
{page.name}
</a>
</Link>
</li>
))}
</ul>
</div>
<div className="col-span-1 lg:col-span-2">
<ul className="flex flex-initial flex-col md:flex-1">
{legalPages.map((page) => (
<li key={page.url} className="py-3 md:py-0 md:pb-4">
<Link href={page.url!}>
<a className="text-primary hover:text-accents-6 transition ease-in-out duration-150">
{page.name}
</a>
</Link>
</li>
))}
</ul>
</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">
<a
aria-label="Github Repository"
href="https://github.com/vercel/commerce"
className={s.link}
>
<Github />
</a>
<I18nWidget />
</div>
</div>
</div>
<div className="py-12 flex flex-col md:flex-row justify-between items-center space-y-4">
<div>
<span>&copy; 2020 ACME, Inc. All rights reserved.</span>
</div>
<div className="flex items-center text-primary">
<span className="text-primary">Crafted by</span>
<a
rel="noopener"
href="https://vercel.com"
aria-label="Vercel.com Link"
target="_blank"
className="text-primary"
>
<Vercel
className="inline-block h-6 ml-4 text-primary"
alt="Vercel.com Logo"
/>
</a>
</div>
</div>
</Container>
</footer>
)
}
function usePages(pages?: Page[]) {
const { locale } = useRouter()
const sitePages: Page[] = []
const legalPages: Page[] = []
if (pages) {
pages.forEach((page) => {
const slug = page.url && getSlug(page.url)
if (!slug) return
if (locale && !slug.startsWith(`${locale}/`)) return
if (isLegalPage(slug, locale)) {
legalPages.push(page)
} else {
sitePages.push(page)
}
})
}
return {
sitePages: sitePages.sort(bySortOrder),
legalPages: legalPages.sort(bySortOrder),
}
}
const isLegalPage = (slug: string, locale?: string) =>
locale
? LEGAL_PAGES.some((p) => `${locale}/${p}` === slug)
: LEGAL_PAGES.includes(slug)
// Sort pages by the sort order assigned in the BC dashboard
function bySortOrder(a: Page, b: Page) {
return (a.sort_order ?? 0) - (b.sort_order ?? 0)
}
export default Footer

View File

@ -0,0 +1 @@
export { default } from './Footer'

View File

@ -0,0 +1,18 @@
import { FC } from 'react'
import NextHead from 'next/head'
import { DefaultSeo } from 'next-seo'
import config from '@config/seo.json'
const Head: FC = () => {
return (
<>
<DefaultSeo {...config} />
<NextHead>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="/site.webmanifest" key="site-manifest" />
</NextHead>
</>
)
}
export default Head

View File

@ -0,0 +1 @@
export { default } from './Head'

View File

@ -0,0 +1,23 @@
.root {
@apply py-12 flex flex-col w-full px-6;
@screen md {
@apply flex-row;
}
& .asideWrapper {
@apply pr-3 w-full relative;
@screen md {
@apply w-48;
}
}
& .aside {
@apply flex flex-row w-full justify-around mb-12;
@screen md {
@apply mb-0 block sticky top-32;
}
}
}

View File

@ -0,0 +1,73 @@
import { FC } from 'react'
import Link from 'next/link'
import type { Product } from '@commerce/types'
import { Grid } from '@components/ui'
import { ProductCard } from '@components/product'
import s from './HomeAllProductsGrid.module.css'
import { getCategoryPath, getDesignerPath } from '@lib/search'
interface Props {
categories?: any
brands?: any
products?: Product[]
}
const HomeAllProductsGrid: FC<Props> = ({
categories,
brands,
products = [],
}) => {
return (
<div className={s.root}>
<div className={s.asideWrapper}>
<div className={s.aside}>
<ul className="mb-10">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getCategoryPath('')}>
<a>All Categories</a>
</Link>
</li>
{categories.map((cat: any) => (
<li key={cat.path} className="py-1 text-accents-8 text-base">
<Link href={getCategoryPath(cat.path)}>
<a>{cat.name}</a>
</Link>
</li>
))}
</ul>
<ul className="">
<li className="py-1 text-base font-bold tracking-wide">
<Link href={getDesignerPath('')}>
<a>All Designers</a>
</Link>
</li>
{brands.flatMap(({ node }: any) => (
<li key={node.path} className="py-1 text-accents-8 text-base">
<Link href={getDesignerPath(node.path)}>
<a>{node.name}</a>
</Link>
</li>
))}
</ul>
</div>
</div>
<div className="flex-1">
<Grid layout="normal">
{products.map((product) => (
<ProductCard
key={product.path}
product={product}
variant="simple"
imgProps={{
width: 480,
height: 480,
}}
/>
))}
</Grid>
</div>
</div>
)
}
export default HomeAllProductsGrid

View File

@ -0,0 +1 @@
export { default } from './HomeAllProductsGrid'

View File

@ -0,0 +1,42 @@
.root {
@apply relative;
}
.button {
@apply h-10 px-2 rounded-md border border-accents-2 flex items-center justify-center;
}
.button:hover {
@apply border-accents-4 shadow-sm;
}
.button:focus {
@apply outline-none;
}
.dropdownMenu {
@apply fixed right-0 top-12 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
@screen lg {
@apply absolute border border-accents-1 shadow-lg w-56 h-auto;
}
}
.closeButton {
@screen md {
@apply hidden;
}
}
.item {
@apply flex cursor-pointer px-6 py-3 transition ease-in-out duration-150 text-primary leading-6 font-medium items-center;
text-transform: capitalize;
}
.item:hover {
@apply bg-accents-1;
}
.icon {
transform: rotate(180deg);
}

View File

@ -0,0 +1,101 @@
import cn from 'classnames'
import Link from 'next/link'
import { FC, useState } from 'react'
import { useRouter } from 'next/router'
import s from './I18nWidget.module.css'
import { Cross, ChevronUp } from '@components/icons'
import ClickOutside from '@lib/click-outside'
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 [display, setDisplay] = useState(false)
const {
locale,
locales,
defaultLocale = 'en-US',
asPath: currentPath,
} = useRouter()
const options = locales?.filter((val) => val !== locale)
const currentLocale = locale || defaultLocale
return (
<ClickOutside active={display} onClick={() => setDisplay(false)}>
<nav className={s.root}>
<div
className="flex items-center relative"
onClick={() => setDisplay(!display)}
>
<button className={s.button} aria-label="Language selector">
<img
width="20"
height="20"
className="block mr-2 w-5"
src={`/${LOCALES_MAP[currentLocale].img.filename}`}
alt={LOCALES_MAP[currentLocale].img.alt}
/>
{options && (
<span className="cursor-pointer">
<ChevronUp className={cn({ [s.icon]: display })} />
</span>
)}
</button>
</div>
<div className="absolute top-0 right-0">
{options?.length && display ? (
<div className={s.dropdownMenu}>
<div className="flex flex-row justify-end px-6">
<button
onClick={() => setDisplay(false)}
aria-label="Close panel"
className={s.closeButton}
>
<Cross className="h-6 w-6" />
</button>
</div>
<ul>
{options.map((locale) => (
<li key={locale}>
<Link href={currentPath} locale={locale}>
<a
className={cn(s.item)}
onClick={() => setDisplay(false)}
>
{LOCALES_MAP[locale].name}
</a>
</Link>
</li>
))}
</ul>
</div>
) : null}
</div>
</nav>
</ClickOutside>
)
}
export default I18nWidget

View File

@ -0,0 +1 @@
export { default } from './I18nWidget'

View File

@ -0,0 +1,4 @@
.root {
@apply h-full bg-primary mx-auto transition-colors duration-150;
max-width: 2460px;
}

View File

@ -0,0 +1,99 @@
import cn from 'classnames'
import dynamic from 'next/dynamic'
import s from './Layout.module.css'
import { useRouter } from 'next/router'
import React, { FC } from 'react'
import { useUI } from '@components/ui/context'
import { Navbar, Footer } from '@components/common'
import { useAcceptCookies } from '@lib/hooks/useAcceptCookies'
import { Sidebar, Button, Modal, LoadingDots } from '@components/ui'
import CartSidebarView from '@components/cart/CartSidebarView'
import CheckoutSidebarView from '@components/checkout/CheckoutSidebarView'
import PaymentMethodView from '@components/checkout/PaymentMethodView'
import ShippingView from '@components/checkout/ShippingView'
import LoginView from '@components/auth/LoginView'
import { CommerceProvider } from '@framework'
import type { Page } from '@framework/common/get-all-pages'
const Loading = () => (
<div className="w-80 h-80 flex items-center text-center justify-center p-3">
<LoadingDots />
</div>
)
const dynamicProps = {
loading: () => <Loading />,
}
const SignUpView = dynamic(
() => import('@components/auth/SignUpView'),
dynamicProps
)
const ForgotPassword = dynamic(
() => import('@components/auth/ForgotPassword'),
dynamicProps
)
const FeatureBar = dynamic(
() => import('@components/common/FeatureBar'),
dynamicProps
)
interface Props {
pageProps: {
pages?: Page[]
commerceFeatures: Record<string, boolean>
}
}
const Layout: FC<Props> = ({
children,
pageProps: { commerceFeatures, ...pageProps },
}) => {
const {
displaySidebar,
displayModal,
closeSidebar,
closeModal,
modalView,
sidebarView,
} = useUI()
const { acceptedCookies, onAcceptCookies } = useAcceptCookies()
const { locale = 'en-US' } = useRouter()
return (
<CommerceProvider locale={locale}>
<div className={cn(s.root)}>
<Navbar />
<main className="fit">{children}</main>
<Footer pages={pageProps.pages} />
<Modal open={displayModal} onClose={closeModal}>
{modalView === 'LOGIN_VIEW' && <LoginView />}
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
</Modal>
<Sidebar open={displaySidebar} onClose={closeSidebar}>
{sidebarView === 'CART_VIEW' && <CartSidebarView />}
{sidebarView === 'CHECKOUT_VIEW' && <CheckoutSidebarView />}
{sidebarView === 'PAYMENT_VIEW' && <PaymentMethodView />}
{sidebarView === 'SHIPPING_VIEW' && <ShippingView />}
</Sidebar>
<FeatureBar
title="This site uses cookies to improve your experience. By clicking, you agree to our Privacy Policy."
hide={acceptedCookies}
action={
<Button className="mx-5" onClick={() => onAcceptCookies()}>
Accept cookies
</Button>
}
/>
</div>
</CommerceProvider>
)
}
export default Layout

View File

@ -0,0 +1 @@
export { default } from './Layout'

View File

@ -0,0 +1,24 @@
.root {
@apply sticky top-0 bg-primary z-40 transition-all duration-150;
}
.link {
@apply inline-flex items-center text-primary leading-6 font-medium transition ease-in-out duration-75 cursor-pointer text-accents-6;
}
.link:hover {
@apply text-accents-9;
}
.link:focus {
@apply outline-none text-accents-8;
}
.logo {
@apply cursor-pointer rounded-full border transform duration-100 ease-in-out;
&:hover {
@apply shadow-md;
transform: scale(1.05);
}
}

View File

@ -0,0 +1,50 @@
import { FC } from 'react'
import Link from 'next/link'
import { Logo, Container } from '@components/ui'
import { Searchbar, UserNav } from '@components/common'
import NavbarRoot from './NavbarRoot'
import s from './Navbar.module.css'
const Navbar: FC = () => (
<NavbarRoot>
<Container>
<div className="relative flex flex-row justify-between py-4 align-center md:py-6">
<div className="flex items-center flex-1">
<Link href="/">
<a className={s.logo} aria-label="Logo">
<Logo />
</a>
</Link>
<nav className="hidden ml-6 space-x-4 lg:block">
<Link href="/search">
<a className={s.link}>All</a>
</Link>
<Link href="/search?q=clothes">
<a className={s.link}>Clothes</a>
</Link>
<Link href="/search?q=accessories">
<a className={s.link}>Accessories</a>
</Link>
<Link href="/search?q=shoes">
<a className={s.link}>Shoes</a>
</Link>
</nav>
</div>
<div className="justify-center flex-1 hidden lg:flex">
<Searchbar />
</div>
<div className="flex justify-end flex-1 space-x-8">
<UserNav />
</div>
</div>
<div className="flex pb-4 lg:px-6 lg:hidden">
<Searchbar id="mobile-search" />
</div>
</Container>
</NavbarRoot>
)
export default Navbar

View File

@ -0,0 +1,33 @@
import { FC, useState, useEffect } from 'react'
import throttle from 'lodash.throttle'
import cn from 'classnames'
import s from './Navbar.module.css'
const NavbarRoot: FC = ({ children }) => {
const [hasScrolled, setHasScrolled] = useState(false)
useEffect(() => {
const handleScroll = throttle(() => {
const offset = 0
const { scrollTop } = document.documentElement
const scrolled = scrollTop > offset
if (hasScrolled !== scrolled) {
setHasScrolled(scrolled)
}
}, 200)
document.addEventListener('scroll', handleScroll)
return () => {
document.removeEventListener('scroll', handleScroll)
}
}, [hasScrolled])
return (
<div className={cn(s.root, { 'shadow-magical': hasScrolled })}>
{children}
</div>
)
}
export default NavbarRoot

View File

@ -0,0 +1 @@
export { default } from './Navbar'

View File

@ -0,0 +1,19 @@
.input {
@apply bg-transparent px-3 py-2 appearance-none w-full transition duration-150 ease-in-out pr-10;
@screen sm {
min-width: 300px;
}
}
.input:focus {
@apply outline-none shadow-outline-normal;
}
.iconContainer {
@apply absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none;
}
.icon {
@apply h-5 w-5;
}

View File

@ -0,0 +1,66 @@
import { FC, useEffect, useMemo } from 'react'
import cn from 'classnames'
import s from './Searchbar.module.css'
import { useRouter } from 'next/router'
interface Props {
className?: string
id?: string
}
const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
const router = useRouter()
useEffect(() => {
router.prefetch('/search')
}, [])
return useMemo(
() => (
<div
className={cn(
'relative text-sm bg-accents-1 text-base w-full transition-colors duration-150',
className
)}
>
<label className="hidden" htmlFor={id}>
Search
</label>
<input
id={id}
className={s.input}
placeholder="Search for products..."
defaultValue={router.query.q}
onKeyUp={(e) => {
e.preventDefault()
if (e.key === 'Enter') {
const q = e.currentTarget.value
router.push(
{
pathname: `/search`,
query: q ? { q } : {},
},
undefined,
{ shallow: true }
)
}
}}
/>
<div className={s.iconContainer}>
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
/>
</svg>
</div>
</div>
),
[]
)
}
export default Searchbar

View File

@ -0,0 +1 @@
export { default } from './Searchbar'

View File

@ -0,0 +1,24 @@
.dropdownMenu {
@apply fixed right-0 mt-2 origin-top-right outline-none bg-primary z-40 w-full h-full;
@screen lg {
@apply absolute top-10 border border-accents-1 shadow-lg w-56 h-auto;
}
}
.link {
@apply text-primary flex cursor-pointer px-6 py-3 flex transition ease-in-out duration-150 leading-6 font-medium items-center;
text-transform: capitalize;
}
.link:hover {
@apply bg-accents-1;
}
.link.active {
@apply font-bold bg-accents-2;
}
.off {
@apply hidden;
}

View File

@ -0,0 +1,125 @@
import cn from 'classnames'
import Link from 'next/link'
import { FC, useRef, useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/router'
import s from './DropdownMenu.module.css'
import { Avatar } from '@components/common'
import { Moon, Sun } from '@components/icons'
import { useUI } from '@components/ui/context'
import ClickOutside from '@lib/click-outside'
import useLogout from '@framework/auth/use-logout'
import {
disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
} from 'body-scroll-lock'
interface DropdownMenuProps {
open?: boolean
}
const LINKS = [
{
name: 'My Orders',
href: '/orders',
},
{
name: 'My Profile',
href: '/profile',
},
{
name: 'My Cart',
href: '/cart',
},
]
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
const logout = useLogout()
const { pathname } = useRouter()
const { theme, setTheme } = useTheme()
const [display, setDisplay] = useState(false)
const { closeSidebarIfPresent } = useUI()
const ref = useRef() as React.MutableRefObject<HTMLUListElement>
useEffect(() => {
if (ref.current) {
if (display) {
disableBodyScroll(ref.current)
} else {
enableBodyScroll(ref.current)
}
}
return () => {
clearAllBodyScrollLocks()
}
}, [display])
return (
<ClickOutside active={display} onClick={() => setDisplay(false)}>
<div>
<button
className={s.avatarButton}
onClick={() => setDisplay(!display)}
aria-label="Menu"
>
<Avatar />
</button>
{display && (
<ul className={s.dropdownMenu} ref={ref}>
{LINKS.map(({ name, href }) => (
<li key={href}>
<div>
<Link href={href}>
<a
className={cn(s.link, {
[s.active]: pathname === href,
})}
onClick={() => {
setDisplay(false)
closeSidebarIfPresent()
}}
>
{name}
</a>
</Link>
</div>
</li>
))}
<li>
<a
className={cn(s.link, 'justify-between')}
onClick={() => {
theme === 'dark' ? setTheme('light') : setTheme('dark')
setDisplay(false)
}}
>
<div>
Theme: <strong>{theme}</strong>{' '}
</div>
<div className="ml-3">
{theme == 'dark' ? (
<Moon width={20} height={20} />
) : (
<Sun width="20" height={20} />
)}
</div>
</a>
</li>
<li>
<a
className={cn(s.link, 'border-t border-accents-2 mt-4')}
onClick={() => logout()}
>
Logout
</a>
</li>
</ul>
)}
</div>
</ClickOutside>
)
}
export default DropdownMenu

View File

@ -0,0 +1,40 @@
.root {
@apply relative;
}
.list {
@apply flex flex-row items-center justify-items-end h-full;
}
.item {
@apply mr-6 cursor-pointer relative transition ease-in-out duration-100 flex items-center outline-none text-primary;
&:hover {
@apply text-accents-6 transition scale-110 duration-100;
}
&:last-child {
@apply mr-0;
}
&:focus,
&:active {
@apply outline-none;
}
}
.bagCount {
@apply border border-accents-1 bg-secondary text-secondary absolute rounded-full right-3 top-3 flex items-center justify-center font-bold text-xs;
padding-left: 2.5px;
padding-right: 2.5px;
min-width: 1.25rem;
min-height: 1.25rem;
}
.avatarButton {
@apply inline-flex justify-center rounded-full;
}
.avatarButton:focus {
@apply outline-none;
}

View File

@ -0,0 +1,61 @@
import { FC } from 'react'
import Link from 'next/link'
import cn from 'classnames'
import type { LineItem } from '@framework/types'
import useCart from '@framework/cart/use-cart'
import useCustomer from '@framework/customer/use-customer'
import { Avatar } from '@components/common'
import { Heart, Bag } from '@components/icons'
import { useUI } from '@components/ui/context'
import DropdownMenu from './DropdownMenu'
import s from './UserNav.module.css'
interface Props {
className?: string
}
const countItem = (count: number, item: LineItem) => count + item.quantity
const UserNav: FC<Props> = ({ className }) => {
const { data } = useCart()
const { data: customer } = useCustomer()
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
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}>
<Bag />
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
</li>
{process.env.COMMERCE_WISHLIST_ENABLED && (
<li className={s.item}>
<Link href="/wishlist">
<a onClick={closeSidebarIfPresent} aria-label="Wishlist">
<Heart />
</a>
</Link>
</li>
)}
<li className={s.item}>
{customer ? (
<DropdownMenu />
) : (
<button
className={s.avatarButton}
aria-label="Menu"
onClick={() => openModal()}
>
<Avatar />
</button>
)}
</li>
</ul>
</div>
</nav>
)
}
export default UserNav

View File

@ -0,0 +1 @@
export { default } from './UserNav'

View File

@ -0,0 +1,9 @@
export { default as Avatar } from './Avatar'
export { default as FeatureBar } from './FeatureBar'
export { default as Footer } from './Footer'
export { default as Layout } from './Layout'
export { default as Navbar } from './Navbar'
export { default as Searchbar } from './Searchbar'
export { default as UserNav } from './UserNav'
export { default as Head } from './Head'
export { default as I18nWidget } from './I18nWidget'

View File

@ -1,23 +0,0 @@
import cn from "classnames";
import React, { FunctionComponent } from "react";
import s from "./Avatar.module.css";
interface Props {
className?: string;
children?: any;
}
const Avatar: FunctionComponent<Props> = ({ className }) => {
const rootClassName = cn(s.root, className);
return (
<div className={rootClassName}>
<img
className="inline-block h-8 w-8 rounded-full"
src="https://vercel.com/api/www/avatar/61182a9f6bda512b4d9263c9c8a60aabe0402f4c?s=204"
alt=""
></img>
</div>
);
};
export default Avatar;

View File

@ -1 +0,0 @@
export { default } from "./Avatar";

View File

@ -1,21 +0,0 @@
.root {
@apply py-4 px-6 bg-black text-white flex flex-row justify-center items-center;
}
.title {
@apply text-white font-medium;
}
.separator {
@apply mx-3 bg-white;
width: 1px;
height: 20px;
}
.separator:before {
content: "";
}
.description {
@apply text-white font-medium;
}

View File

@ -1,26 +0,0 @@
import cn from "classnames";
import { FunctionComponent } from "react";
import s from "./Featurebar.module.css";
interface Props {
className?: string;
title: string;
description: string;
}
const Featurebar: FunctionComponent<Props> = ({
title,
description,
className,
}) => {
const rootClassName = cn(s.root, className);
return (
<div className={rootClassName}>
<span className={s.title}>{title}</span>
<span className={s.separator} />
<span className={s.description}>{description}</span>
</div>
);
};
export default Featurebar;

View File

@ -1 +0,0 @@
export { default } from "./Featurebar";

View File

@ -1,7 +0,0 @@
.root {
@apply p-0;
}
.container {
@apply flex justify-between items-center flex-row px-4 py-5;
}

View File

@ -1,20 +0,0 @@
import cn from "classnames";
import React, { FunctionComponent } from "react";
import s from "./Footer.module.css";
import { Container } from "@components/ui";
interface Props {
className?: string;
children?: any;
}
const Footer: FunctionComponent<Props> = ({ className }) => {
const rootClassName = cn(s.root, className);
return (
<footer className={rootClassName}>
<Container className={s.container}></Container>
</footer>
);
};
export default Footer;

View File

@ -1 +0,0 @@
export { default } from "./Footer";

View File

@ -1,3 +0,0 @@
.root {
@apply h-full border-indigo-900;
}

View File

@ -1,26 +0,0 @@
import cn from "classnames";
import React, { FunctionComponent } from "react";
import s from "./Layout.module.css";
import { Navbar, Featurebar } from "@components/core";
import { Container } from "@components/ui";
interface Props {
className?: string;
children?: any;
}
const Layout: FunctionComponent<Props> = ({ className, children }) => {
const rootClassName = cn(s.root, className);
return (
<Container className={rootClassName}>
<Featurebar
title="Free Standard Shipping on orders over $99.99"
description="Due to COVID-19, some orders may experience processing and delivery delays."
/>
<Navbar />
<main className="h-screen">{children}</main>
</Container>
);
};
export default Layout;

View File

@ -1 +0,0 @@
export { default } from "./Layout";

View File

@ -1,3 +0,0 @@
.root {
@apply flex justify-between items-center flex-row px-6 h-20 relative;
}

View File

@ -1 +0,0 @@
export { default } from "./Navbar";

View File

@ -1,24 +0,0 @@
.root {
@apply px-4 flex items-center;
}
.container {
@apply relative rounded-lg flex flex-row text-sm items-center bg-accent-1;
}
.input {
@apply bg-transparent px-3 py-2 appearance-none w-full transition duration-150 ease-in-out rounded-lg text-accent-1 placeholder-accent-4;
min-width: 300px;
}
.input:focus {
@apply outline-none shadow-outline-gray;
}
.iconContainer {
@apply absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none;
}
.icon {
@apply h-5 w-5;
}

View File

@ -1,32 +0,0 @@
import cn from "classnames";
import React, { FunctionComponent } from "react";
import s from "./Searchbar.module.css";
interface Props {
className?: string;
children?: any;
}
const Searchbar: FunctionComponent<Props> = ({ className }) => {
const rootClassName = cn(s.root, className);
return (
<div className={rootClassName}>
<div className="flex-1 flex justify-between px-2">
<div className={s.container}>
<input className={s.input} placeholder="Search for products..." />
<div className={s.iconContainer}>
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
/>
</svg>
</div>
</div>
</div>
</div>
);
};
export default Searchbar;

View File

@ -1 +0,0 @@
export { default } from "./Searchbar";

View File

@ -0,0 +1,27 @@
const ArrowLeft = ({ ...props }) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
{...props}
>
<path
d="M19 12H5"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 19L5 12L12 5"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default ArrowLeft

33
components/icons/Bag.tsx Normal file
View File

@ -0,0 +1,33 @@
const Bag = ({ ...props }) => {
return (
<svg
width="20"
height="22"
viewBox="0 0 20 22"
fill="none"
stroke="currentColor"
{...props}
>
<path
d="M4 1L1 5V19C1 19.5304 1.21071 20.0391 1.58579 20.4142C1.96086 20.7893 2.46957 21 3 21H17C17.5304 21 18.0391 20.7893 18.4142 20.4142C18.7893 20.0391 19 19.5304 19 19V5L16 1H4Z"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M1 5H19"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14 9C14 10.0609 13.5786 11.0783 12.8284 11.8284C12.0783 12.5786 11.0609 13 10 13C8.93913 13 7.92172 12.5786 7.17157 11.8284C6.42143 11.0783 6 10.0609 6 9"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default Bag

View File

@ -0,0 +1,21 @@
const Check = ({ ...props }) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
{...props}
>
<path
d="M20 6L9 17L4 12"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default Check

View File

@ -0,0 +1,20 @@
const ChevronUp = ({ ...props }) => {
return (
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
{...props}
>
<path d="M15 18l-6-6 6-6" />
</svg>
)
}
export default ChevronUp

View File

@ -0,0 +1,20 @@
const ChevronUp = ({ ...props }) => {
return (
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
{...props}
>
<path d="M18 15l-6-6-6 6" />
</svg>
)
}
export default ChevronUp

View File

@ -0,0 +1,20 @@
const CreditCard = ({ ...props }) => {
return (
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
>
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
<path d="M1 10h22" />
</svg>
)
}
export default CreditCard

View File

@ -0,0 +1,21 @@
const Cross = ({ ...props }) => {
return (
<svg
viewBox="0 0 24 24"
width="24"
height="24"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
shapeRendering="geometricPrecision"
{...props}
>
<path d="M18 6L6 18" />
<path d="M6 6l12 12" />
</svg>
)
}
export default Cross

View File

@ -0,0 +1,22 @@
const DoubleChevron = ({ ...props }) => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M16 8.90482L12 4L8 8.90482M8 15.0952L12 20L16 15.0952"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
export default DoubleChevron

Some files were not shown because too many files have changed in this diff Show More