├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── README.md ├── decs.d.ts ├── next-env.d.ts ├── next-sitemap.config.js ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── amex.svg ├── demo.gif ├── discover.svg ├── hero.jpg ├── mastercard.svg ├── robots.txt ├── sitemap.xml └── visa.svg ├── src ├── components │ ├── Account │ │ ├── Dashboard │ │ │ └── index.tsx │ │ ├── Grid │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── Menu │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ └── Orders │ │ │ └── index.tsx │ ├── AddressForm │ │ ├── countryList.ts │ │ ├── index.tsx │ │ └── styled.ts │ ├── AuthForm │ │ ├── index.tsx │ │ └── styled.ts │ ├── Cart │ │ ├── CartGrid │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── CartItem │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ └── CartTotal │ │ │ ├── index.tsx │ │ │ └── styled.ts │ ├── Category │ │ ├── CategoryElements.ts │ │ └── index.tsx │ ├── Cookies │ │ └── index.tsx │ ├── Footer │ │ ├── index.tsx │ │ └── styled.ts │ ├── Hero │ │ ├── index.tsx │ │ └── styled.ts │ ├── NavIcons │ │ ├── index.tsx │ │ └── styled.ts │ ├── Navbar │ │ ├── index.tsx │ │ └── styled.ts │ ├── OrderSummary │ │ ├── index.tsx │ │ └── styled.ts │ ├── PageTitle │ │ └── index.tsx │ ├── Product │ │ ├── AddToCartForm │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── ProductCard │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ └── ProductPrice │ │ │ ├── index.tsx │ │ │ └── styled.ts │ ├── Seo │ │ └── index.tsx │ ├── Sidebar │ │ ├── index.tsx │ │ └── styled.ts │ └── StripePayment │ │ ├── index.tsx │ │ └── styled.ts ├── containers │ ├── Account │ │ ├── index.tsx │ │ └── styled.ts │ ├── Cart │ │ ├── index.tsx │ │ └── styled.ts │ ├── Checkout │ │ ├── index.tsx │ │ └── styled.ts │ ├── Login │ │ ├── index.tsx │ │ └── styled.ts │ ├── Main │ │ ├── index.tsx │ │ └── styled.ts │ ├── Product │ │ ├── index.tsx │ │ └── styled.ts │ ├── Register │ │ ├── index.tsx │ │ └── styled.ts │ └── Shop │ │ ├── index.tsx │ │ └── styled.ts ├── context │ └── cart.tsx ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── about.tsx │ ├── account.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ ├── customers │ │ │ ├── create.ts │ │ │ └── retrieve.ts │ │ ├── orders │ │ │ ├── create.ts │ │ │ └── retrieve.ts │ │ ├── products │ │ │ └── retrieve.ts │ │ └── shipping │ │ │ └── retrieve.ts │ ├── cart.tsx │ ├── checkout.tsx │ ├── contact.tsx │ ├── home.tsx │ ├── login.tsx │ ├── products │ │ └── [slug].tsx │ ├── register.tsx │ ├── shop.tsx │ └── success.tsx ├── styles │ ├── main.ts │ ├── theme.ts │ └── utils.ts ├── types │ └── index.ts └── utils │ └── functions.ts └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "next/babel" 4 | ], 5 | "plugins": [ 6 | [ 7 | "styled-components", 8 | { 9 | "ssr": true, 10 | "fileName": false 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaVersion: 6, 6 | sourceType: 'module', 7 | ecmaFeatures: { jsx: true }, 8 | }, 9 | extends: ['prettier/@typescript-eslint', 'plugin:prettier/recommended'], 10 | globals: { 11 | React: 'writable', 12 | }, 13 | settings: { 14 | react: { 15 | version: 'detect', 16 | }, 17 | }, 18 | env: { 19 | node: true, 20 | browser: true, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .env.local 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | tabWidth: 2, 6 | useTabs: false, 7 | printWidth: 100, 8 | endOfLine: "auto" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js WooCommerce Storefront Theme 2 | 3 | Using Next.js, TypeScript and styled-components. 4 | 5 | [![Quality gate](https://sonarcloud.io/api/project_badges/quality_gate?project=Onixaz_nextjs-woocommerce-storefront)](https://sonarcloud.io/dashboard?id=Onixaz_nextjs-woocommerce-storefront) 6 | 7 | #### _WIP_ 8 | 9 | ![demo](https://github.com/Onixaz/nextjs-woocommerce-storefront/blob/main/public/demo.gif) 10 | 11 | ## The Goal 12 | 13 | The idea behind this repo was to showcase the power of [Next.js](https://nextjs.org/) by building a frontend for [WooCommerce](https://woocommerce.com/) using nothing but [Woo's REST API](https://woocommerce.github.io/woocommerce-rest-api-docs/) only. This means truly headless and secure WooCommerce without any redirects to checkouts etc. In a true [Jamstack](https://jamstack.org/) fashion. 14 | 15 | ## Features 16 | 17 | - WooCommerce Storefront theme inspired responsive design. 18 | - Static page generation using getStaticProps and getStaticPaths for SEO and performance. 19 | - Client side fetching of dynamic data like prices / account details using [SWR](https://swr.vercel.app/). 20 | - WooCommerce REST API abstraction using [Next's API routes](https://nextjs.org/docs/api-routes/introduction). 21 | - JWT based authentication for data fetching / endpoint protection. 22 | - Cart system using [CoCart](https://wordpress.org/plugins/cart-rest-api-for-woocommerce) plugin. 23 | - Customer registration and authentication using [NextAuth.js](https://next-auth.js.org/). 24 | - Checkout system using [Stripe](https://stripe.com/) as a payment method example. 25 | 26 | ## How to use 27 | 28 | Tested with Wordpress v6.4.1 WooCommerce v8.3.0 and PHP v7.4.3 29 | 30 | Install required plugins on your Wordpress: 31 | 32 | - [WooCommerce](https://wordpress.org/plugins/woocommerce/) (obviously) 33 | - [JWT Authentication for WP REST API](https://wordpress.org/plugins/jwt-authentication-for-wp-rest-api/) 34 | - [Password Reset with Code for WordPress REST API](https://wordpress.org/plugins/bdvs-password-reset/) (to be implemented) 35 | - [CoCart - Decoupling WooCommerce Made Easy](https://wordpress.org/plugins/cart-rest-api-for-woocommerce) 36 | - [CoCart – CORS Support](https://wordpress.org/plugins/cocart-cors/) 37 | 38 | Make sure Permalinks are set to **Post Name (Settings -> Permalinks).** Also make sure your **JWT Authentication for WP REST API** plugin is configured correctly. 39 | You will also need to add a shipping method to **Locations not covered by your other zones** for now. 40 | 41 | Lastly, you'll need to import some products. For testing you can use sample data from Woo https://docs.woocommerce.com/document/importing-woocommerce-sample-data/ just like I did. 42 | 43 | To test in-app payments you'll need to register a Stripe account for the publishable key and secret. (https://stripe.com/docs/keys) 44 | 45 | **Next clone this repo, cd into it and npm install.** 46 | 47 | Create **.env.local** file in the root of the project. 48 | 49 | It should consist of 50 | 51 | ``` 52 | NEXT_PUBLIC_WP_API_URL=https://example.com 53 | NEXTAUTH_URL=http://localhost:3000 // change to actual production url 54 | WP_JWT_AUTH_SECRET_KEY=your-random-secret 55 | NEXTAUTH_SECRET_KEY=your-another-random-secret 56 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your-stripe-publishable-key 57 | STRIPE_SECRET_KEY=your-stripe-secret-key 58 | 59 | ``` 60 | 61 | Notice that **NEXT_PUBLIC_WP_API_URL** and **NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY** should have **NEXT_PUBLIC** prefix, since these variables need to be exposed to the browser. 62 | 63 | **WP_JWT_AUTH_SECRET_KEY** which will be used to sign jwt should match the one you define in **wp-config.php** when configuring "JWT Authentication for WP REST API" plugin. 64 | 65 | Finally **npm run dev.** 66 | 67 | ## Notice 68 | 69 | ## Todo 70 | 71 | - ~~User registration and login functionality.~~ 72 | - ~~Dynamic prices using SWR (client side data fetching).~~ 73 | - ~~Shipping options.~~ 74 | - ~~Products pagination.~~ 75 | - ~~User specific cart.~~ 76 | - User dashboard (orders, addresses, password reset). 77 | - Pages for categories. 78 | - Blog page. 79 | - Image optimization. 80 | - Filters. 81 | - Coupons system. 82 | - Product reviews. 83 | - Wishlist. 84 | - Search. 85 | - More payment methods. 86 | - Tests 87 | 88 | #### Contributions are welcome 89 | 90 | MIT License 91 | -------------------------------------------------------------------------------- /decs.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Onixaz/nextjs-woocommerce-storefront/f54e1ad4be7be89224c0eceee7e0c799a3f53760/decs.d.ts -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { siteUrl: 'https://www.pajustudio.net', generateRobotsTxt: true } 2 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 2 | enabled: process.env.ANALYZE === 'true', 3 | }) 4 | 5 | module.exports = withBundleAnalyzer({ 6 | experimental: { 7 | nextScriptWorkers: true, 8 | }, 9 | async redirects() { 10 | return [ 11 | { 12 | source: '/', 13 | destination: '/home', 14 | permanent: true, 15 | }, 16 | ] 17 | }, 18 | }) 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-woocommerce-storefront", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "eslint --ext .ts,.tsx --fix", 11 | "postbuild": "next-sitemap --config next-sitemap.config.js" 12 | }, 13 | "husky": { 14 | "hooks": { 15 | "pre-commit": "lint-staged" 16 | } 17 | }, 18 | "lint-staged": { 19 | "./src/**/*.+(ts|tsx)": [ 20 | "eslint --fix", 21 | "git add" 22 | ], 23 | "./src/**/*.+(css|scss|js)": "prettier --write" 24 | }, 25 | "keywords": [], 26 | "author": "", 27 | "license": "MIT", 28 | "dependencies": { 29 | "@next/bundle-analyzer": "^10.0.7", 30 | "@stripe/react-stripe-js": "^1.1.2", 31 | "@stripe/stripe-js": "^1.11.0", 32 | "jsonwebtoken": "8.5.1", 33 | "next": "^12.3.4", 34 | "next-auth": "^4.24.5", 35 | "postcss": "^8.4.31", 36 | "react": "^17.0.2", 37 | "react-cookie-consent": "^6.2.1", 38 | "react-dom": "^17.0.2", 39 | "react-hook-form": "^6.14.1", 40 | "react-icons": "^3.11.0", 41 | "stripe": "^8.132.0", 42 | "styled-components": "^5.1.1", 43 | "swr": "^0.4.0" 44 | }, 45 | "devDependencies": { 46 | "@builder.io/partytown": "^0.8.1", 47 | "@types/jsonwebtoken": "^8.5.0", 48 | "@types/next-auth": "^3.1.24", 49 | "@types/node": "^14.14.20", 50 | "@types/react": "^16.14.51", 51 | "@types/styled-components": "^5.1.7", 52 | "@typescript-eslint/eslint-plugin": "^3.6.1", 53 | "@typescript-eslint/parser": "^3.6.1", 54 | "babel-plugin-styled-components": "^1.12.0", 55 | "eslint": "^7.17.0", 56 | "eslint-config-next": "^12.3.4", 57 | "eslint-config-prettier": "^6.11.0", 58 | "eslint-plugin-prettier": "^3.3.1", 59 | "husky": "^4.3.7", 60 | "lint-staged": "^10.5.3", 61 | "next-sitemap": "^1.4.5", 62 | "prettier": "^2.2.1", 63 | "typescript": "^4.9.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/amex.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Onixaz/nextjs-woocommerce-storefront/f54e1ad4be7be89224c0eceee7e0c799a3f53760/public/demo.gif -------------------------------------------------------------------------------- /public/discover.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Onixaz/nextjs-woocommerce-storefront/f54e1ad4be7be89224c0eceee7e0c799a3f53760/public/hero.jpg -------------------------------------------------------------------------------- /public/mastercard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / 4 | 5 | # Host 6 | Host: https://www.pajustudio.net 7 | 8 | # Sitemaps 9 | Sitemap: https://www.pajustudio.net/sitemap.xml 10 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://www.pajustudio.net/aboutdaily0.72023-11-21T13:19:35.615Z 4 | https://www.pajustudio.net/accountdaily0.72023-11-21T13:19:35.615Z 5 | https://www.pajustudio.net/cartdaily0.72023-11-21T13:19:35.615Z 6 | https://www.pajustudio.net/checkoutdaily0.72023-11-21T13:19:35.615Z 7 | https://www.pajustudio.net/contactdaily0.72023-11-21T13:19:35.615Z 8 | https://www.pajustudio.net/homedaily0.72023-11-21T13:19:35.615Z 9 | https://www.pajustudio.net/logindaily0.72023-11-21T13:19:35.615Z 10 | https://www.pajustudio.net/registerdaily0.72023-11-21T13:19:35.615Z 11 | https://www.pajustudio.net/shopdaily0.72023-11-21T13:19:35.615Z 12 | https://www.pajustudio.net/successdaily0.72023-11-21T13:19:35.615Z 13 | https://www.pajustudio.net/products/t-shirt-with-logodaily0.72023-11-21T13:19:35.615Z 14 | https://www.pajustudio.net/products/beanie-with-logodaily0.72023-11-21T13:19:35.615Z 15 | https://www.pajustudio.net/products/logo-collectiondaily0.72023-11-21T13:19:35.615Z 16 | https://www.pajustudio.net/products/wordpress-pennantdaily0.72023-11-21T13:19:35.615Z 17 | https://www.pajustudio.net/products/v-neck-t-shirtdaily0.72023-11-21T13:19:35.615Z 18 | https://www.pajustudio.net/products/hoodiedaily0.72023-11-21T13:19:35.615Z 19 | https://www.pajustudio.net/products/hoodie-with-logodaily0.72023-11-21T13:19:35.615Z 20 | https://www.pajustudio.net/products/t-shirtdaily0.72023-11-21T13:19:35.615Z 21 | https://www.pajustudio.net/products/beaniedaily0.72023-11-21T13:19:35.615Z 22 | https://www.pajustudio.net/products/beltdaily0.72023-11-21T13:19:35.615Z 23 | https://www.pajustudio.net/products/capdaily0.72023-11-21T13:19:35.615Z 24 | https://www.pajustudio.net/products/sunglassesdaily0.72023-11-21T13:19:35.615Z 25 | https://www.pajustudio.net/products/hoodie-with-pocketdaily0.72023-11-21T13:19:35.615Z 26 | https://www.pajustudio.net/products/hoodie-with-zipperdaily0.72023-11-21T13:19:35.615Z 27 | https://www.pajustudio.net/products/long-sleeve-teedaily0.72023-11-21T13:19:35.616Z 28 | https://www.pajustudio.net/products/polodaily0.72023-11-21T13:19:35.616Z 29 | https://www.pajustudio.net/products/albumdaily0.72023-11-21T13:19:35.616Z 30 | https://www.pajustudio.net/products/singledaily0.72023-11-21T13:19:35.616Z 31 | -------------------------------------------------------------------------------- /public/visa.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Account/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSession } from 'next-auth/react' 2 | import React from 'react' 3 | 4 | const AccountDashboard: React.FC = () => { 5 | const { data: session }: any = useSession() 6 | 7 | return ( 8 |
9 |

Welcome, {session?.user?.username}!

10 |
11 |

12 | From your account dashboard you can view your recent orders and your wishlist, manage your 13 | shipping and billing addresses, and edit your password and account details using the form 14 | below. 15 |

16 |
17 |

TO BE IMPLEMENTED

18 |
19 | ) 20 | } 21 | 22 | export default AccountDashboard 23 | -------------------------------------------------------------------------------- /src/components/Account/Grid/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import AccountMenu from '../Menu' 3 | import * as AccGridStyles from './styled' 4 | import AccountDashboard from '../Dashboard' 5 | import AccountOrders from '../Orders' 6 | 7 | const AccountGrid: React.FC = () => { 8 | const [view, setView] = useState('dashboard') 9 | 10 | const renderView = (): { [key: string]: React.ReactElement } => ({ 11 | dashboard: , 12 | orders: , 13 | wishlist:

wishlist

, 14 | addresses:

addresses

, 15 | }) 16 | 17 | return ( 18 | 19 | 20 | 21 |
{renderView()[view]}
22 |
23 | ) 24 | } 25 | 26 | export default AccountGrid 27 | -------------------------------------------------------------------------------- /src/components/Account/Grid/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Grid = styled.div` 4 | display: grid; 5 | grid-template-columns: 30% 70%; 6 | min-height: 50vh; 7 | width: 100%; 8 | max-width: 840px; 9 | margin: 0 auto; 10 | ` 11 | -------------------------------------------------------------------------------- /src/components/Account/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { SetStateAction, Dispatch, useContext } from 'react' 2 | import { signOut } from 'next-auth/react' 3 | import * as AccountMenuStyles from './styled' 4 | import { useRouter } from 'next/router' 5 | import { CartContext } from '../../../context/cart' 6 | import { initCart } from '../../../utils/functions' 7 | 8 | interface AccountMenuProps { 9 | setView: Dispatch> 10 | } 11 | 12 | const AccountMenu: React.FC = ({ setView }) => { 13 | const router = useRouter() 14 | const [, setCart] = useContext(CartContext) 15 | 16 | const handleLogout = async (options: any) => { 17 | const newCart = await initCart() 18 | setCart(newCart) 19 | await signOut(options) 20 | router.push('/login') 21 | } 22 | return ( 23 | 24 | 25 | setView('dashboard')}> 26 | Dashboard 27 | 28 | setView('orders')}> 29 | Orders 30 | 31 | setView('wishlist')}> 32 | Wishlist 33 | 34 | setView('addresses')}> 35 | Addresses 36 | 37 | handleLogout({ redirect: false })}> 38 | Logout 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | export default React.memo(AccountMenu) 46 | -------------------------------------------------------------------------------- /src/components/Account/Menu/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | ` 7 | export const Menu = styled.nav` 8 | display: flex; 9 | flex-direction: column; 10 | ` 11 | 12 | export const LinkText = styled.a` 13 | cursor: pointer; 14 | padding: 1rem 0.25rem; 15 | transition: all 0.2s ease-in-out; 16 | border-top: 1px solid #e3e3e3; 17 | 18 | &:hover { 19 | color: ${({ theme }) => theme.primaryPurple}; 20 | } 21 | &:last-child { 22 | border-bottom: 1px solid #e3e3e3; 23 | } 24 | ` 25 | -------------------------------------------------------------------------------- /src/components/Account/Orders/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useSWR from 'swr' 3 | import { Order } from '../../../types' 4 | 5 | const AccountOrders: React.FC = () => { 6 | const { data } = useSWR('/api/orders/retrieve') 7 | 8 | return ( 9 |
10 |
    11 | {data?.map((item: Order) => { 12 | return ( 13 |
  • 14 | 15 | {item.id} {item.date_created} {item.status} {item.total} 16 | 17 |
  • 18 | ) 19 | })} 20 |
21 |
22 | ) 23 | } 24 | 25 | export default AccountOrders 26 | -------------------------------------------------------------------------------- /src/components/AddressForm/countryList.ts: -------------------------------------------------------------------------------- 1 | export const countries = [ 2 | 'Afghanistan', 3 | 'Albania', 4 | 'Algeria', 5 | 'Andorra', 6 | 'Angola', 7 | 'Anguilla', 8 | 'Antigua & Barbuda', 9 | 'Argentina', 10 | 'Armenia', 11 | 'Aruba', 12 | 'Australia', 13 | 'Austria', 14 | 'Azerbaijan', 15 | 'Bahamas', 16 | 'Bahrain', 17 | 'Bangladesh', 18 | 'Barbados', 19 | 'Belarus', 20 | 'Belgium', 21 | 'Belize', 22 | 'Benin', 23 | 'Bermuda', 24 | 'Bhutan', 25 | 'Bolivia', 26 | 'Bosnia & Herzegovina', 27 | 'Botswana', 28 | 'Brazil', 29 | 'British Virgin Islands', 30 | 'Brunei', 31 | 'Bulgaria', 32 | 'Burkina Faso', 33 | 'Burundi', 34 | 'Cambodia', 35 | 'Cameroon', 36 | 'Cape Verde', 37 | 'Cayman Islands', 38 | 'Chad', 39 | 'Chile', 40 | 'China', 41 | 'Colombia', 42 | 'Congo', 43 | 'Cook Islands', 44 | 'Costa Rica', 45 | 'Cote D Ivoire', 46 | 'Croatia', 47 | 'Cruise Ship', 48 | 'Cuba', 49 | 'Cyprus', 50 | 'Czech Republic', 51 | 'Denmark', 52 | 'Djibouti', 53 | 'Dominica', 54 | 'Dominican Republic', 55 | 'Ecuador', 56 | 'Egypt', 57 | 'El Salvador', 58 | 'Equatorial Guinea', 59 | 'Estonia', 60 | 'Ethiopia', 61 | 'Falkland Islands', 62 | 'Faroe Islands', 63 | 'Fiji', 64 | 'Finland', 65 | 'France', 66 | 'French Polynesia', 67 | 'French West Indies', 68 | 'Gabon', 69 | 'Gambia', 70 | 'Georgia', 71 | 'Germany', 72 | 'Ghana', 73 | 'Gibraltar', 74 | 'Greece', 75 | 'Greenland', 76 | 'Grenada', 77 | 'Guam', 78 | 'Guatemala', 79 | 'Guernsey', 80 | 'Guinea', 81 | 'Guinea Bissau', 82 | 'Guyana', 83 | 'Haiti', 84 | 'Honduras', 85 | 'Hong Kong', 86 | 'Hungary', 87 | 'Iceland', 88 | 'India', 89 | 'Indonesia', 90 | 'Iran', 91 | 'Iraq', 92 | 'Ireland', 93 | 'Isle of Man', 94 | 'Israel', 95 | 'Italy', 96 | 'Jamaica', 97 | 'Japan', 98 | 'Jersey', 99 | 'Jordan', 100 | 'Kazakhstan', 101 | 'Kenya', 102 | 'Kuwait', 103 | 'Kyrgyz Republic', 104 | 'Laos', 105 | 'Latvia', 106 | 'Lebanon', 107 | 'Lesotho', 108 | 'Liberia', 109 | 'Libya', 110 | 'Liechtenstein', 111 | 'Lithuania', 112 | 'Luxembourg', 113 | 'Macau', 114 | 'Macedonia', 115 | 'Madagascar', 116 | 'Malawi', 117 | 'Malaysia', 118 | 'Maldives', 119 | 'Mali', 120 | 'Malta', 121 | 'Mauritania', 122 | 'Mauritius', 123 | 'Mexico', 124 | 'Moldova', 125 | 'Monaco', 126 | 'Mongolia', 127 | 'Montenegro', 128 | 'Montserrat', 129 | 'Morocco', 130 | 'Mozambique', 131 | 'Namibia', 132 | 'Nepal', 133 | 'Netherlands', 134 | 'Netherlands Antilles', 135 | 'New Caledonia', 136 | 'New Zealand', 137 | 'Nicaragua', 138 | 'Niger', 139 | 'Nigeria', 140 | 'Norway', 141 | 'Oman', 142 | 'Pakistan', 143 | 'Palestine', 144 | 'Panama', 145 | 'Papua New Guinea', 146 | 'Paraguay', 147 | 'Peru', 148 | 'Philippines', 149 | 'Poland', 150 | 'Portugal', 151 | 'Puerto Rico', 152 | 'Qatar', 153 | 'Reunion', 154 | 'Romania', 155 | 'Russia', 156 | 'Rwanda', 157 | 'Saint Pierre & Miquelon', 158 | 'Samoa', 159 | 'San Marino', 160 | 'Satellite', 161 | 'Saudi Arabia', 162 | 'Senegal', 163 | 'Serbia', 164 | 'Seychelles', 165 | 'Sierra Leone', 166 | 'Singapore', 167 | 'Slovakia', 168 | 'Slovenia', 169 | 'South Africa', 170 | 'South Korea', 171 | 'Spain', 172 | 'Sri Lanka', 173 | 'St Kitts & Nevis', 174 | 'St Lucia', 175 | 'St Vincent', 176 | 'St. Lucia', 177 | 'Sudan', 178 | 'Suriname', 179 | 'Swaziland', 180 | 'Sweden', 181 | 'Switzerland', 182 | 'Syria', 183 | 'Taiwan', 184 | 'Tajikistan', 185 | 'Tanzania', 186 | 'Thailand', 187 | "Timor L'Este", 188 | 'Togo', 189 | 'Tonga', 190 | 'Trinidad & Tobago', 191 | 'Tunisia', 192 | 'Turkey', 193 | 'Turkmenistan', 194 | 'Turks & Caicos', 195 | 'Uganda', 196 | 'Ukraine', 197 | 'United Arab Emirates', 198 | 'United Kingdom', 199 | 'Uruguay', 200 | 'Uzbekistan', 201 | 'Venezuela', 202 | 'Vietnam', 203 | 'Virgin Islands (US)', 204 | 'Yemen', 205 | 'Zambia', 206 | 'Zimbabwe', 207 | ] 208 | -------------------------------------------------------------------------------- /src/components/AddressForm/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as AddressFormStyles from './styled' 3 | import { countries } from './countryList' 4 | import { Subtitle } from '../../styles/utils' 5 | 6 | interface AddressFormProps { 7 | register: any 8 | errors: any 9 | } 10 | 11 | const AddressForm: React.FC = ({ register, errors }) => { 12 | return ( 13 | <> 14 | 15 | 16 | First name 17 | 18 | {errors.first_name && ( 19 | This field is required 20 | )} 21 | 22 | 23 | Last name 24 | 25 | {errors.last_name && ( 26 | This field is required 27 | )} 28 | 29 | 30 | 31 | 32 | Company (optional) 33 | 34 | 35 | 36 | 37 | Country 38 | 53 | {/* */} 54 | 55 | 56 | 57 | Street address 58 | 59 | {errors.address_1 && ( 60 | This field is required 61 | )} 62 | 63 | 64 | 65 | Street address 2 (optional) 66 | 67 | 68 | 69 | 70 | Town / City 71 | 72 | {errors.city && This field is required} 73 | 74 | 75 | 76 | State / County 77 | 78 | {errors.state && This field is required} 79 | 80 | 81 | 82 | Postcode / ZIP 83 | 84 | {errors.postcode && ( 85 | This field is required 86 | )} 87 | 88 | 89 | 90 | Phone 91 | 92 | {errors.phone && This field is required} 93 | 94 | 95 | 96 | Email address 97 | 107 | {errors.email && {errors.email.message}} 108 | 109 | 110 | 111 | Shipping addess same as billing addess 112 | 113 | Additional Information 114 | 115 | Order notes (optional) 116 | 121 | 122 | 123 | ) 124 | } 125 | 126 | export default AddressForm 127 | -------------------------------------------------------------------------------- /src/components/AddressForm/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const FieldWrapper = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | width: 100%; 7 | position: relative; 8 | ` 9 | export const RowBlock = styled.div` 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | width: 100%; 14 | 15 | @media screen and (max-width: 480px) { 16 | flex-direction: column; 17 | } 18 | ` 19 | 20 | export const ShippingBlock = styled.div` 21 | margin: 1rem; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | flex-direction: row-reverse; 26 | width: 100%; 27 | ` 28 | 29 | export const Label = styled.label` 30 | margin: 0.2rem 1rem; 31 | font-size: calc(0.9rem + 0.1vw); 32 | ` 33 | export const Input = styled.input` 34 | background: ${({ theme }) => theme.lightMediumBg}; 35 | margin: 0.1rem 1rem; 36 | padding: 0.5rem; 37 | font-size: calc(0.9rem + 0.1vw); 38 | border: none; 39 | outline: none; 40 | position: relative; 41 | 42 | &[type='checkbox'] { 43 | appearance: none; 44 | &:checked:after { 45 | content: '\\2713'; 46 | color: black; 47 | position: absolute; 48 | line-height: 1rem; 49 | font-size: 1rem; 50 | top: 0; 51 | left: 3px; 52 | } 53 | } 54 | ` 55 | 56 | export const CustomerNote = styled.textarea` 57 | min-height: 150px; 58 | margin: 0.1rem 1rem; 59 | padding: 0.5rem; 60 | font-size: calc(0.9rem + 0.1vw); 61 | background: ${({ theme }) => theme.lightMediumBg}; 62 | border: none; 63 | outline: none; 64 | resize: none; 65 | ` 66 | 67 | export const Error = styled.span` 68 | margin: 0.1rem 1rem; 69 | padding: 0.5rem; 70 | color: red; 71 | ` 72 | -------------------------------------------------------------------------------- /src/components/AuthForm/index.tsx: -------------------------------------------------------------------------------- 1 | import * as AuthFormStyles from './styled' 2 | import { SubmitHandler, useForm } from 'react-hook-form' 3 | import { signIn } from 'next-auth/react' 4 | import React, { useContext, useRef, useState } from 'react' 5 | import { useRouter } from 'next/router' 6 | import { Loader, SectionTitle } from '../../styles/utils' 7 | import Link from 'next/link' 8 | import { CartContext } from '../../context/cart' 9 | 10 | interface AuthFormProps { 11 | isRegister: boolean 12 | } 13 | interface FormValues { 14 | username: string 15 | first_name: string 16 | last_name: string 17 | email: string 18 | password: string 19 | passwordRepeat: string 20 | cartData: string 21 | } 22 | 23 | const AuthForm: React.FC = ({ isRegister }) => { 24 | const { register, handleSubmit, errors, watch } = useForm() 25 | const [submiting, setSubmiting] = useState(false) 26 | const [response, setResponse] = useState('') 27 | const [cart] = useContext(CartContext) 28 | const password = useRef({}) 29 | const router = useRouter() 30 | password.current = watch('password', '') 31 | 32 | const btnText = isRegister ? 'Register' : 'Login' 33 | 34 | const onSubmit: SubmitHandler = async (data) => { 35 | try { 36 | setSubmiting(true) 37 | const cartData = JSON.stringify(cart) 38 | data = { ...data, cartData } 39 | if (isRegister) { 40 | const req = await fetch('/api/customers/create', { 41 | method: 'POST', 42 | headers: { 'Content-Type': 'application/json' }, 43 | body: JSON.stringify(data), 44 | }) 45 | 46 | const { message } = await req.json() 47 | 48 | if (req.status === 200) { 49 | await signIn('credentials', { 50 | redirect: false, 51 | ...data, 52 | }) 53 | 54 | router.push('account') 55 | } else { 56 | setResponse(message) 57 | } 58 | } else { 59 | const user: any = await signIn('credentials', { 60 | redirect: false, 61 | ...data, 62 | }) 63 | 64 | if (user.ok === true) { 65 | router.push('account') 66 | } else { 67 | setResponse('Wrong username or password') 68 | } 69 | setSubmiting(false) 70 | } 71 | } catch (error) { 72 | setSubmiting(false) 73 | console.error(error) 74 | } 75 | } 76 | return ( 77 | 78 | {!isRegister && My account} 79 | 80 | 81 | {isRegister ? 'Register as a new customer!' : 'Login'} 82 | 83 | 84 | 85 | Username 86 | 93 | {errors.username && {errors.username.message}} 94 | 95 | {isRegister && ( 96 | <> 97 | 98 | First Name 99 | 100 | {errors.first_name && ( 101 | This field is required 102 | )} 103 | 104 | 105 | Last Name 106 | 107 | {errors.last_name && ( 108 | This field is required 109 | )} 110 | 111 | 112 | Email address 113 | 123 | {errors.email && {errors.email.message}} 124 | {' '} 125 | 126 | )} 127 | 128 | 129 | Password 130 | 141 | {errors.password && {errors.password.message}} 142 | 143 | 144 | {isRegister && ( 145 | 146 | Repeat password 147 | value === password.current || "Paswords don't match", 153 | })} 154 | /> 155 | {errors.passwordRepeat && ( 156 | {errors.passwordRepeat.message} 157 | )} 158 | 159 | )} 160 | 161 | 162 | {submiting ? : btnText} 163 | 164 | {response} 165 | {!isRegister && ( 166 | 167 | Don't have an account? 168 | 169 | )} 170 | 171 | ) 172 | } 173 | 174 | export default AuthForm 175 | -------------------------------------------------------------------------------- /src/components/AuthForm/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.form` 4 | //padding-top: 5rem; 5 | max-width: 520px; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | flex-direction: column; 10 | margin: 0 auto; 11 | ` 12 | 13 | export const FieldWrapper = styled.div` 14 | display: flex; 15 | flex-direction: column; 16 | width: 100%; 17 | position: relative; 18 | ` 19 | export const RowBlock = styled.div` 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | width: 100%; 24 | 25 | @media screen and (max-width: 480px) { 26 | flex-direction: column; 27 | } 28 | ` 29 | 30 | export const Input = styled.input` 31 | background: ${({ theme }) => theme.lightMediumBg}; 32 | margin: 0.1rem 1rem; 33 | padding: 0.5rem; 34 | font-size: calc(0.9rem + 0.1vw); 35 | border: none; 36 | outline: none; 37 | position: relative; 38 | 39 | &[type='checkbox'] { 40 | appearance: none; 41 | &:checked:after { 42 | content: '\\2713'; 43 | color: black; 44 | position: absolute; 45 | line-height: 1rem; 46 | font-size: 1rem; 47 | top: 0; 48 | left: 3px; 49 | } 50 | } 51 | ` 52 | 53 | export const Error = styled.span` 54 | margin: 0.1rem 1rem; 55 | padding: 0.5rem; 56 | color: red; 57 | ` 58 | export const Label = styled.label` 59 | margin: 0.2rem 1rem; 60 | font-size: calc(0.9rem + 0.1vw); 61 | ` 62 | 63 | export const SubmitBtn = styled.button` 64 | margin: 3rem auto; 65 | min-width: 150px; 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | font-size: calc(1rem + 0.1vw); 70 | background-color: #333333; 71 | border-color: #333333; 72 | color: #ffffff; 73 | cursor: pointer; 74 | padding: 0.5em 1.5em; 75 | text-decoration: none; 76 | font-weight: 600; 77 | 78 | transition: all 0.2s ease-in-out; 79 | ` 80 | export const Response = styled.p` 81 | margin: 0.1rem 1rem; 82 | padding: 0.5rem; 83 | color: red; 84 | ` 85 | 86 | export const Message = styled.p` 87 | cursor: pointer; 88 | margin: 0 auto; 89 | max-width: 400px; 90 | font-size: calc(1rem + 0.1vw); 91 | letter-spacing: 1px; 92 | 93 | transition: all 0.2s ease-in-out; 94 | 95 | &:hover { 96 | color: ${({ theme }) => theme.primaryPurple}; 97 | } 98 | ` 99 | export const Subtitle = styled.h2` 100 | margin: -1rem auto 1rem auto; 101 | font-size: calc(1.5rem + 0.1vw); 102 | font-weight: 200; 103 | text-align: center; 104 | ` 105 | -------------------------------------------------------------------------------- /src/components/Cart/CartGrid/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as CartGridStyles from './styled' 3 | import SingleCartItem from '../../../components/Cart/CartItem' 4 | import { Cart, CartItem } from '../../../types' 5 | import { getSingleProduct } from '../../../utils/functions' 6 | 7 | interface CartGridProps { 8 | cart: Cart 9 | data: any 10 | } 11 | 12 | const CartGrid: React.FC = ({ cart, data }) => { 13 | return ( 14 | <> 15 | 16 | 17 | {cart.items.map((item: CartItem) => { 18 | return ( 19 | 20 | 21 | 22 | Product 23 | Price 24 | Quantity 25 | Subtotal 26 | 27 | ) 28 | })} 29 | 30 | 31 | {cart.items.map((item: CartItem) => { 32 | const product = getSingleProduct(item.product_id, data) 33 | return ( 34 | 35 | ) 36 | })} 37 | 38 | 39 | 40 | ) 41 | } 42 | 43 | export default CartGrid 44 | -------------------------------------------------------------------------------- /src/components/Cart/CartGrid/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | display: grid; 5 | position: relative; 6 | grid-template-columns: 1fr; 7 | width: 90%; 8 | @media screen and (max-width: 768px) { 9 | grid-template-columns: repeat(2, 1fr); 10 | } 11 | ` 12 | export const Totals = styled.div` 13 | display: flex; 14 | justify-content: flex-end; 15 | ` 16 | 17 | export const FirstCol = styled.div` 18 | display: none; 19 | 20 | @media screen and (max-width: 768px) { 21 | display: flex; 22 | flex-direction: column; 23 | } 24 | ` 25 | 26 | export const SecondCol = styled.div` 27 | display: flex; 28 | flex-direction: column; 29 | @media screen and (max-width: 768px) { 30 | flex-direction: column; 31 | } 32 | ` 33 | 34 | export const DescriptionRow = styled.div` 35 | @media screen and (max-width: 768px) { 36 | margin: 1rem 0; 37 | 38 | display: flex; 39 | flex-direction: column; 40 | } 41 | ` 42 | 43 | export const Description = styled.div` 44 | background: #fafafa; 45 | 46 | font-size: calc(1rem + 0.1vw); 47 | font-weight: bolder; 48 | color: ${({ theme }) => theme.primaryText}; 49 | opacity: 0.9; 50 | text-align: center; 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | width: 100%; 55 | height: 120px; 56 | ` 57 | 58 | export const QuantityBlock = styled.div` 59 | min-width: 200px; 60 | ` 61 | 62 | export const CheckoutBtn = styled.button` 63 | margin-top: 3rem; 64 | font-size: calc(1.2rem + 0.1vw); 65 | background-color: #333333; 66 | border-color: #333333; 67 | color: #ffffff; 68 | cursor: pointer; 69 | padding: 0.5em 1.5em; 70 | text-decoration: none; 71 | font-weight: 600; 72 | display: inline-block; 73 | transition: all 0.2s ease-in-out; 74 | ` 75 | -------------------------------------------------------------------------------- /src/components/Cart/CartItem/index.tsx: -------------------------------------------------------------------------------- 1 | import * as CartItemStyles from './styled' 2 | import React, { useContext, useRef, useState } from 'react' 3 | import { initCart, updateCart } from '../../../utils/functions' 4 | import { CartContext } from '../../../context/cart' 5 | import { CartItem } from '../../../types' 6 | import Link from 'next/link' 7 | import { Loader } from '../../../styles/utils' 8 | 9 | interface CartItemProps { 10 | item: CartItem 11 | price: number 12 | } 13 | 14 | const SingleCartItem: React.FC = ({ item, price }) => { 15 | const [cart, setCart, isUpdating, setIsUpdating] = useContext(CartContext) 16 | const [isRemoving, setIsRemoving] = useState(false) 17 | const [isAnimating, setIsAnimating] = useState(false) 18 | 19 | const quantityRef = useRef(null) 20 | 21 | const removeItem = async (cartItem: CartItem) => { 22 | setIsRemoving(true) 23 | setIsUpdating(true) 24 | try { 25 | const res = await fetch( 26 | `${process.env.NEXT_PUBLIC_WP_API_URL}/wp-json/cocart/v1/item?cart_key=${cart.key}`, 27 | { 28 | method: 'DELETE', 29 | body: JSON.stringify({ 30 | cart_item_key: cartItem.key, 31 | return_cart: true, 32 | }), 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | }, 36 | }, 37 | ) 38 | if (res.status !== 200) throw Error('Problem with remote cart') 39 | const data = await res.json() 40 | 41 | setIsUpdating(false) 42 | setIsRemoving(false) 43 | setCart(() => updateCart(cart, data)) 44 | } catch (error) { 45 | const newCart = await initCart() 46 | setCart(newCart) 47 | setIsUpdating(false) 48 | setIsRemoving(false) 49 | } 50 | } 51 | 52 | const updateItem = async (e: React.SyntheticEvent, cartItem: CartItem, quantity: number) => { 53 | e.preventDefault() 54 | setIsUpdating(true) 55 | setIsAnimating(true) 56 | try { 57 | const res = await fetch( 58 | `${process.env.NEXT_PUBLIC_WP_API_URL}/wp-json/cocart/v1/item?cart_key=${cart.key}`, 59 | { 60 | method: 'POST', 61 | body: JSON.stringify({ 62 | cart_item_key: cartItem.key, 63 | quantity, 64 | return_cart: true, 65 | }), 66 | headers: { 67 | 'Content-Type': 'application/json', 68 | }, 69 | }, 70 | ) 71 | if (res.status !== 200) throw Error('Problem with remote cart') 72 | const data = await res.json() 73 | 74 | setIsUpdating(false) 75 | setIsAnimating(false) 76 | setCart(() => updateCart(cart, data)) 77 | } catch (error) { 78 | console.error(error) 79 | const newCart = await initCart() 80 | setCart(newCart) 81 | setIsAnimating(false) 82 | setIsUpdating(false) 83 | } 84 | } 85 | 86 | const itemTotal = price * item.quantity 87 | 88 | return ( 89 | <> 90 | 91 | 92 | removeItem(item)}> 93 | {isRemoving ? : }{' '} 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {item.product_name} 102 | 103 | 104 | ${price.toFixed(2)} 105 | 106 | 107 | 114 | { 117 | updateItem(e, item, parseInt(quantityRef.current!.value)) 118 | }} 119 | > 120 | {isAnimating ? ( 121 | 122 | ) : ( 123 | Update 124 | )} 125 | 126 | 127 | 128 | 129 | ${itemTotal.toFixed(2)} 130 | 131 | 132 | 133 | ) 134 | } 135 | 136 | export default SingleCartItem 137 | -------------------------------------------------------------------------------- /src/components/Cart/CartItem/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { RiCloseCircleFill } from 'react-icons/ri' 3 | 4 | export const Thumbnail = styled.img` 5 | padding: 1rem 0.5rem; 6 | width: 100%; 7 | height: 100%; 8 | max-width: 100px; 9 | 10 | @media screen and (max-width: 768px) { 11 | position: absolute; 12 | height: 160px; 13 | max-width: 160px; 14 | left: 50%; 15 | transform: translate(-50%, -25%); 16 | } 17 | ` 18 | 19 | export const ProductLink = styled.a` 20 | text-decoration: underline; 21 | cursor: pointer; 22 | ` 23 | 24 | export const ProductSubtotal = styled.p` 25 | font-weight: bolder; 26 | font-size: calc(0.95rem + 0.1vw); 27 | color: ${({ theme }) => theme.primaryText}; 28 | ` 29 | 30 | export const RemoveFromCartBtn = styled.button` 31 | border: none; 32 | background: transparent; 33 | 34 | color: #333333; 35 | cursor: pointer; 36 | 37 | @media screen and (max-width: 768px) { 38 | position: absolute; 39 | right: 0; 40 | transform: translate(50%, -150%); 41 | } 42 | ` 43 | 44 | export const RemoveIcon = styled(RiCloseCircleFill)` 45 | font-size: calc(1.5rem + 0.1vw); 46 | 47 | @media screen and (max-width: 768px) { 48 | font-size: 2rem; 49 | } 50 | ` 51 | 52 | export const QuantityForm = styled.form` 53 | margin: 2rem 0; 54 | display: flex; 55 | flex-direction: row; 56 | ` 57 | 58 | export const InputField = styled.input` 59 | padding: 0.5em; 60 | margin-right: 1rem; 61 | max-width: 60px; 62 | font-size: 1rem; 63 | text-align: center; 64 | background-color: #f2f2f2; 65 | color: #43454b; 66 | border: none; 67 | box-sizing: border-box; 68 | font-weight: 400; 69 | 70 | &[type='number']::-webkit-inner-spin-button { 71 | opacity: 1; 72 | } 73 | ` 74 | export const UpdateCartItemBtn = styled.button` 75 | display: flex; 76 | justify-content: center; 77 | align-items: center; 78 | background-color: #333333; 79 | border-color: #333333; 80 | color: #ffffff; 81 | cursor: pointer; 82 | padding: 0.5em 1.5em; 83 | width: calc(75px + 0.2vw); 84 | height: 41px; 85 | transition: all 0.2s ease-in-out; 86 | ` 87 | 88 | export const UpdateText = styled.p` 89 | font-weight: 600; 90 | font-size: calc(0.75rem + 0.1vw); 91 | ` 92 | export const CartEl = styled.div` 93 | text-align: center; 94 | display: flex; 95 | justify-content: center; 96 | align-items: center; 97 | width: 100%; 98 | height: 120px; 99 | background: #fafafa; 100 | ` 101 | 102 | export const CartRow = styled.div` 103 | display: flex; 104 | flex-direction: row; 105 | 106 | @media screen and (max-width: 768px) { 107 | flex-direction: column; 108 | margin: 1rem 0; 109 | } 110 | ` 111 | export const RemovingLoader = styled.div` 112 | border: 2px solid #333333; 113 | border-radius: 50%; 114 | border-top: 2px solid #3333; 115 | width: 1.5em; 116 | height: 1.5em; 117 | animation: spin 1s linear infinite; 118 | 119 | @media screen and (max-width: 768px) { 120 | position: absolute; 121 | right: -8px; 122 | top: -68px; 123 | } 124 | ` 125 | -------------------------------------------------------------------------------- /src/components/Cart/CartTotal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import useSWR from 'swr' 3 | import { CartContext } from '../../../context/cart' 4 | import { Loader } from '../../../styles/utils' 5 | import { CartItem } from '../../../types' 6 | import { getSingleProduct } from '../../../utils/functions' 7 | import * as CartTotalStyles from './styled' 8 | 9 | interface CartTotalProps { 10 | adds?: number 11 | } 12 | 13 | const CartTotal: React.FC = ({ adds }) => { 14 | const { data } = useSWR('/api/products/retrieve') 15 | const [cart] = useContext(CartContext) 16 | 17 | if (!data) { 18 | return 19 | } 20 | 21 | const cartTotal = cart.items.reduce((acc: number, curr: CartItem) => { 22 | const product = getSingleProduct(curr.product_id, data) 23 | if (!product) return 0 24 | 25 | return acc + curr.quantity * product.price 26 | }, 0) 27 | 28 | return ( 29 | 30 | ${adds && adds > 0 ? (adds + cartTotal).toFixed(2) : cartTotal.toFixed(2)} 31 | 32 | ) 33 | } 34 | 35 | export default React.memo(CartTotal) 36 | -------------------------------------------------------------------------------- /src/components/Cart/CartTotal/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Total = styled.p` 4 | margin: 0 0.25rem; 5 | white-space: nowrap; 6 | letter-spacing: 1.1px; 7 | ` 8 | -------------------------------------------------------------------------------- /src/components/Category/CategoryElements.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const CategoryImg = styled.img` 4 | width: 100%; 5 | height: 100%; 6 | //border-radius: 30px; 7 | ` 8 | 9 | export const CategoryName = styled.p` 10 | text-align: center; 11 | font-size: calc(1.5rem + 0.1vw); 12 | letter-spacing: 1px; 13 | opacity: 0.9; 14 | font-weight: 200; 15 | padding: 1rem 0; 16 | ` 17 | export const CategoryCard = styled.div` 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: center; 21 | ` 22 | 23 | export const CategoryImgWrapper = styled.div` 24 | height: 340px; 25 | width: 100%; 26 | overflow: hidden; 27 | position: relative; 28 | padding: 0.75rem; 29 | ` 30 | -------------------------------------------------------------------------------- /src/components/Category/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CategoryImg, CategoryName, CategoryCard, CategoryImgWrapper } from './CategoryElements' 3 | import { Category } from '../../types' 4 | interface SingleCategoryProps { 5 | category: Category 6 | } 7 | 8 | const SingleCategory: React.FC = ({ category }) => { 9 | return ( 10 | 11 | 12 | {category.image !== null && ( 13 | 14 | )} 15 | 16 | 17 | {category.name} ({category.count}) 18 | 19 | 20 | ) 21 | } 22 | 23 | export default SingleCategory 24 | -------------------------------------------------------------------------------- /src/components/Cookies/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CookieConsent from 'react-cookie-consent' 3 | 4 | interface CookiesConsentProps {} 5 | 6 | const CookiesConsent: React.FC = () => { 7 | return ( 8 | <> 9 | 27 | Want cookies? 28 | 29 | 30 | ) 31 | } 32 | 33 | export default CookiesConsent 34 | -------------------------------------------------------------------------------- /src/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as FooterStyles from './styled' 3 | 4 | const Footer = () => { 5 | return ( 6 | 7 | Made with Next.js by Paju Studios 8 | 9 | ) 10 | } 11 | 12 | export default Footer 13 | -------------------------------------------------------------------------------- /src/components/Footer/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Container = styled.section` 4 | height: 80px; 5 | width: 100%; 6 | justify-content: center; 7 | align-items: center; 8 | display: flex; 9 | background: #f0f0f0; 10 | ` 11 | export const Copyright = styled.p` 12 | font-size: calc(1rem + 0.1vw); 13 | font-weight: 200; 14 | letter-spacing: 1px; 15 | ` 16 | -------------------------------------------------------------------------------- /src/components/Hero/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as HeroStyles from './styled' 3 | 4 | const Hero: React.FC = () => { 5 | return ( 6 | 7 | 8 | Welcome 9 | 10 | This is your unofficial WooCommerce Storefront theme made with Next.js 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | export default Hero 18 | -------------------------------------------------------------------------------- /src/components/Hero/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | padding: 5rem 0.75rem 0 0.75rem; 7 | max-width: 1200px; 8 | margin: 0 auto; 9 | ` 10 | 11 | export const Bg = styled.div` 12 | position: relative; 13 | width: 100%; 14 | height: 50vh; 15 | 16 | background: url('./hero.jpg'); 17 | background-position: center; 18 | background-size: cover; 19 | //border-radius: 30px; 20 | ` 21 | 22 | export const Heading = styled.h1` 23 | font-size: calc(2.5rem + 0.6vw); 24 | position: absolute; 25 | letter-spacing: 1px; 26 | font-weight: 200; 27 | top: 40%; 28 | left: 50%; 29 | transform: translate(-50%, -50%); 30 | ` 31 | 32 | export const Subheading = styled.h2` 33 | text-align: center; 34 | padding-top: 1rem; 35 | font-size: calc(0.75rem + 0.6vw); 36 | position: absolute; 37 | letter-spacing: 1px; 38 | font-weight: 200; 39 | top: 60%; 40 | left: 50%; 41 | transform: translate(-50%, -50%); 42 | ` 43 | -------------------------------------------------------------------------------- /src/components/NavIcons/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo } from 'react' 2 | import * as NavIconStyles from './styled' 3 | import Link from 'next/link' 4 | import { useRouter } from 'next/router' 5 | import { CartContext } from '../../context/cart' 6 | import CartTotal from '../Cart/CartTotal' 7 | import { useSession } from 'next-auth/react' 8 | 9 | interface NavigationIconsProps { 10 | scrollNav: boolean 11 | isMobile: boolean 12 | } 13 | 14 | const NavigationIcons: React.FC = ({ scrollNav, isMobile }) => { 15 | const [cart] = useContext(CartContext) 16 | const router = useRouter() 17 | const { data: session } = useSession() 18 | 19 | const totalQuantity = useMemo(() => { 20 | if (cart.items.length > 0) { 21 | return cart.items.reduce( 22 | (acc: number, curr: { [key: string]: number }) => acc + curr.quantity, 23 | 0, 24 | ) 25 | } else { 26 | return 0 27 | } 28 | }, [cart]) 29 | 30 | return ( 31 | 32 | 0 ? true : false}> 33 | Total: 34 | 35 | 36 | 37 | 38 | 39 | 0 ? true : false}> 40 | {totalQuantity} 41 | 42 | 43 | 44 | 45 | 46 | router.push(session ? '/account' : '/login')} /> 47 | 48 | ) 49 | } 50 | 51 | export default React.memo(NavigationIcons) 52 | -------------------------------------------------------------------------------- /src/components/NavIcons/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { RiShoppingCart2Fill } from 'react-icons/ri' 3 | import { MdAccountCircle } from 'react-icons/md' 4 | 5 | export const Total = styled.span<{ hasItems: boolean }>` 6 | display: ${({ hasItems }) => (hasItems ? 'flex' : 'none')}; 7 | flex-direction: row; 8 | letter-spacing: 1.1px; 9 | font-weight: 400; 10 | ` 11 | 12 | export const CartIconWrapper = styled.div` 13 | position: relative; 14 | ` 15 | 16 | export const CartBadge = styled.button<{ hasItems: boolean }>` 17 | display: ${({ hasItems }) => (hasItems ? '' : 'none')}; 18 | background: red; 19 | border-radius: 50%; 20 | color: #fff; 21 | font-weight: bold; 22 | outline: none; 23 | border: none; 24 | position: absolute; 25 | left: 25px; 26 | z-index: 2; 27 | width: 30px; 28 | height: 30px; 29 | ` 30 | export const CartIcon = styled(RiShoppingCart2Fill)` 31 | font-size: 2rem; 32 | cursor: pointer; 33 | margin: 0 0.5rem; 34 | ` 35 | export const AccIcon = styled(MdAccountCircle)` 36 | font-size: 2rem; 37 | cursor: pointer; 38 | margin: 0 0.5rem; 39 | ` 40 | 41 | export const IconHolder = styled.div<{ scrollNav: boolean; isMobile: boolean }>` 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | flex-direction: row; 46 | padding: 0 1rem; 47 | @media screen and (max-width: 768px) { 48 | display: ${({ isMobile }) => (isMobile ? 'flex' : 'none')}; 49 | } 50 | ${Total} { 51 | color: ${({ scrollNav, theme }) => (scrollNav ? theme.primaryText : theme.primaryWhite)}; 52 | font-size: ${({ isMobile }) => (isMobile ? '1.2rem' : '0.9rem')}; 53 | } 54 | 55 | ${CartIcon} { 56 | color: ${({ scrollNav, theme }) => (scrollNav ? theme.primaryText : theme.primaryWhite)}; 57 | margin: ${({ isMobile }) => (isMobile ? '0.5rem' : '0 0.5rem')}; 58 | } 59 | 60 | ${CartBadge} { 61 | top: ${({ isMobile }) => (isMobile ? '-5px' : '-15px')}; 62 | } 63 | ${AccIcon} { 64 | color: ${({ scrollNav, theme }) => (scrollNav ? theme.primaryText : theme.primaryWhite)}; 65 | } 66 | ` 67 | -------------------------------------------------------------------------------- /src/components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import * as NavbarStyles from './styled' 3 | import { FaBars } from 'react-icons/fa' 4 | import Link from 'next/link' 5 | import NavigationIcons from '../NavIcons' 6 | 7 | interface NavbarProps { 8 | toggle: () => void 9 | } 10 | 11 | const Navbar: React.FC = ({ toggle }) => { 12 | const [scrollNav, setScrollNav] = useState(true) 13 | 14 | const changeNav = () => { 15 | if (window.scrollY < 40) { 16 | setScrollNav(true) 17 | } else { 18 | setScrollNav(false) 19 | } 20 | } 21 | 22 | useEffect(() => { 23 | changeNav() 24 | window.addEventListener('scroll', changeNav) 25 | return () => window.removeEventListener('scroll', changeNav) 26 | }, [scrollNav]) 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | Logo.lt 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Home 43 | 44 | 45 | 46 | 47 | Shop 48 | 49 | 50 | 51 | 52 | About 53 | 54 | 55 | 56 | 57 | Contact 58 | 59 | 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | 67 | export default React.memo(Navbar) 68 | -------------------------------------------------------------------------------- /src/components/Navbar/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const LinkText = styled.a` 4 | cursor: pointer; 5 | font-size: 1.1rem; 6 | text-decoration: none; 7 | 8 | font-weight: 600; 9 | letter-spacing: 1px; 10 | ` 11 | 12 | export const LogoWrapper = styled.div` 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | padding: 1rem; 17 | ` 18 | 19 | export const LogoText = styled.a` 20 | color: ${({ theme }) => theme.primaryPurple}; 21 | cursor: pointer; 22 | font-size: calc(1.8rem + 0.1vw); 23 | letter-spacing: 2px; 24 | opacity: 0.95; 25 | 26 | font-weight: 600; 27 | ` 28 | 29 | export const MobileIcon = styled.div` 30 | display: none; 31 | @media screen and (max-width: 768px) { 32 | display: block; 33 | position: absolute; 34 | opacity: 0.8; 35 | top: 1.2rem; 36 | right: 1.2rem; 37 | 38 | font-size: 2rem; //transform: translate(-100%, 60%); 39 | cursor: pointer; 40 | } 41 | ` 42 | 43 | export const Menu = styled.ul` 44 | display: flex; 45 | justify-content: center; 46 | align-items: center; 47 | list-style: none; 48 | position: absolute; 49 | top: 50%; 50 | left: 50%; 51 | transform: translate(-50%, -50%); 52 | 53 | @media screen and (max-width: 768px) { 54 | display: none; 55 | } 56 | ` 57 | 58 | export const Item = styled.li` 59 | height: 80px; 60 | display: flex; 61 | align-items: center; 62 | //padding: 0 0.8rem; 63 | margin: 0 0.8rem; 64 | ` 65 | 66 | export const BtnWrapper = styled.nav` 67 | display: flex; 68 | align-items: center; 69 | 70 | @media screen and (max-width: 768px) { 71 | display: none; 72 | } 73 | ` 74 | 75 | export const Container = styled.div` 76 | display: flex; 77 | justify-content: space-between; 78 | height: 80px; 79 | z-index: 1; 80 | width: 95%; 81 | max-width: 1200px; 82 | position: relative; 83 | ` 84 | export const Nav = styled.nav<{ scrollNav: boolean }>` 85 | background: ${({ scrollNav, theme }) => (scrollNav ? 'transparent' : theme.primaryBlack)}; 86 | transition: all 0.2s ease-in; 87 | height: 80px; 88 | width: 100%; 89 | 90 | //padding-top: ${({ scrollNav }) => (scrollNav ? '40px' : '0px')}; 91 | margin-top: -80px; 92 | display: flex; 93 | justify-content: space-evenly; 94 | align-items: center; 95 | position: sticky; 96 | top: 0; 97 | bottom: 0; 98 | z-index: 99; 99 | 100 | ${LinkText} { 101 | color: ${({ scrollNav, theme }) => (scrollNav ? theme.primaryBlack : theme.primaryWhite)}; 102 | 103 | &:hover { 104 | transition: all 0.2s ease-in-out; 105 | color: ${({ theme }) => theme.primaryPurple}; 106 | } 107 | } 108 | 109 | ${MobileIcon} { 110 | color: ${({ scrollNav, theme }) => (scrollNav ? theme.primaryText : theme.primaryWhite)}; 111 | } 112 | ` 113 | -------------------------------------------------------------------------------- /src/components/OrderSummary/index.tsx: -------------------------------------------------------------------------------- 1 | import * as OrderSummaryStyles from './styled' 2 | import React, { useState } from 'react' 3 | import { Cart, CartItem } from '../../types' 4 | import useSWR from 'swr' 5 | import { Loader } from '../../styles/utils' 6 | import CartTotal from '../Cart/CartTotal' 7 | 8 | interface OrderSummaryProps { 9 | register: any 10 | errors: any 11 | cart: Cart 12 | } 13 | 14 | const OrderSummary: React.FC = ({ register, errors, cart }) => { 15 | const { data } = useSWR('/api/shipping/retrieve') 16 | const [shippingCost, setShippingCost] = useState(0) 17 | 18 | if (!data) { 19 | return 20 | } 21 | 22 | return ( 23 | 24 | 25 | Product 26 | Subtotal 27 | {cart.items.length > 0 && 28 | cart.items.map((item: CartItem) => ( 29 | 30 | 31 | {item.product_name} x {item.quantity} 32 | 33 | 34 | ${item.line_total!.toFixed(2)} 35 | 36 | 37 | ))} 38 | Subtotal 39 | 40 | 41 | 42 | 43 | Shipping 44 | 45 | 46 | {data?.map((shipping: any) => { 47 | const decodedCost = JSON.parse(window.atob(shipping.cost.split('.')[1])) 48 | return ( 49 | 50 | { 53 | setShippingCost(decodedCost) 54 | }} 55 | type="radio" 56 | name="shipping" 57 | value={JSON.stringify({ 58 | cost: shipping.cost, 59 | method_id: shipping.method, 60 | method_title: shipping.title, 61 | })} 62 | /> 63 | 64 | 65 | {shipping.title} {decodedCost > 0 ? ' - $' + decodedCost : ' - Free'} 66 | 67 | 68 | ) 69 | })} 70 | 71 | 72 | 73 | Total 74 | 75 | 76 | 77 | 78 | {errors.shipping ? ( 79 | Please select a shipping method 80 | ) : ( 81 | '' 82 | )} 83 | 84 | ) 85 | } 86 | 87 | export default OrderSummary 88 | -------------------------------------------------------------------------------- /src/components/OrderSummary/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | display: flex; 5 | margin: 2rem 0; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | background: #fafafa; 10 | width: 100%; 11 | ` 12 | 13 | export const Grid = styled.ul` 14 | display: grid; 15 | grid-template-columns: repeat(2, 1fr); 16 | width: 100%; 17 | ` 18 | 19 | export const DescriptionTall = styled.li` 20 | background: #f0f0f0; 21 | letter-spacing: 2px; 22 | padding: 0.25rem 0.5rem; 23 | font-size: calc(1rem + 0.1vw); 24 | font-weight: bolder; 25 | color: ${({ theme }) => theme.primaryText}; 26 | 27 | text-align: center; 28 | display: flex; 29 | justify-content: center; 30 | align-items: center; 31 | width: 100%; 32 | height: 75px; 33 | ` 34 | 35 | export const DescriptionWhite = styled.li<{ shippingOptions: boolean }>` 36 | background: #fff; 37 | letter-spacing: 2px; 38 | padding: 0.25rem 0.5rem; 39 | font-size: ${({ shippingOptions }) => 40 | shippingOptions ? `calc(0.8rem + 0.1vw)` : `calc(1rem + 0.1vw)`}; 41 | font-weight: bolder; 42 | color: ${({ theme }) => theme.primaryText}; 43 | //opacity: 0.9; 44 | text-align: center; 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | width: 100%; 49 | height: 75px; 50 | ` 51 | export const ItemTotal = styled.li` 52 | padding: 0.25rem 0.5rem; 53 | justify-content: center; 54 | display: flex; 55 | align-items: center; 56 | text-align: center; 57 | max-width: 160px; 58 | margin: 0 auto; 59 | font-size: calc(0.9rem + 0.1vw); 60 | color: ${({ theme }) => theme.primaryText}; 61 | text-align: center; 62 | display: flex; 63 | justify-content: center; 64 | align-items: center; 65 | width: 100%; 66 | height: 75px; 67 | ` 68 | 69 | export const ItemName = styled.li` 70 | padding: 0.25rem 0.5rem; 71 | font-weight: 500; 72 | justify-content: center; 73 | display: flex; 74 | align-items: center; 75 | text-align: center; 76 | max-width: 160px; 77 | margin: 0 auto; 78 | font-size: calc(0.9rem + 0.1vw); 79 | white-space: nowrap; 80 | color: ${({ theme }) => theme.primaryText}; 81 | 82 | text-align: center; 83 | display: flex; 84 | justify-content: center; 85 | align-items: center; 86 | width: 100%; 87 | height: 75px; 88 | ` 89 | 90 | export const DescriptionLow = styled.li` 91 | background: #f0f0f0; 92 | letter-spacing: 2px; 93 | padding: 0.25rem 0.5rem; 94 | font-size: calc(1rem + 0.1vw); 95 | font-weight: bolder; 96 | color: ${({ theme }) => theme.primaryText}; 97 | text-align: center; 98 | display: flex; 99 | justify-content: center; 100 | align-items: center; 101 | width: 100%; 102 | ` 103 | 104 | export const ShippingWrapper = styled.li` 105 | display: flex; 106 | align-items: center; 107 | flex-direction: column; 108 | background: #fff; 109 | width: 100%; 110 | margin: 0 auto; 111 | ` 112 | 113 | export const Values = styled.div` 114 | display: flex; 115 | background: #fff; 116 | flex-direction: column; 117 | ` 118 | 119 | export const Method = styled.div` 120 | display: flex; 121 | flex-direction: row; 122 | background: #fff; 123 | align-items: center; 124 | ` 125 | 126 | export const Label = styled.label` 127 | background: #fff; 128 | letter-spacing: 2px; 129 | padding: 0.2rem 0.25rem; 130 | text-align: center; 131 | color: ${({ theme }) => theme.primaryText}; 132 | ` 133 | 134 | export const Error = styled.span` 135 | margin: 0.1rem 1rem; 136 | padding: 0.5rem; 137 | color: red; 138 | ` 139 | 140 | export const Radio = styled.input` 141 | &:after { 142 | width: 15px; 143 | height: 15px; 144 | top: -2px; 145 | left: -1px; 146 | border-radius: 15px; 147 | position: relative; 148 | background-color: #d1d3d1; 149 | content: ''; 150 | display: inline-block; 151 | visibility: visible; 152 | border: 2px solid white; 153 | } 154 | 155 | &:checked:after { 156 | width: 15px; 157 | height: 15px; 158 | top: -2px; 159 | left: -1px; 160 | border-radius: 15px; 161 | position: relative; 162 | background-color: ${({ theme }) => theme.primaryPurple}; 163 | content: ''; 164 | display: inline-block; 165 | visibility: visible; 166 | border: 2px solid white; 167 | } 168 | ` 169 | -------------------------------------------------------------------------------- /src/components/PageTitle/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | 4 | interface CustomHeadProps { 5 | title: string 6 | description: string 7 | } 8 | 9 | const PageTitle: React.FC = ({ title, description }) => { 10 | return ( 11 | 12 | {title} 13 | 14 | 15 | ) 16 | } 17 | 18 | export default PageTitle 19 | -------------------------------------------------------------------------------- /src/components/Product/AddToCartForm/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef } from 'react' 2 | import { Loader } from '../../../styles/utils' 3 | import { CartContext } from '../../../context/cart' 4 | import { Product } from '../../../types' 5 | import * as AddToCartFormStyles from './styled' 6 | import { updateCart } from '../../../utils/functions' 7 | 8 | interface UpdateCartButtonProps { 9 | product: Product 10 | } 11 | 12 | const AddToCartForm: React.FC = ({ product }) => { 13 | const [cart, setCart, isUpdating, setIsUpdating] = useContext(CartContext) 14 | const quantityRef = useRef(null) 15 | 16 | const handleAddToCart = async (e: React.SyntheticEvent, item: Product, quantity: number) => { 17 | e.preventDefault() 18 | //lazy form validation :) 19 | quantity = quantity > 0 ? quantity : 1 20 | 21 | setIsUpdating(true) 22 | 23 | try { 24 | const res = await fetch( 25 | `${process.env.NEXT_PUBLIC_WP_API_URL}/wp-json/cocart/v1/add-item?cart_key=${cart.key}`, 26 | { 27 | method: 'POST', 28 | body: JSON.stringify({ 29 | product_id: String(item.id), 30 | quantity: quantity, 31 | return_cart: true, 32 | //adding image for cart page 33 | cart_item_data: { img: item.images[0].src, slug: item.slug }, 34 | }), 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | }, 38 | }, 39 | ) 40 | if (res.status !== 200) throw Error('Problem with remote cart') 41 | const data = await res.json() 42 | 43 | setCart(() => updateCart(cart, data)) 44 | setIsUpdating(false) 45 | } catch (error) { 46 | console.error(error) 47 | setIsUpdating(false) 48 | } 49 | } 50 | 51 | return ( 52 | 53 | 59 | 60 | handleAddToCart(e, product, parseInt(quantityRef.current.value))} 63 | > 64 | {isUpdating ? : 'Add To Cart'} 65 | 66 | 67 | ) 68 | } 69 | 70 | export default AddToCartForm 71 | -------------------------------------------------------------------------------- /src/components/Product/AddToCartForm/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Form = styled.form` 4 | margin: 2rem 0; 5 | display: flex; 6 | flex-direction: row; 7 | ` 8 | 9 | export const InputField = styled.input` 10 | padding: 0.5em; 11 | margin-right: 1rem; 12 | max-width: 60px; 13 | font-size: 1rem; 14 | text-align: center; 15 | background-color: #f2f2f2; 16 | color: #43454b; 17 | border: none; 18 | font-weight: 400; 19 | 20 | &[type='number']::-webkit-inner-spin-button { 21 | opacity: 1; 22 | } 23 | ` 24 | 25 | export const Btn = styled.button` 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | cursor: pointer; 30 | opacity: 1; 31 | padding: 0.5em 1.5em; 32 | text-decoration: none; 33 | font-weight: 600; 34 | text-shadow: none; 35 | 36 | transition: all 0.2s ease-in-out; 37 | background: #333333; 38 | border-color: #333333; 39 | color: #fff; 40 | width: calc(120px + 0.2vw); 41 | height: 41px; 42 | //display: inline-block; 43 | transition: all 0.2s ease-in-out; 44 | ` 45 | 46 | export const Text = styled.p` 47 | font-weight: 600; 48 | font-size: calc(0.75rem + 0.1vw); 49 | ` 50 | -------------------------------------------------------------------------------- /src/components/Product/ProductCard/index.tsx: -------------------------------------------------------------------------------- 1 | import * as ProductCardStyles from './styled' 2 | 3 | import Link from 'next/link' 4 | import { Product } from '../../../types' 5 | import React from 'react' 6 | import ProductPrice from '../ProductPrice' 7 | 8 | interface ProductItemProps { 9 | product: Product 10 | } 11 | 12 | const SingleProduct: React.FC = ({ product }) => { 13 | return ( 14 | 15 | 16 | 17 | {product.images && product.images.length > 0 ? ( 18 | 19 | ) : null} 20 | 21 | 22 | {product.name} 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default SingleProduct 31 | -------------------------------------------------------------------------------- /src/components/Product/ProductCard/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const PriceWrapper = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | width: 100%; 8 | margin: 0 auto; 9 | ` 10 | 11 | export const RegularPrice = styled.p<{ isOnSale: boolean }>` 12 | color: ${({ theme }) => theme.primaryText}; 13 | text-decoration: ${({ isOnSale }) => (isOnSale ? `line-through` : `none`)}; 14 | font-weight: bolder; 15 | font-size: ${({ isOnSale }) => (isOnSale ? `calc(0.8rem + 0.1vw)` : `calc(1rem + 0.1vw)`)}; 16 | margin: 0 0.25rem; 17 | opacity: ${({ isOnSale }) => (isOnSale ? `0.5` : `0.9`)}; 18 | ` 19 | export const SalePrice = styled.p` 20 | color: ${({ theme }) => theme.primaryText}; 21 | font-weight: bold; 22 | font-size: calc(1rem + 0.1vw); 23 | margin: 0 0.25rem; 24 | opacity: 0.9; 25 | ` 26 | 27 | export const Name = styled.p` 28 | font-size: calc(1rem + 0.2vw); 29 | padding-bottom: 0.25rem; 30 | align-self: auto; 31 | letter-spacing: 0.5px; 32 | transition: all 0.1s ease-in-out; 33 | color: ${({ theme }) => theme.primaryText}; 34 | margin: 0 1rem; 35 | ` 36 | 37 | export const Img = styled.img` 38 | width: 100%; 39 | height: 100%; 40 | object-fit: cover; 41 | transition: transform 0.25s, visibility 0.25s ease-in; 42 | ` 43 | export const ImgWrapper = styled.div` 44 | height: 300px; 45 | width: 100%; 46 | overflow: hidden; 47 | transform-origin: 0 0; 48 | 49 | &:hover { 50 | ${Img} { 51 | transform: scale(1.1); 52 | } 53 | } 54 | ` 55 | 56 | export const Wrapper = styled.div` 57 | display: flex; 58 | padding: 0.75rem; 59 | justify-content: center; 60 | align-items: center; 61 | flex-direction: column; 62 | 63 | &:hover { 64 | cursor: pointer; 65 | 66 | ${Name} { 67 | color: ${({ theme }) => theme.primaryPurple}; 68 | } 69 | } 70 | ` 71 | -------------------------------------------------------------------------------- /src/components/Product/ProductPrice/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import * as ProductPriceStyles from './styled' 3 | import useSwr from 'swr' 4 | import { Product } from '../../../types' 5 | import { Loader } from '../../../styles/utils' 6 | import { getSingleProduct } from '../../../utils/functions' 7 | 8 | interface ProductPriceProps { 9 | product: Product 10 | center: boolean 11 | size: number 12 | } 13 | 14 | const ProductPrice: FC = ({ product, center, size }) => { 15 | const { data } = useSwr(`/api/products/retrieve`) 16 | 17 | if (!data) { 18 | return 19 | } 20 | 21 | const { sale_price, regular_price } = getSingleProduct(product.id, data) 22 | 23 | return ( 24 | 25 | {!sale_price ? ( 26 | 27 | ${parseFloat(regular_price).toFixed(2)} 28 | 29 | ) : ( 30 | <> 31 | 32 | ${parseFloat(regular_price).toFixed(2)} 33 | 34 | 35 | ${parseFloat(sale_price).toFixed(2)} 36 | 37 | 38 | )} 39 | 40 | ) 41 | } 42 | 43 | export default ProductPrice 44 | -------------------------------------------------------------------------------- /src/components/Product/ProductPrice/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div<{ center: boolean }>` 4 | display: flex; 5 | align-items: center; 6 | justify-content: ${({ center }) => (center ? 'center' : 'start')}; 7 | width: 100%; 8 | margin: 0 auto; 9 | ` 10 | 11 | export const Sale = styled.p<{ size: number }>` 12 | color: ${({ theme }) => theme.primaryText}; 13 | font-weight: bold; 14 | font-size: ${({ size }) => `calc(${size}rem + 0.1vw)`}; 15 | margin: 0 0.25rem; 16 | opacity: 0.9; 17 | ` 18 | 19 | export const Regular = styled.p<{ isOnSale: boolean; size: number }>` 20 | color: ${({ theme }) => theme.primaryText}; 21 | text-decoration: ${({ isOnSale }) => (isOnSale ? `line-through` : `none`)}; 22 | font-weight: bolder; 23 | font-size: ${({ isOnSale, size }) => 24 | isOnSale ? `calc(${size - 0.2}rem + 0.1vw)` : `calc(${size}rem + 0.1vw)`}; 25 | margin: 0 0.25rem; 26 | opacity: ${({ isOnSale }) => (isOnSale ? `0.5` : `0.9`)}; 27 | ` 28 | -------------------------------------------------------------------------------- /src/components/Seo/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import React from 'react' 3 | 4 | interface CustomHeadProps {} 5 | 6 | const baseInfo = { 7 | author: 'Paju', 8 | titlePrefix: 'Paju Studios', 9 | name: 'pajustudios.eu', 10 | url: 'https://wwww.pajustudios.eu', 11 | description: 'Eu design studios', 12 | keywords: `Design, Web Development`, 13 | } 14 | 15 | const Seo: React.FC = () => { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default Seo 31 | -------------------------------------------------------------------------------- /src/components/Sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as SidebarStyles from './styled' 2 | 3 | import Link from 'next/link' 4 | import NavigationIcons from '../NavIcons' 5 | import React from 'react' 6 | 7 | interface SidebarProps { 8 | toggle: () => void 9 | isOpen: boolean 10 | } 11 | 12 | const Sidebar: React.FC = ({ toggle, isOpen }) => { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Home 23 | 24 | 25 | 26 | 27 | Shop 28 | 29 | 30 | 31 | 32 | About 33 | 34 | 35 | 36 | 37 | Contact 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ) 47 | } 48 | 49 | export default Sidebar 50 | -------------------------------------------------------------------------------- /src/components/Sidebar/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { FaTimes } from 'react-icons/fa' 3 | 4 | export const Container = styled.aside<{ isOpen: boolean }>` 5 | position: fixed; 6 | z-index: 999; 7 | overflow: hidden; 8 | width: 100%; 9 | height: 100%; 10 | background: #0d0d0d; 11 | display: grid; 12 | align-items: center; 13 | top: 0; 14 | left: 0; 15 | transition: 0.3s ease-in-out; 16 | opacity: ${({ isOpen }) => (isOpen ? '100%' : '0')}; 17 | top: ${({ isOpen }) => (isOpen ? '0' : '-100%')}; 18 | ` 19 | 20 | export const ClosedIcon = styled(FaTimes)` 21 | color: #ffffff; 22 | ` 23 | 24 | export const Icon = styled.div` 25 | position: absolute; 26 | top: 1rem; 27 | right: 2rem; 28 | background: transparent; 29 | font-size: 2.3rem; 30 | cursor: pointer; 31 | outline: none; 32 | ` 33 | 34 | export const Wrapper = styled.div` 35 | margin-top: 3rem; 36 | color: #fff; 37 | position: relative; 38 | ` 39 | 40 | export const SideBtnWrap = styled.div` 41 | display: flex; 42 | justify-content: center; 43 | ` 44 | 45 | export const Route = styled.button` 46 | border-radius: 50px; 47 | background: ${({ theme }) => theme.primaryRed}; 48 | white-space: nowrap; 49 | padding: 16px 64px; 50 | color: #010606; 51 | font-size: 16px; 52 | font-weight: 600; 53 | outline: none; 54 | border: none; 55 | cursor: pointer; 56 | transition: all 0.2s ease-in-out; 57 | text-decoration: none; 58 | &:hover { 59 | transition: all 0.2s ease-in-out; 60 | background: #fff; 61 | color: #010606; 62 | } 63 | ` 64 | 65 | export const Menu = styled.ul` 66 | display: grid; 67 | padding: 0 40px; 68 | grid-template-columns: 1fr; 69 | grid-template-rows: repeat(6, 80px); 70 | text-align: center; 71 | @media screen and (max-width: 480px) { 72 | grid-template-rows: repeat (6, 60px); 73 | } 74 | ` 75 | 76 | export const LinkWrapper = styled.button` 77 | outline: none; 78 | border: none; 79 | background: #0d0d0d; //change color 80 | display: flex; 81 | align-items: center; 82 | justify-content: center; 83 | ` 84 | export const LinkText = styled.a` 85 | font-size: 1.5rem; 86 | letter-spacing: 1.1px; 87 | text-decoration: none; 88 | list-style: none; 89 | 90 | transition: 0.2s ease-in-out; 91 | color: #fff; 92 | cursor: pointer; 93 | &:hover { 94 | color: ${({ theme }) => theme.primaryPurple}; 95 | transition: 0.2s ease-in-out; 96 | } 97 | ` 98 | export const IconHolder = styled.div` 99 | position: absolute; 100 | display: flex; 101 | justify-content: center; 102 | align-items: center; 103 | top: 15%; 104 | left: 50%; 105 | border-bottom: 1px solid #fff; 106 | transform: translate(-50%, -50%); 107 | ` 108 | -------------------------------------------------------------------------------- /src/components/StripePayment/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CardElement } from '@stripe/react-stripe-js' 3 | import * as StripePaymentStyles from './styled' 4 | 5 | interface PaymentFormContentProps {} 6 | 7 | const StripePayment: React.FC = () => { 8 | const iframeStyles = { 9 | base: { 10 | color: '#4c4e4', 11 | fontSize: '18px', 12 | iconColor: '#000', 13 | '::placeholder': { 14 | color: '#b3beca', 15 | }, 16 | ':focus': { 17 | iconColor: '#96588a', 18 | }, 19 | }, 20 | invalid: { 21 | iconColor: '#e02333', 22 | color: '#e02333', 23 | }, 24 | complete: { 25 | iconColor: '#000', 26 | }, 27 | } 28 | 29 | const cardElementOpts = { 30 | style: iframeStyles, 31 | hidePostalCode: true, 32 | } 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Fill in the card details. Use 4242 4242 4242 4242 for testing. 50 | 51 | 52 | ) 53 | } 54 | export default StripePayment 55 | -------------------------------------------------------------------------------- /src/components/StripePayment/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | width: 100%; 9 | ` 10 | 11 | export const Wrapper = styled.div` 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | width: 80%; 17 | margin: 1rem; 18 | ` 19 | export const Info = styled.p` 20 | font-weight: 400; 21 | 22 | max-width: 340px; 23 | font-size: calc(0.9rem + 0.1vw); 24 | letter-spacing: 0.5px; 25 | padding: 1rem; 26 | 27 | color: ${({ theme }) => theme.primaryText}; 28 | ` 29 | 30 | export const CardElementWrapper = styled.div` 31 | height: 50px; 32 | margin: 1rem; 33 | background: #fff; 34 | border-radius: 5px; 35 | max-width: 320px; 36 | width: 100%; 37 | display: flex; 38 | align-items: center; 39 | 40 | & .StripeElement { 41 | width: 100%; 42 | padding: 15px; 43 | } 44 | ` 45 | 46 | export const ImgHolder = styled.span` 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | ` 51 | export const CreditImg = styled.img` 52 | width: 20%; 53 | height: auto; 54 | padding: 0.5rem; 55 | ` 56 | -------------------------------------------------------------------------------- /src/containers/Account/index.tsx: -------------------------------------------------------------------------------- 1 | import * as AccountPageStyles from './styled' 2 | import { useSession } from 'next-auth/react' 3 | import React from 'react' 4 | import { BasicContainer } from '../../styles/utils' 5 | import AccountGrid from '../../components/Account/Grid' 6 | 7 | const AccountPageContainer: React.FC = () => { 8 | const { data: session }: any = useSession() 9 | 10 | return ( 11 | 12 | 13 | {session ? ( 14 | 15 | ) : ( 16 | Please login or register first 17 | )} 18 | 19 | 20 | ) 21 | } 22 | 23 | export default AccountPageContainer 24 | -------------------------------------------------------------------------------- /src/containers/Account/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | margin: 5rem 0 0 0; 5 | //min-height: 50vh; 6 | ` 7 | export const Info = styled.p` 8 | display: flex; 9 | justify-content: center; 10 | font-size: calc(2rem + 0.1vw); 11 | font-weight: 200; 12 | text-align: center; 13 | padding: 1rem; 14 | margin: 1rem; 15 | ` 16 | -------------------------------------------------------------------------------- /src/containers/Cart/index.tsx: -------------------------------------------------------------------------------- 1 | import { BasicContainer, Loader, SectionTitle } from '../../styles/utils' 2 | import * as CartPageStyles from './styled' 3 | import React, { useContext } from 'react' 4 | import { CartContext } from '../../context/cart' 5 | import { NextPage } from 'next' 6 | import CartGrid from '../../components/Cart/CartGrid' 7 | import Link from 'next/link' 8 | import useSWR from 'swr' 9 | interface CartPageProps {} 10 | 11 | const CartPageContainer: NextPage = () => { 12 | const [cart] = useContext(CartContext) 13 | const { data } = useSWR('/api/products/retrieve') 14 | 15 | if (!data) { 16 | return 17 | } 18 | 19 | return ( 20 | 21 | 22 | {cart.items.length > 0 ? ( 23 | <> 24 | Cart 25 | 26 | 27 | Proceed to Checkout 28 | 29 | 30 | ) : ( 31 | Your cart is empty 32 | )} 33 | 34 | 35 | ) 36 | } 37 | 38 | export default CartPageContainer 39 | -------------------------------------------------------------------------------- /src/containers/Cart/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | flex-direction: column; 8 | max-width: 840px; 9 | margin: 0 auto; 10 | width: 100%; 11 | min-height: 50vh; 12 | ` 13 | 14 | export const EmptyCart = styled.h2` 15 | font-size: calc(2rem + 0.1vw); 16 | font-weight: 200; 17 | letter-spacing: 1px; 18 | ` 19 | 20 | export const CheckoutBtn = styled.button` 21 | margin-top: 3rem; 22 | font-size: calc(1.2rem + 0.1vw); 23 | background-color: #333333; 24 | border-color: #333333; 25 | color: #ffffff; 26 | cursor: pointer; 27 | padding: 0.5em 1.5em; 28 | text-decoration: none; 29 | font-weight: 600; 30 | display: inline-block; 31 | transition: all 0.2s ease-in-out; 32 | ` 33 | -------------------------------------------------------------------------------- /src/containers/Checkout/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from 'react' 2 | import { useStripe, useElements } from '@stripe/react-stripe-js' 3 | import { useForm } from 'react-hook-form' 4 | import * as CheckoutPageStyles from './styled' 5 | import AddressForm from '../../components/AddressForm' 6 | import StripePayment from '../../components/StripePayment' 7 | import OrderSummary from '../../components/OrderSummary' 8 | import { BasicContainer, Loader, Subtitle } from '../../styles/utils' 9 | import { CartContext } from '../../context/cart' 10 | import { NextPage } from 'next' 11 | import { createOrder, initCart } from '../../utils/functions' 12 | import { Customer } from '../../types' 13 | 14 | interface CheckoutPageContainerProps {} 15 | 16 | const CheckoutPageContainer: NextPage = () => { 17 | const [chosenPaymentMethod, setChosenPaymentMethod] = useState('stripe') 18 | const [cart, setCart] = useContext(CartContext) 19 | const { register, handleSubmit, errors } = useForm() 20 | const [isProcessing, setIsProcessing] = useState(false) 21 | const [serverMsg, setServerMsg] = useState('') 22 | const stripe = useStripe() 23 | const elements = useElements() 24 | 25 | const onSubmit = async (customer: Customer) => { 26 | try { 27 | if (!cart || cart.items.length === 0 || !stripe || !elements) return 28 | 29 | setIsProcessing(true) 30 | let payment: any 31 | 32 | //TODO: Add more payment methods (Paypal e.g) 33 | 34 | if (chosenPaymentMethod === 'stripe') { 35 | const card = elements.getElement('card') 36 | if (!card) throw 'Stripe error' 37 | 38 | const stripeRes = await stripe.createPaymentMethod({ 39 | type: 'card', 40 | card, 41 | }) 42 | 43 | payment = stripeRes.paymentMethod?.id 44 | } 45 | 46 | // end of stripe block 47 | 48 | if (!payment) throw 'No valid payment method' 49 | const { message } = await createOrder(customer, payment, cart) 50 | const newCart = await initCart() 51 | if (message === 'Success') { 52 | setCart(newCart) 53 | setServerMsg('Thank you for your order. Check your email for details!') 54 | } else { 55 | setServerMsg('Sorry something went wrong. Please try again later...') 56 | } 57 | setIsProcessing(false) 58 | } catch (error) { 59 | console.log(error) 60 | setServerMsg('Sorry something went wrong. Please try again later...') 61 | setIsProcessing(false) 62 | } 63 | } 64 | 65 | return ( 66 | 67 | 68 | 69 | Billing details 70 | 71 | 72 | 73 | Your order 74 | 75 | 76 | 77 | Pay with credit card 78 | 79 | 80 | 81 | 82 | Your personal data will be used to process your order, support your experience 83 | throughout this website, and for other purposes described in our privacy policy. 84 | 85 | 86 | {isProcessing ? : 'Place Order'} 87 | 88 | {serverMsg} 89 | 90 | 91 | 92 | ) 93 | } 94 | 95 | export default CheckoutPageContainer 96 | -------------------------------------------------------------------------------- /src/containers/Checkout/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.form` 4 | margin-top: 1rem; 5 | display: grid; 6 | grid-gap: 2rem; 7 | max-width: 1000px; 8 | margin: 0 auto; 9 | grid-template-columns: 1fr 1fr; 10 | position: relative; 11 | grid-template-areas: 12 | 'address order' 13 | 'address payment' 14 | 'address checkout'; 15 | 16 | @media screen and (max-width: 992px) { 17 | grid-template-columns: 1fr; 18 | grid-template-areas: 19 | 'order' 20 | 'address' 21 | 'payment' 22 | 'checkout'; 23 | } 24 | ` 25 | 26 | export const Address = styled.div` 27 | display: flex; 28 | 29 | flex-direction: column; 30 | align-items: center; 31 | grid-area: address; 32 | ` 33 | export const Order = styled.div` 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | grid-area: order; 38 | ` 39 | export const Payment = styled.div` 40 | display: flex; 41 | background: #fafafa; 42 | justify-content: center; 43 | flex-direction: column; 44 | align-items: center; 45 | grid-area: payment; 46 | ` 47 | 48 | export const PlaceOrderBtn = styled.button` 49 | height: 60px; 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | font-size: calc(1.2rem + 0.1vw); 54 | background-color: #333333; 55 | border-color: #333333; 56 | color: #ffffff; 57 | cursor: pointer; 58 | width: 90%; 59 | margin: 1rem auto; 60 | padding: 0.5em 1.5em; 61 | text-decoration: none; 62 | font-weight: 600; 63 | transition: all 0.2s ease-in-out; 64 | ` 65 | export const SubmitHolder = styled.div` 66 | display: flex; 67 | margin: 1rem auto; 68 | justify-content: center; 69 | align-items: center; 70 | flex-direction: column; 71 | 72 | background: #fafafa; 73 | ` 74 | export const PrivacyNotice = styled.p` 75 | font-size: calc(0.8rem + 0.1vw); 76 | line-height: 1.7; 77 | margin: 1rem; 78 | padding: 1rem; 79 | color: ${({ theme }) => theme.primaryText}; 80 | ` 81 | export const ServerMessage = styled.p` 82 | font-size: calc(1.2rem + 0.1vw); 83 | font-weight: 200; 84 | padding: 1rem; 85 | letter-spacing: 1px; 86 | color: ${({ theme }) => theme.primaryText}; 87 | ` 88 | -------------------------------------------------------------------------------- /src/containers/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AuthForm from '../../components/AuthForm' 3 | import { BasicContainer } from '../../styles/utils' 4 | import * as LoginPageStyles from './styled' 5 | 6 | const LoginPageContainer = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default LoginPageContainer 17 | -------------------------------------------------------------------------------- /src/containers/Login/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export const Wrapper = styled.div` 4 | margin: 5rem 0 0 0; 5 | ` 6 | -------------------------------------------------------------------------------- /src/containers/Main/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import CookiesConsent from '../../components/Cookies' 4 | import { Elements } from '@stripe/react-stripe-js' 5 | import Footer from '../../components/Footer' 6 | import LayoutElement from './styled' 7 | import Navbar from '../../components/Navbar' 8 | import Seo from '../../components/Seo' 9 | import Sidebar from '../../components/Sidebar' 10 | import { loadStripe } from '@stripe/stripe-js' 11 | 12 | interface LayoutProps {} 13 | const stripePromise = loadStripe(`${process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!}`) 14 | const Layout: React.FC = ({ children }) => { 15 | const [isOpen, setIsOpen] = useState(false) 16 | 17 | const toggle = () => { 18 | setIsOpen(!isOpen) 19 | } 20 | return ( 21 | <> 22 | 23 | 24 | 25 | 26 | 27 |
{children}
28 | 29 |