├── .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 | [](https://sonarcloud.io/dashboard?id=Onixaz_nextjs-woocommerce-storefront)
6 |
7 | #### _WIP_
8 |
9 | 
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/about daily 0.7 2023-11-21T13:19:35.615Z
4 | https://www.pajustudio.net/account daily 0.7 2023-11-21T13:19:35.615Z
5 | https://www.pajustudio.net/cart daily 0.7 2023-11-21T13:19:35.615Z
6 | https://www.pajustudio.net/checkout daily 0.7 2023-11-21T13:19:35.615Z
7 | https://www.pajustudio.net/contact daily 0.7 2023-11-21T13:19:35.615Z
8 | https://www.pajustudio.net/home daily 0.7 2023-11-21T13:19:35.615Z
9 | https://www.pajustudio.net/login daily 0.7 2023-11-21T13:19:35.615Z
10 | https://www.pajustudio.net/register daily 0.7 2023-11-21T13:19:35.615Z
11 | https://www.pajustudio.net/shop daily 0.7 2023-11-21T13:19:35.615Z
12 | https://www.pajustudio.net/success daily 0.7 2023-11-21T13:19:35.615Z
13 | https://www.pajustudio.net/products/t-shirt-with-logo daily 0.7 2023-11-21T13:19:35.615Z
14 | https://www.pajustudio.net/products/beanie-with-logo daily 0.7 2023-11-21T13:19:35.615Z
15 | https://www.pajustudio.net/products/logo-collection daily 0.7 2023-11-21T13:19:35.615Z
16 | https://www.pajustudio.net/products/wordpress-pennant daily 0.7 2023-11-21T13:19:35.615Z
17 | https://www.pajustudio.net/products/v-neck-t-shirt daily 0.7 2023-11-21T13:19:35.615Z
18 | https://www.pajustudio.net/products/hoodie daily 0.7 2023-11-21T13:19:35.615Z
19 | https://www.pajustudio.net/products/hoodie-with-logo daily 0.7 2023-11-21T13:19:35.615Z
20 | https://www.pajustudio.net/products/t-shirt daily 0.7 2023-11-21T13:19:35.615Z
21 | https://www.pajustudio.net/products/beanie daily 0.7 2023-11-21T13:19:35.615Z
22 | https://www.pajustudio.net/products/belt daily 0.7 2023-11-21T13:19:35.615Z
23 | https://www.pajustudio.net/products/cap daily 0.7 2023-11-21T13:19:35.615Z
24 | https://www.pajustudio.net/products/sunglasses daily 0.7 2023-11-21T13:19:35.615Z
25 | https://www.pajustudio.net/products/hoodie-with-pocket daily 0.7 2023-11-21T13:19:35.615Z
26 | https://www.pajustudio.net/products/hoodie-with-zipper daily 0.7 2023-11-21T13:19:35.615Z
27 | https://www.pajustudio.net/products/long-sleeve-tee daily 0.7 2023-11-21T13:19:35.616Z
28 | https://www.pajustudio.net/products/polo daily 0.7 2023-11-21T13:19:35.616Z
29 | https://www.pajustudio.net/products/album daily 0.7 2023-11-21T13:19:35.616Z
30 | https://www.pajustudio.net/products/single daily 0.7 2023-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 | console.log(e.target.value)}
40 | style={{ margin: '0.25rem 1rem', padding: '0.5rem', fontSize: 'calc(0.9rem + 0.1vw)' }}
41 | name="country"
42 | ref={register}
43 | >
44 | {' '}
45 | {countries.map((country: string, index: number) => {
46 | return (
47 |
48 | {country}
49 |
50 | )
51 | })}
52 |
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 |
30 |
31 |
32 | >
33 | )
34 | }
35 |
36 | export default Layout
37 |
--------------------------------------------------------------------------------
/src/containers/Main/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const LayoutElement = styled.div`
4 | min-height: 105vh;
5 | display: grid;
6 | grid-template-columns: 1fr;
7 | grid-template-rows: auto 1fr auto;
8 | `
9 |
10 | export default LayoutElement
11 |
--------------------------------------------------------------------------------
/src/containers/Product/index.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from '../../types'
2 | import React from 'react'
3 | import { BasicGrid, BasicContainer } from '../../styles/utils'
4 | import * as ProductPageStyles from './styled'
5 | import AddToCartForm from '../../components/Product/AddToCartForm'
6 | import ProductPrice from '../../components/Product/ProductPrice'
7 |
8 | interface ProductPageContentProps {
9 | product: Product
10 | }
11 |
12 | const ProductPageContainer: React.FC = ({ product }) => {
13 | return (
14 |
15 |
16 |
17 |
18 | {product.images && (
19 |
20 | )}
21 |
22 |
23 |
24 | {product.name}
25 |
26 |
29 |
30 |
31 |
32 |
33 | Categories:{' '}
34 |
35 | {product.categories[0].name}
36 |
37 |
38 |
39 |
40 |
41 |
44 |
45 |
46 | )
47 | }
48 |
49 | export default ProductPageContainer
50 |
--------------------------------------------------------------------------------
/src/containers/Product/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const ContentWrapper = styled.div`
4 | display: flex;
5 | padding-top: 5rem;
6 | justify-content: center;
7 | flex-direction: column;
8 | align-items: center;
9 | max-width: 800px;
10 | margin: 0 auto;
11 | width: 100%;
12 | min-height: 50vh;
13 |
14 | @media screen and (max-width: 768px) {
15 | padding-top: 2rem;
16 | }
17 | `
18 |
19 | export const ImgWrapper = styled.div`
20 | height: 440px;
21 | width: 100%;
22 | overflow: hidden;
23 | position: relative;
24 | `
25 |
26 | export const Img = styled.img`
27 | width: 100%;
28 | height: 100%;
29 |
30 | padding: 0.75rem;
31 |
32 | object-fit: cover;
33 | transition: 0.5s all;
34 | `
35 |
36 | export const InfoWrapper = styled.div`
37 | display: flex;
38 | justify-content: center;
39 | flex-direction: column;
40 | padding: 1rem;
41 |
42 | @media screen and (max-width: 768px) {
43 | flex-direction: row;
44 | }
45 |
46 | @media screen and (max-width: 480px) {
47 | flex-direction: column;
48 | }
49 | `
50 |
51 | export const InfoWrapperCol = styled.div`
52 | display: flex;
53 | justify-content: center;
54 | flex-direction: column;
55 | margin: 0 1rem;
56 | `
57 |
58 | export const Name = styled.h2`
59 | font-size: calc(2rem + 0.1vw);
60 | font-weight: 200;
61 | letter-spacing: 1px;
62 | padding-top: 2rem;
63 | `
64 |
65 | export const ShortDescription = styled.div`
66 | padding: 0.5rem 0;
67 | font-size: calc(1.2rem + 0.1vw);
68 | `
69 | export const LongDescription = styled.div`
70 | font-size: calc(1rem + 0.1vw);
71 | margin-top: 1rem;
72 | padding: 1rem;
73 |
74 | @media screen and (max-width: 768px) {
75 | max-width: 480px;
76 | margin: 0 auto;
77 | }
78 | `
79 |
80 | export const AddToCartForm = styled.form`
81 | margin: 2rem 0;
82 | display: flex;
83 | flex-direction: row;
84 | `
85 |
86 | export const InputField = styled.input`
87 | padding: 0.5em;
88 | margin-right: 1rem;
89 | max-width: 60px;
90 | font-size: 1rem;
91 | text-align: center;
92 | background-color: #f2f2f2;
93 | color: #43454b;
94 | border: none;
95 | font-weight: 400;
96 |
97 | &[type='number']::-webkit-inner-spin-button {
98 | opacity: 1;
99 | }
100 | `
101 | export const AddToCartBtn = styled.button`
102 | display: flex;
103 | justify-content: center;
104 | align-items: center;
105 | cursor: pointer;
106 | opacity: 1;
107 | padding: 0.5em 1.5em;
108 | text-decoration: none;
109 | font-weight: 600;
110 | text-shadow: none;
111 |
112 | transition: all 0.2s ease-in-out;
113 | background: #333333;
114 | border-color: #333333;
115 | color: #fff;
116 | width: calc(120px + 0.2vw);
117 | height: 41px;
118 | //display: inline-block;
119 | transition: all 0.2s ease-in-out;
120 | `
121 |
122 | export const Category = styled.p`
123 | font-size: calc(0.8rem + 0.1vw);
124 | margin-top: 2rem;
125 | letter-spacing: 1px;
126 | `
127 | export const CategorySpan = styled.span`
128 | text-decoration: underline;
129 | `
130 |
--------------------------------------------------------------------------------
/src/containers/Register/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import AuthForm from '../../components/AuthForm'
3 | import { useRouter } from 'next/router'
4 | import { BasicContainer } from '../../styles/utils'
5 | import * as RegisterPageStyles from './styled'
6 |
7 | const RegisterPageContainer = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 | )
15 | }
16 |
17 | export default RegisterPageContainer
18 |
--------------------------------------------------------------------------------
/src/containers/Register/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/Shop/index.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import * as ShopPageStyles from './styled'
3 | import React, { useState } from 'react'
4 | import PageTitle from '../../components/PageTitle'
5 | import SingleProduct from '../../components/Product/ProductCard'
6 | import { BasicContainer, BasicGrid, SectionTitle } from '../../styles/utils'
7 | import { Product } from '../../types'
8 |
9 | interface ShopPageProps {
10 | products: Product[]
11 | }
12 |
13 | const ShopPageContainer: NextPage = ({ products = [] }) => {
14 | const [currentPage, setCurrentPage] = useState(1)
15 |
16 | //TODO allow user to select how many products to show per page
17 | const [productsPerPage] = useState(8)
18 |
19 | const indexOfLastProduct = currentPage * productsPerPage
20 | const indexOfFirstProduct = indexOfLastProduct - productsPerPage
21 | const currentProducts = products.slice(indexOfFirstProduct, indexOfLastProduct)
22 | const numberOfPages = Math.ceil(products.length / productsPerPage)
23 |
24 | return (
25 | <>
26 |
30 |
31 |
32 | {currentProducts.length > 0 ? (
33 | <>
34 |
35 | setCurrentPage(currentPage - 1)}
37 | active={false}
38 | visible={currentPage === 1 ? false : true}
39 | >
40 | {'<'}
41 |
42 |
43 | {Array.from({ length: numberOfPages }, (_, index) => {
44 | return (
45 | setCurrentPage(index + 1)}
48 | active={index + 1 === currentPage ? true : false}
49 | visible={true}
50 | >
51 | {index + 1}
52 |
53 | )
54 | })}
55 | setCurrentPage(currentPage + 1)}
57 | active={false}
58 | visible={currentPage === numberOfPages ? false : true}
59 | >
60 | {'>'}
61 |
62 |
63 |
64 | {currentProducts.map((product: any) => {
65 | return
66 | })}
67 |
68 | >
69 | ) : (
70 | No products to show!
71 | )}
72 |
73 | >
74 | )
75 | }
76 | export default ShopPageContainer
77 |
--------------------------------------------------------------------------------
/src/containers/Shop/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const PagesList = styled.ul`
4 | display: flex;
5 | padding: 0.5rem;
6 | flex-direction: row;
7 | justify-content: flex-end;
8 | `
9 |
10 | export const Page = styled.li<{ active: boolean; visible: boolean }>`
11 | cursor: pointer;
12 | padding: 0.35rem 0.8rem;
13 | margin: 0.1rem;
14 | pointer-events: ${({ active }) => (active ? 'none' : 'all')};
15 | visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
16 | background: ${({ theme, active }) => (active ? '#d0d0d0' : theme.lightMediumBg)};
17 |
18 | &:hover {
19 | background: ${({ active }) => (active ? '#d0d0d0' : '#e2e2e2')};
20 | }
21 | `
22 |
--------------------------------------------------------------------------------
/src/context/cart.tsx:
--------------------------------------------------------------------------------
1 | import { useSession } from 'next-auth/react'
2 | import React, { useEffect, useState } from 'react'
3 | import { Cart } from '../types'
4 | import { getCart, initCart } from '../utils/functions'
5 |
6 | export const CartContext = React.createContext(null)
7 |
8 | interface CartProviderProps {}
9 |
10 | const CartProvider: React.FC = ({ children }) => {
11 | const [cart, setCart] = useState({ items: [], key: '', timestamp: 0 })
12 | const [isUpdating, setIsUpdating] = useState(false)
13 | const { data: session }: any = useSession()
14 |
15 | //to change cart expiration date on server
16 | //https://github.com/co-cart/co-cart/search?q=cocart_cart_expiring+in%3Afile&type=Code
17 | //however you still need to expire your local cart so the carts don't get out of sync
18 |
19 | const expireIn = 259200000 //3 days example
20 |
21 | const createUserCart = async () => {
22 | setIsUpdating(true)
23 | const customerRes = await fetch('/api/customers/retrieve')
24 | const customerJson = await customerRes.json()
25 |
26 | const cartKey = customerJson.meta_data.find((x: { [key: string]: string }) => x.key === 'cart')
27 |
28 | console.log('the cartKey is', cartKey)
29 | const newCart = cartKey ? await getCart(cartKey.value) : await initCart()
30 | setCart(newCart)
31 | setIsUpdating(false)
32 | }
33 |
34 | const createGuestCart = async () => {
35 | setIsUpdating(true)
36 | const newCart = await initCart()
37 | setCart(newCart)
38 | setIsUpdating(false)
39 | }
40 |
41 | const checkForLocalCart = (expirity: number) => {
42 | const cartFromLocalStorage = localStorage.getItem('local_cart')
43 | if (
44 | !cartFromLocalStorage ||
45 | new Date().getTime() - JSON.parse(cartFromLocalStorage).timestamp > expirity ||
46 | !JSON.parse(cartFromLocalStorage).key ||
47 | JSON.parse(cartFromLocalStorage).items.length === 0
48 | ) {
49 | return null
50 | } else {
51 | return JSON.parse(cartFromLocalStorage)
52 | }
53 | }
54 |
55 | useEffect(() => {
56 | if (isUpdating) return
57 | if (session) {
58 | createUserCart()
59 | } else {
60 | const localCart = checkForLocalCart(expireIn)
61 | if (localCart) {
62 | setCart(localCart)
63 | } else {
64 | createGuestCart()
65 | }
66 | }
67 | }, [session])
68 |
69 | useEffect(() => {
70 | localStorage.setItem('local_cart', JSON.stringify(cart))
71 | }, [cart])
72 |
73 | return (
74 |
75 | {children}
76 |
77 | )
78 | }
79 |
80 | export default CartProvider
81 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { Session } from 'next-auth'
2 | import { SessionProvider } from 'next-auth/react'
3 | import { ThemeProvider } from 'styled-components'
4 | import { SWRConfig } from 'swr'
5 | import Layout from '../containers/Main'
6 | import CartProvider from '../context/cart'
7 | import GlobalStyle from '../styles/main'
8 | import theme from '../styles/theme'
9 |
10 | import type { AppProps } from 'next/app'
11 |
12 | //node-fetch self signed cert fix for getStaticProps
13 | //https://stackoverflow.com/questions/10888610/ignore-invalid-self-signed-ssl-certificate-in-node-js-with-https-request/21961005#21961005
14 | //process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
15 |
16 | function MyApp({
17 | Component,
18 | pageProps,
19 | }: AppProps<{
20 | session: Session
21 | }>) {
22 | return (
23 | <>
24 |
25 |
26 | fetch(url).then((r) => r.json()) }}>
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | >
37 | )
38 | }
39 |
40 | export default MyApp
41 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
2 |
3 | import { ServerStyleSheet } from 'styled-components'
4 |
5 | export default class MyDocument extends Document {
6 | static async getInitialProps(ctx: DocumentContext) {
7 | const sheet = new ServerStyleSheet()
8 | const originalRenderPage = ctx.renderPage
9 |
10 | try {
11 | ctx.renderPage = () =>
12 | originalRenderPage({
13 | enhanceApp: (App) => (props) => sheet.collectStyles( ),
14 | })
15 |
16 | const initialProps = await Document.getInitialProps(ctx)
17 | return {
18 | ...initialProps,
19 | styles: (
20 | <>
21 | {initialProps.styles}
22 | {sheet.getStyleElement()}
23 | >
24 | ),
25 | }
26 | } finally {
27 | sheet.seal()
28 | }
29 | }
30 |
31 | render() {
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import PageTitle from '../components/PageTitle'
3 |
4 | import { BasicContainer, SectionTitle } from '../styles/utils'
5 |
6 | interface AboutPageProps {}
7 |
8 | const AboutPage: NextPage = () => {
9 | return (
10 | <>
11 |
15 |
16 | Check the code on Github
17 |
18 | >
19 | )
20 | }
21 |
22 | export default AboutPage
23 |
--------------------------------------------------------------------------------
/src/pages/account.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import AccountPageContainer from '../containers/Account'
3 | import PageTitle from '../components/PageTitle'
4 | import { BasicContainer } from '../styles/utils'
5 |
6 | interface AboutPageProps {}
7 |
8 | const AccountPage: NextPage = () => {
9 | return (
10 | <>
11 |
15 |
16 |
17 |
18 | >
19 | )
20 | }
21 |
22 | export default AccountPage
23 |
--------------------------------------------------------------------------------
/src/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | //
2 |
3 | import NextAuth from 'next-auth'
4 | import CredentialsProvider from 'next-auth/providers/credentials'
5 | import jwt from 'jsonwebtoken'
6 | import { poster } from '../../../utils/functions'
7 | import { AuthUserData } from '../../../types'
8 |
9 | interface userData {
10 | username: string
11 | key: string
12 | id: string
13 | }
14 |
15 | export default async function auth(req: any, res: any) {
16 | const providers = [
17 | CredentialsProvider({
18 | name: 'Credentials',
19 |
20 | credentials: {
21 | username: {
22 | label: 'Username',
23 | type: 'text',
24 | },
25 | password: {
26 | label: 'Password',
27 | type: 'password',
28 | },
29 | },
30 | async authorize(userData: AuthUserData | undefined) {
31 | try {
32 | if (!userData) {
33 | return null
34 | }
35 | //get user from wp
36 | const { username, password, cartData } = userData
37 |
38 | const authRes = await poster('/wp-json/jwt-auth/v1/token', { username, password }, 'POST')
39 | const authJson = await authRes.json()
40 |
41 | if (authJson && authJson.token) {
42 | const userId: any = jwt.decode(authJson.token)
43 | const userUrl = `/wp-json/wc/v3/customers/${userId.data.user.id}`
44 |
45 | if (cartData) {
46 | const cart = JSON.parse(cartData)
47 | if (cart.items.length > 0) {
48 | await poster(userUrl, { meta_data: [{ key: 'cart', value: cart.key }] }, 'PUT')
49 | }
50 | }
51 |
52 | const user: userData = {
53 | username: authJson.user_display_name,
54 | key: authJson.token,
55 | id: userId.data.user.id,
56 | }
57 | return user
58 | } else {
59 | console.log(authJson)
60 | return null
61 | }
62 | } catch (error) {
63 | console.error(error)
64 | return null
65 | }
66 | },
67 | }),
68 | ]
69 |
70 | return await NextAuth(req, res, {
71 | providers,
72 | session: {
73 | strategy: 'jwt',
74 | maxAge: 60 * 60 * 24 * 7, //7 days (same as jwt from wp)
75 | },
76 | secret: process.env.NEXTAUTH_SECRET_KEY,
77 | callbacks: {
78 | async jwt({ token, user }: { token: any; user: any }) {
79 | user && (token.user = user)
80 | return token
81 | },
82 | async session({ session, token }: { session: any; token: any }) {
83 | session.user = token.user
84 | return session
85 | },
86 | },
87 | })
88 | }
89 |
--------------------------------------------------------------------------------
/src/pages/api/customers/create.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next'
2 |
3 | import { poster } from '../../../utils/functions'
4 |
5 | export default async function (req: NextApiRequest, res: NextApiResponse) {
6 | if (req.method !== 'POST') return res.status(405).json({ message: 'Method not allowed' })
7 |
8 | try {
9 | const wooReq = await poster('/wp-json/wc/v3/customers', { ...req.body }, 'POST')
10 | const wooRes = await wooReq.json()
11 |
12 | if (wooRes.code === 'registration-error-email-exists') {
13 | return res
14 | .status(400)
15 | .json({ message: 'An account is already registered with this email address.' })
16 | } else if (wooRes.code === 'registration-error-username-exists') {
17 | return res.status(400).json({
18 | message: 'An account is already registered with this username. Please choose another.',
19 | })
20 | } else {
21 | return res.status(200).json({ message: `Success` })
22 | }
23 | } catch (error) {
24 | return res.status(500).json({ message: 'Server error' })
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/pages/api/customers/retrieve.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next'
2 | import { authorizeUser, fetcher } from '../../../utils/functions'
3 |
4 | export default async function (req: NextApiRequest, res: NextApiResponse) {
5 | if (req.method !== 'GET') return res.status(400).json({ message: 'Method not allowed' })
6 |
7 | const key = await authorizeUser(req)
8 | if (!key) return res.status(401).json({ message: 'Access denied' })
9 |
10 | const response = await fetcher(`/wp-json/wc/v3/customers/${key.data.user.id}`)
11 |
12 | return res.status(200).send(await response.json())
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/api/orders/create.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next'
2 | import jwt from 'jsonwebtoken'
3 | import { getSession } from 'next-auth/react'
4 | import Stripe from 'stripe'
5 | import { Cart, Customer } from '../../../types'
6 | import { poster } from '../../../utils/functions'
7 |
8 | interface OrderDetails {
9 | customer: Customer
10 | payment: string
11 | cart: Cart
12 | }
13 |
14 | const stripe = new Stripe(`${process.env.STRIPE_SECRET_KEY!}`, { apiVersion: '2020-08-27' })
15 |
16 | export default async function (req: NextApiRequest, res: NextApiResponse) {
17 | if (req.method !== 'POST') return res.status(405).json({ message: 'Method not allowed' })
18 |
19 | const { customer, payment, cart }: OrderDetails = req.body
20 | if (!customer || !customer.shipping || !payment || !cart)
21 | return res.status(400).json({ message: 'Bad request' })
22 |
23 | try {
24 | const line_items = { ...cart.items }
25 | const { method_id, method_title, cost } = JSON.parse(customer.shipping)
26 |
27 | let user: any
28 | const session: any = await getSession({ req })
29 |
30 | if (session) {
31 | user = jwt.verify(session.user.key, process.env.WP_JWT_AUTH_SECRET_KEY!)
32 | }
33 |
34 | const total = jwt.verify(cost, process.env.NEXTAUTH_SECRET_KEY!)
35 |
36 | const wooBody = {
37 | payment_method: `Credit Card`,
38 | payment_method_title: 'Credit Card',
39 | set_paid: false,
40 | billing: {
41 | ...customer,
42 | },
43 | shipping: {
44 | ...customer,
45 | },
46 | line_items,
47 | customer_note: customer.customer_note,
48 | customer_id: user ? user.data.user.id : 0,
49 | shipping_lines: [
50 | {
51 | method_id,
52 | method_title,
53 | total,
54 | },
55 | ],
56 | }
57 |
58 | const wooResponse = await poster(`/wp-json/wc/v3/orders`, wooBody, 'POST')
59 | const order = await wooResponse.json()
60 |
61 | if (order.data && order.data.status === 400) {
62 | console.error(order.data)
63 | throw new Error(order.message)
64 | }
65 |
66 | const amount = Math.round(parseFloat(order.total) * 100)
67 |
68 | const paymentIntent = await stripe.paymentIntents.create({
69 | payment_method: payment,
70 | amount,
71 | currency: 'usd',
72 | confirm: true,
73 | confirmation_method: 'manual',
74 | })
75 |
76 | if (paymentIntent.status === 'succeeded') {
77 | res.status(200).json({ message: 'Success' })
78 |
79 | poster(
80 | `/wp-json/wc/v3/orders/${order.id}`,
81 |
82 | { set_paid: true, transaction_id: paymentIntent.id },
83 | 'PUT',
84 | )
85 | } else {
86 | res.status(400).json({ message: 'Failure in processing the payment' })
87 | }
88 | } catch (error) {
89 | console.error(error)
90 | res.status(500).json({ message: 'Server error' })
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/pages/api/orders/retrieve.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next'
2 | import { authorizeUser, fetcher } from '../../../utils/functions'
3 |
4 | export default async function (req: NextApiRequest, res: NextApiResponse) {
5 | if (req.method !== 'GET') return res.status(400).json({ message: 'Method not allowed' })
6 |
7 | const key = await authorizeUser(req)
8 | if (!key) return res.status(401).json({ message: 'Access denied' })
9 |
10 | const response = await fetcher(`/wp-json/wc/v3/orders?customer=${key.data.user.id}`)
11 | const data = await response.json()
12 |
13 | return res.status(200).send(data)
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/api/products/retrieve.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next'
2 | import { fetcher } from '../../../utils/functions'
3 |
4 | export default async function (req: NextApiRequest, res: NextApiResponse) {
5 | if (req.method !== 'GET') return res.status(400).json({ message: 'Method not allowed' })
6 | const response = await fetcher(`/wp-json/wc/v3/products?per_page=100&status=publish`)
7 |
8 | return res.status(200).send(await response.json())
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/api/shipping/retrieve.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next'
2 | import { fetcher } from '../../../utils/functions'
3 | import jwt from 'jsonwebtoken'
4 |
5 | interface Shipping {
6 | id: number
7 | title: string
8 | method_id: string
9 | settings: { cost: { value: string } }
10 | cost: number
11 | enabled: boolean
12 | }
13 |
14 | export default async function (req: NextApiRequest, res: NextApiResponse) {
15 | if (req.method !== 'GET') return res.status(400).json({ message: 'Method not allowed' })
16 | const response = await fetcher(`/wp-json/wc/v3/shipping/zones/0/methods`)
17 | const shippingJSON = await response.json()
18 |
19 | const shippingOptions = shippingJSON.map((item: Shipping) => {
20 | return {
21 | id: item.id,
22 | title: item.title,
23 | method: item.method_id,
24 | cost: jwt.sign(
25 | String(item.settings.cost.value ? parseFloat(item.settings.cost.value) : 0),
26 | process.env.NEXTAUTH_SECRET_KEY!,
27 | ),
28 | enabled: item.enabled,
29 | }
30 | })
31 |
32 | return res.status(200).send(shippingOptions)
33 | }
34 |
--------------------------------------------------------------------------------
/src/pages/cart.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import PageTitle from '../components/PageTitle'
3 | import CartPageContainer from '../containers/Cart'
4 |
5 | interface CartPageProps {}
6 |
7 | const CartPage: NextPage = () => {
8 | return (
9 | <>
10 |
14 |
15 | >
16 | )
17 | }
18 |
19 | export default CartPage
20 |
--------------------------------------------------------------------------------
/src/pages/checkout.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import PageTitle from '../components/PageTitle'
3 | import CheckoutPageContainer from '../containers/Checkout'
4 |
5 | interface CheckoutPageProps {}
6 |
7 | const CheckoutPage: NextPage = () => {
8 | return (
9 | <>
10 |
14 |
15 |
16 | >
17 | )
18 | }
19 |
20 | export default CheckoutPage
21 |
--------------------------------------------------------------------------------
/src/pages/contact.tsx:
--------------------------------------------------------------------------------
1 | import { BasicContainer, SectionTitle } from '../styles/utils'
2 |
3 | import { NextPage } from 'next'
4 | import PageTitle from '../components/PageTitle'
5 |
6 | interface ContactPageProps {}
7 |
8 | const ContactPage: NextPage = () => {
9 | return (
10 | <>
11 |
15 |
16 | Contact me!
17 |
18 | >
19 | )
20 | }
21 |
22 | export default ContactPage
23 |
--------------------------------------------------------------------------------
/src/pages/home.tsx:
--------------------------------------------------------------------------------
1 | import { BasicContainer, BasicGrid, SectionTitle } from '../styles/utils'
2 | import { Category, Product } from '../types'
3 | import Hero from '../components/Hero'
4 | import { NextPage } from 'next'
5 | import React from 'react'
6 | import SingleCategory from '../components/Category'
7 | import SingleProduct from '../components/Product/ProductCard'
8 | import { fetcher } from '../utils/functions'
9 | import PageTitle from '../components/PageTitle'
10 |
11 | interface HomePageProps {
12 | categories: Category[]
13 | featured: Product[]
14 | }
15 |
16 | const HomePage: NextPage = ({ categories, featured }) => {
17 | return (
18 | <>
19 |
23 | {/* TODO refactor hero */}
24 |
25 |
26 |
27 | Shop by Category
28 |
29 |
30 | {categories?.map((category: Category) => {
31 | return
32 | })}
33 |
34 | Featured Products
35 |
36 | {featured?.map((product: Product) => {
37 | return (
38 |
39 |
40 |
41 | )
42 | })}
43 |
44 |
45 | >
46 | )
47 | }
48 |
49 | export default HomePage
50 |
51 | export async function getStaticProps() {
52 | const categoriesRes = await fetcher(`/wp-json/wc/v3/products/categories`)
53 | const categoriesJson = await categoriesRes.json()
54 |
55 | if (categoriesRes.status !== 200) {
56 | console.error('Home page error on getStaticProps', categoriesJson)
57 | return {
58 | props: { categories: [], featured: [] },
59 | }
60 | }
61 |
62 | const categories = categoriesJson.filter((item: Product) => {
63 | return item.name !== 'Uncategorized'
64 | })
65 |
66 | const productsRes = await fetcher(
67 | `/wp-json/wc/v3/products?per_page=4&status=publish&type=simple&featured=true`,
68 | )
69 | const featured = await productsRes.json()
70 |
71 | return {
72 | props: { categories, featured },
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/pages/login.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import React from 'react'
3 | import PageTitle from '../components/PageTitle'
4 | import LoginPageContainer from '../containers/Login'
5 |
6 | interface ContactPageProps {}
7 |
8 | const LoginPage: NextPage = () => {
9 | return (
10 | <>
11 |
15 |
16 | >
17 | )
18 | }
19 |
20 | export default LoginPage
21 |
--------------------------------------------------------------------------------
/src/pages/products/[slug].tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import { Product } from '../../types'
3 | import { fetcher } from '../../utils/functions'
4 | import { GetStaticProps } from 'next'
5 | import ProductPageContainer from '../../containers/Product'
6 |
7 | interface ProductPageProps {
8 | product: Product
9 | }
10 |
11 | const ProductPage: NextPage = ({ product }) => {
12 | return
13 | }
14 |
15 | export default ProductPage
16 |
17 | export const getStaticProps: GetStaticProps = async ({ params }) => {
18 | const productsRes = await fetcher(`/wp-json/wc/v3/products?slug=${params?.slug}`)
19 |
20 | const found = await productsRes.json()
21 |
22 | return {
23 | props: {
24 | product: found[0],
25 | },
26 | }
27 | }
28 |
29 | export const getStaticPaths = async () => {
30 | const productsRes = await fetcher(`/wp-json/wc/v3/products?per_page=30`)
31 | const products = await productsRes.json()
32 |
33 | const publishedProducts = products.filter((product: { [key: string]: string }) => {
34 | return product.status === 'publish'
35 | })
36 |
37 | const paths = publishedProducts.map((product: Product) => ({
38 | params: { slug: String(product.slug) },
39 | }))
40 |
41 | return { paths, fallback: false }
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/register.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import React from 'react'
3 | import PageTitle from '../components/PageTitle'
4 | import RegisterPageContainer from '../containers/Register'
5 |
6 | interface ContactPageProps {}
7 |
8 | const RegisterPage: NextPage = () => {
9 | return (
10 | <>
11 |
15 |
16 | >
17 | )
18 | }
19 |
20 | export default RegisterPage
21 |
--------------------------------------------------------------------------------
/src/pages/shop.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from 'next'
2 | import { Product } from '../types/index'
3 | import React from 'react'
4 | import { fetcher } from '../utils/functions'
5 | import ShopPageContainer from '../containers/Shop'
6 |
7 | interface ShopPageProps {
8 | products: Product[]
9 | }
10 |
11 | const ShopPage: NextPage = ({ products }) => {
12 | return
13 | }
14 |
15 | export default ShopPage
16 |
17 | export async function getStaticProps() {
18 | //TODO: implement variable products
19 | const res = await fetcher(`/wp-json/wc/v3/products?per_page=100&status=publish&type=simple`)
20 | const products = await res.json()
21 |
22 | if (res.status !== 200) {
23 | console.error('Shop page error on getStaticProps', products)
24 | return {
25 | props: { products: [] },
26 | }
27 | }
28 |
29 | return {
30 | props: { products },
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/success.tsx:
--------------------------------------------------------------------------------
1 | import { BasicContainer, SectionTitle } from '../styles/utils'
2 |
3 | import { NextPage } from 'next'
4 | import React from 'react'
5 | import PageTitle from '../components/PageTitle'
6 |
7 | const SuccessPage: NextPage = () => {
8 | return (
9 | <>
10 |
14 |
15 | Thank you for your order!
16 |
17 | >
18 | )
19 | }
20 |
21 | export default SuccessPage
22 |
--------------------------------------------------------------------------------
/src/styles/main.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components'
2 | import { ThemeType } from './theme'
3 |
4 | interface Props {
5 | theme: ThemeType
6 | }
7 |
8 | const GlobalStyle = createGlobalStyle`
9 | *, *:after, *:before {
10 | box-sizing: border-box;
11 | margin: 0;
12 | padding: 0;
13 | outline: none;
14 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
15 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
16 | line-height: 1.6;
17 | }
18 |
19 | body{
20 | background: ${({ theme }) => theme.lightWhiteBg};
21 | }
22 |
23 | a {
24 | text-decoration: none;
25 | color: ${({ theme }) => theme.primaryBlack}
26 | }
27 | ul {
28 | list-style: none
29 | }
30 |
31 | img{
32 | max-width: 100%;
33 | }
34 |
35 |
36 | input:-webkit-autofill,
37 | input:-webkit-autofill:hover,
38 | input:-webkit-autofill:focus,
39 | input:-webkit-autofill:active {
40 | box-shadow: inherit;
41 | -webkit-box-shadow: 0 0 0 30px ${({ theme }) => theme.lightMediumBg} inset !important;
42 | }
43 |
44 | `
45 |
46 | export default GlobalStyle
47 |
--------------------------------------------------------------------------------
/src/styles/theme.ts:
--------------------------------------------------------------------------------
1 | const defaultTheme = {
2 | lightMediumBg: '#f2f2f2',
3 | lightWhiteBg: '#fff',
4 | //text button colors
5 | primaryBlack: '#0d0d0d',
6 | secondaryBlack: '#898989',
7 | primaryText: '#4c4e4e',
8 | primaryWhite: '#fafafa',
9 | primaryPurple: '#96588a',
10 | }
11 |
12 | export type ThemeType = typeof defaultTheme
13 | export default defaultTheme
14 |
--------------------------------------------------------------------------------
/src/styles/utils.ts:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | export const BasicContainer = styled.section`
4 | padding: 5rem 0;
5 | max-width: 1200px;
6 | width: 95%;
7 | margin: 0 auto;
8 | `
9 |
10 | export const FlexGrid = styled.div`
11 | display: flex;
12 | flex-wrap: wrap;
13 | justify-content: center;
14 |
15 | & > * {
16 | flex-basis: 20%;
17 | min-width: 15em;
18 | max-width: 25em;
19 | flex-grow: 1;
20 | }
21 | `
22 |
23 | export const BasicGrid = styled.div<{ lg: number; md: number; sm: number; xs: number }>`
24 | display: grid;
25 | grid-template-columns: ${({ lg }) => `repeat(${lg}, 1fr)`};
26 | width: 100%;
27 | height: 80%;
28 |
29 | @media screen and (max-width: 992px) {
30 | grid-template-columns: ${({ md }) => `repeat(${md}, 1fr)`};
31 | }
32 |
33 | @media screen and (max-width: 768px) {
34 | grid-template-columns: ${({ sm }) => `repeat(${sm}, 1fr)`};
35 | }
36 | @media screen and (max-width: 480px) {
37 | grid-template-columns: ${({ xs }) => `repeat(${xs}, 1fr)`};
38 | }
39 | `
40 |
41 | export const SectionTitle = styled.h2`
42 | display: flex;
43 | justify-content: center;
44 | font-size: calc(2rem + 0.1vw);
45 | font-weight: 200;
46 | text-align: center;
47 | padding: 1rem;
48 | margin: 1rem;
49 | `
50 |
51 | export const MainButton = styled.button``
52 |
53 | export const Subtitle = styled.h2`
54 | font-size: calc(1.5rem + 0.1vw);
55 | font-weight: 200;
56 | letter-spacing: 1px;
57 | padding: 1rem;
58 | margin: 1rem;
59 | `
60 |
61 | export const Loader = styled.div`
62 |
63 | border: 2px solid #f3f3f3;
64 | border-radius: 50%;
65 | border-top: 2px solid #3333;
66 | width: 1.5em;
67 | height: 1.5em;
68 | animation: spin 1s linear infinite;
69 | }
70 |
71 | @keyframes spin {
72 | 0% { transform: rotate(0deg); }
73 | 100% { transform: rotate(360deg); }
74 |
75 | `
76 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface Product {
2 | name: string
3 | slug?: string
4 | id: number
5 | featured: boolean
6 | type: string
7 | status: string
8 | images: Array<{
9 | src: string
10 | alt: string
11 | }>
12 |
13 | price: string
14 | regular_price: string
15 | sale_price: string
16 | short_description: string
17 | description: string
18 | categories: Array<{
19 | name: string
20 | }>
21 | }
22 |
23 | export interface Category {
24 | id: number
25 | name: string
26 | image: { [key: string]: string }
27 | count: { [key: string]: number }
28 | }
29 |
30 | export interface CartItem {
31 | key?: string
32 | product_id: number
33 | quantity: number
34 | product_price?: string
35 | product_name?: string
36 | img?: string
37 | slug?: string
38 | line_total?: number
39 | }
40 |
41 | export interface Cart {
42 | key: string | null
43 | timestamp: number
44 | items: CartItem[]
45 | }
46 |
47 | export interface Customer {
48 | first_name?: string
49 | last_name?: string
50 | address_1?: string
51 | address_2?: string
52 | city?: string
53 | state?: string
54 | postcode?: string
55 | country?: string
56 | email?: string
57 | phone?: string
58 | customer_note?: string
59 | shipping?: string
60 | }
61 |
62 | export interface Order {
63 | id: number
64 | status: string
65 | total: string
66 | date_created: string
67 | }
68 |
69 | export interface AuthUserData {
70 | username: string
71 | password: string
72 | cartData?: string
73 | }
74 |
--------------------------------------------------------------------------------
/src/utils/functions.ts:
--------------------------------------------------------------------------------
1 | import { Cart, CartItem, Customer, Product } from '../types'
2 | import jwt from 'jsonwebtoken'
3 | import { NextApiRequest } from 'next'
4 | import { getSession } from 'next-auth/react'
5 |
6 | export const authorizeAdmin = () => {
7 | const payload = {
8 | iss: `${process.env.NEXT_PUBLIC_WP_API_URL}`,
9 |
10 | data: {
11 | user: {
12 | id: '1',
13 | },
14 | },
15 | }
16 |
17 | return jwt.sign(payload, `${process.env.WP_JWT_AUTH_SECRET_KEY!}`, { expiresIn: 60 })
18 | }
19 |
20 | export const authorizeUser = async (req: NextApiRequest) => {
21 | try {
22 | const session: any = await getSession({ req })
23 | if (!session) return null
24 |
25 | const key: any = jwt.verify(session.user.key, process.env.WP_JWT_AUTH_SECRET_KEY!, {
26 | algorithms: ['HS256'],
27 | ignoreNotBefore: true,
28 | })
29 |
30 | return key
31 | } catch (error) {
32 | return null
33 | }
34 | }
35 |
36 | export const fetcher = async (url: string) => {
37 | const token = authorizeAdmin()
38 |
39 | return fetch(process.env.NEXT_PUBLIC_WP_API_URL + url, {
40 | headers: {
41 | Authorization: `Bearer ${token}`,
42 |
43 | 'Content-Type': 'application/json',
44 | },
45 | credentials: 'include',
46 | mode: 'cors',
47 | })
48 | }
49 |
50 | export const poster = async (url: string, data: object, method: string) => {
51 | const token = authorizeAdmin()
52 |
53 | return fetch(process.env.NEXT_PUBLIC_WP_API_URL + url, {
54 | headers: {
55 | Authorization: `Bearer ${token}`,
56 |
57 | 'Content-Type': 'application/json',
58 | },
59 | method: method,
60 | body: JSON.stringify(data),
61 | credentials: 'include',
62 | mode: 'cors',
63 | })
64 | }
65 |
66 | export const initCart = async () => {
67 | const res = await fetch(`${process.env.NEXT_PUBLIC_WP_API_URL}/wp-json/cocart/v1/get-cart`)
68 | const cartKey = res.headers.get('x-cocart-api')
69 |
70 | return {
71 | items: [],
72 | key: cartKey,
73 | timestamp: new Date().getTime(),
74 | }
75 | }
76 |
77 | export const getCart = async (cartKey: string) => {
78 | const res = await fetch(
79 | `${process.env.NEXT_PUBLIC_WP_API_URL}/wp-json/cocart/v1/get-cart?cart_key=${cartKey}`,
80 | )
81 |
82 | const cart: CartItem[] = await res.json()
83 | return {
84 | items: Object.values(cart),
85 | key: cartKey,
86 | timestamp: new Date().getTime(),
87 | }
88 | }
89 |
90 | export const getSingleProduct = (productId: number, data: any) => {
91 | const product = data.find((item: Product) => {
92 | return item.id === productId
93 | })
94 |
95 | return product
96 | }
97 |
98 | export const updateCart = (cart: Cart, data: Response) => {
99 | const newCart = { ...cart }
100 | newCart.items = Object.values(data)
101 |
102 | return newCart
103 | }
104 |
105 | export const createOrder = async (customer: Customer, payment: string, cart: Cart) => {
106 | const res = await fetch(`/api/orders/create`, {
107 | method: 'POST',
108 | headers: {
109 | 'Content-Type': 'application/json',
110 | },
111 | body: JSON.stringify({ customer, payment, cart }),
112 | })
113 |
114 | return res.json()
115 | }
116 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noEmit": true,
16 | "esModuleInterop": true,
17 | "module": "esnext",
18 | "moduleResolution": "node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": true,
21 | "jsx": "preserve",
22 | "incremental": true
23 | },
24 | "exclude": [
25 | "node_modules"
26 | ],
27 | "include": [
28 | "next-env.d.ts",
29 | "**/*.ts",
30 | "**/*.tsx",
31 | "next.config.js",
32 | "decs.d.ts",
33 | "next-sitemap.config.js"
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------