mirror of
https://github.com/vercel/commerce.git
synced 2025-06-19 05:31:22 +00:00
Changes
This commit is contained in:
parent
eb44455cde
commit
4b06e7c062
23
.editorconfig
Normal file
23
.editorconfig
Normal 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
|
@ -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
12
.env.template
Normal 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
1
.gitignore
vendored
@ -25,6 +25,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.next
|
||||
public
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
@ -1 +0,0 @@
|
||||
lib
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["esbenp.prettier-vscode"]
|
||||
}
|
124
README.md
124
README.md
@ -1 +1,123 @@
|
||||
# e-comm-example
|
||||
[](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
130
assets/base.css
Normal 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
12
assets/chrome-bug.css
Normal 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
3
assets/components.css
Normal file
@ -0,0 +1,3 @@
|
||||
.fit {
|
||||
min-height: calc(100vh - 88px);
|
||||
}
|
628
assets/font.css
628
assets/font.css
@ -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;
|
||||
}
|
@ -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
7
assets/main.css
Normal file
@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@import './base.css';
|
||||
|
||||
@tailwind components;
|
||||
@import './components.css';
|
||||
|
||||
@tailwind utilities;
|
11
codegen.json
11
codegen.json
@ -2,23 +2,26 @@
|
||||
"schema": {
|
||||
"https://buybutton.store/graphql": {
|
||||
"headers": {
|
||||
"Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJlYXQiOjE3NjcxMzkyMDAsInN1Yl90eXBlIjoyLCJ0b2tlbl90eXBlIjoxLCJjb3JzIjpbImh0dHBzOi8vZGV2ZWxvcGVyLmJpZ2NvbW1lcmNlLmNvbSJdLCJjaWQiOjEsImlhdCI6MTU3NjI1MzgyNCwic3ViIjoiM3dtZThrcWtrNjQwNzZueWljMGkzamk0NG5wajQ2byIsInNpZCI6OTk5MzMxNzg0LCJpc3MiOiJCQyJ9.Rqt6hNI2W-XSOzHl4pqtfhAOygwka6atCIaIZ_WAa9v3dOctnBlZpBV5wzd3ICCy4sTCOZ9mJwcFH5_CHmJpNQ"
|
||||
"Authorization": "Bearer xzy"
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": [
|
||||
{
|
||||
"./lib/bigcommerce/api/queries/**/*.ts": {
|
||||
"./framework/bigcommerce/api/**/*.ts": {
|
||||
"noRequire": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"generates": {
|
||||
"./lib/bigcommerce/schema.d.ts": {
|
||||
"./framework/bigcommerce/schema.d.ts": {
|
||||
"plugins": ["typescript", "typescript-operations"]
|
||||
},
|
||||
"./lib/bigcommerce/schema.graphql": {
|
||||
"./framework/bigcommerce/schema.graphql": {
|
||||
"plugins": ["schema-ast"]
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"afterAllFileWrite": ["prettier --write"]
|
||||
}
|
||||
}
|
||||
|
6
commerce.config.json
Normal file
6
commerce.config.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"features": {
|
||||
"wishlist": true,
|
||||
"checkout": true
|
||||
}
|
||||
}
|
78
components/auth/ForgotPassword.tsx
Normal file
78
components/auth/ForgotPassword.tsx
Normal 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
|
104
components/auth/LoginView.tsx
Normal file
104
components/auth/LoginView.tsx
Normal 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
|
114
components/auth/SignUpView.tsx
Normal file
114
components/auth/SignUpView.tsx
Normal 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
3
components/auth/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as LoginView } from './LoginView'
|
||||
export { default as SignUpView } from './SignUpView'
|
||||
export { default as ForgotPassword } from './ForgotPassword'
|
43
components/cart/CartItem/CartItem.module.css
Normal file
43
components/cart/CartItem/CartItem.module.css
Normal 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;
|
||||
}
|
187
components/cart/CartItem/CartItem.tsx
Normal file
187
components/cart/CartItem/CartItem.tsx
Normal 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
|
1
components/cart/CartItem/index.ts
Normal file
1
components/cart/CartItem/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './CartItem'
|
16
components/cart/CartSidebarView/CartSidebarView.module.css
Normal file
16
components/cart/CartSidebarView/CartSidebarView.module.css
Normal 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;
|
||||
}
|
156
components/cart/CartSidebarView/CartSidebarView.tsx
Normal file
156
components/cart/CartSidebarView/CartSidebarView.tsx
Normal 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 couldn’t 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
|
1
components/cart/CartSidebarView/index.ts
Normal file
1
components/cart/CartSidebarView/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './CartSidebarView'
|
2
components/cart/index.ts
Normal file
2
components/cart/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as CartSidebarView } from './CartSidebarView'
|
||||
export { default as CartItem } from './CartItem'
|
@ -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;
|
||||
}
|
185
components/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx
Normal file
185
components/checkout/CheckoutSidebarView/CheckoutSidebarView.tsx
Normal 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 couldn’t 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
|
1
components/checkout/CheckoutSidebarView/index.ts
Normal file
1
components/checkout/CheckoutSidebarView/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './CheckoutSidebarView'
|
40
components/checkout/PaymentMethodView/PaymentMethodView.tsx
Normal file
40
components/checkout/PaymentMethodView/PaymentMethodView.tsx
Normal 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
|
1
components/checkout/PaymentMethodView/index.ts
Normal file
1
components/checkout/PaymentMethodView/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './PaymentMethodView'
|
12
components/checkout/README.md
Normal file
12
components/checkout/README.md
Normal 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
|
||||
```
|
2
components/checkout/ShippingView/ShippingView.module.css
Normal file
2
components/checkout/ShippingView/ShippingView.module.css
Normal file
@ -0,0 +1,2 @@
|
||||
.root {
|
||||
}
|
40
components/checkout/ShippingView/ShippingView.tsx
Normal file
40
components/checkout/ShippingView/ShippingView.tsx
Normal 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
|
1
components/checkout/ShippingView/index.ts
Normal file
1
components/checkout/ShippingView/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './ShippingView'
|
24
components/common/Avatar/Avatar.tsx
Normal file
24
components/common/Avatar/Avatar.tsx
Normal 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
|
1
components/common/Avatar/index.ts
Normal file
1
components/common/Avatar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Avatar'
|
7
components/common/FeatureBar/FeatureBar.module.css
Normal file
7
components/common/FeatureBar/FeatureBar.module.css
Normal 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;
|
||||
}
|
||||
}
|
39
components/common/FeatureBar/FeatureBar.tsx
Normal file
39
components/common/FeatureBar/FeatureBar.tsx
Normal 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
|
1
components/common/FeatureBar/index.ts
Normal file
1
components/common/FeatureBar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './FeatureBar'
|
9
components/common/Footer/Footer.module.css
Normal file
9
components/common/Footer/Footer.module.css
Normal file
@ -0,0 +1,9 @@
|
||||
.link {
|
||||
& > svg {
|
||||
@apply transform duration-75 ease-linear;
|
||||
}
|
||||
|
||||
&:hover > svg {
|
||||
@apply scale-110;
|
||||
}
|
||||
}
|
159
components/common/Footer/Footer.tsx
Normal file
159
components/common/Footer/Footer.tsx
Normal 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>© 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
|
1
components/common/Footer/index.ts
Normal file
1
components/common/Footer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Footer'
|
18
components/common/Head/Head.tsx
Normal file
18
components/common/Head/Head.tsx
Normal 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
|
1
components/common/Head/index.ts
Normal file
1
components/common/Head/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Head'
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
1
components/common/HomeAllProductsGrid/index.ts
Normal file
1
components/common/HomeAllProductsGrid/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './HomeAllProductsGrid'
|
42
components/common/I18nWidget/I18nWidget.module.css
Normal file
42
components/common/I18nWidget/I18nWidget.module.css
Normal 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);
|
||||
}
|
101
components/common/I18nWidget/I18nWidget.tsx
Normal file
101
components/common/I18nWidget/I18nWidget.tsx
Normal 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
|
1
components/common/I18nWidget/index.ts
Normal file
1
components/common/I18nWidget/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './I18nWidget'
|
4
components/common/Layout/Layout.module.css
Normal file
4
components/common/Layout/Layout.module.css
Normal file
@ -0,0 +1,4 @@
|
||||
.root {
|
||||
@apply h-full bg-primary mx-auto transition-colors duration-150;
|
||||
max-width: 2460px;
|
||||
}
|
99
components/common/Layout/Layout.tsx
Normal file
99
components/common/Layout/Layout.tsx
Normal 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
|
1
components/common/Layout/index.ts
Normal file
1
components/common/Layout/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Layout'
|
24
components/common/Navbar/Navbar.module.css
Normal file
24
components/common/Navbar/Navbar.module.css
Normal 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);
|
||||
}
|
||||
}
|
50
components/common/Navbar/Navbar.tsx
Normal file
50
components/common/Navbar/Navbar.tsx
Normal 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
|
33
components/common/Navbar/NavbarRoot.tsx
Normal file
33
components/common/Navbar/NavbarRoot.tsx
Normal 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
|
1
components/common/Navbar/index.ts
Normal file
1
components/common/Navbar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Navbar'
|
19
components/common/Searchbar/Searchbar.module.css
Normal file
19
components/common/Searchbar/Searchbar.module.css
Normal 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;
|
||||
}
|
66
components/common/Searchbar/Searchbar.tsx
Normal file
66
components/common/Searchbar/Searchbar.tsx
Normal 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
|
1
components/common/Searchbar/index.ts
Normal file
1
components/common/Searchbar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './Searchbar'
|
24
components/common/UserNav/DropdownMenu.module.css
Normal file
24
components/common/UserNav/DropdownMenu.module.css
Normal 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;
|
||||
}
|
125
components/common/UserNav/DropdownMenu.tsx
Normal file
125
components/common/UserNav/DropdownMenu.tsx
Normal 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
|
40
components/common/UserNav/UserNav.module.css
Normal file
40
components/common/UserNav/UserNav.module.css
Normal 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;
|
||||
}
|
61
components/common/UserNav/UserNav.tsx
Normal file
61
components/common/UserNav/UserNav.tsx
Normal 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
|
1
components/common/UserNav/index.ts
Normal file
1
components/common/UserNav/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './UserNav'
|
9
components/common/index.ts
Normal file
9
components/common/index.ts
Normal 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'
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default } from "./Avatar";
|
@ -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;
|
||||
}
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default } from "./Featurebar";
|
@ -1,7 +0,0 @@
|
||||
.root {
|
||||
@apply p-0;
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply flex justify-between items-center flex-row px-4 py-5;
|
||||
}
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default } from "./Footer";
|
@ -1,3 +0,0 @@
|
||||
.root {
|
||||
@apply h-full border-indigo-900;
|
||||
}
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default } from "./Layout";
|
@ -1,3 +0,0 @@
|
||||
.root {
|
||||
@apply flex justify-between items-center flex-row px-6 h-20 relative;
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { default } from "./Navbar";
|
@ -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;
|
||||
}
|
@ -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;
|
@ -1 +0,0 @@
|
||||
export { default } from "./Searchbar";
|
27
components/icons/ArrowLeft.tsx
Normal file
27
components/icons/ArrowLeft.tsx
Normal 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
33
components/icons/Bag.tsx
Normal 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
|
21
components/icons/Check.tsx
Normal file
21
components/icons/Check.tsx
Normal 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
|
20
components/icons/ChevronLeft.tsx
Normal file
20
components/icons/ChevronLeft.tsx
Normal 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
|
20
components/icons/ChevronUp.tsx
Normal file
20
components/icons/ChevronUp.tsx
Normal 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
|
20
components/icons/CreditCard.tsx
Normal file
20
components/icons/CreditCard.tsx
Normal 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
|
21
components/icons/Cross.tsx
Normal file
21
components/icons/Cross.tsx
Normal 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
|
22
components/icons/DoubleChevron.tsx
Normal file
22
components/icons/DoubleChevron.tsx
Normal 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
Loading…
x
Reference in New Issue
Block a user