├── .env.local ├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── Cart.js ├── Nav.js ├── Product.js └── User.js ├── lib ├── context.js ├── formatMoney.js ├── getStripe.js └── query.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ ├── auth │ │ └── [...auth0].js │ └── stripe.js ├── index.js ├── product │ └── [slug].js ├── profile.js └── success.js ├── public ├── favicon.ico ├── shiba-success.png └── vercel.svg └── styles ├── CartStyles.js ├── Gallery.js ├── NavStyles.js ├── ProductDetails.js ├── ProductStyle.js └── globals.css /.env.local: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_BACKEND_API="http://localhost:1337/graphql" 2 | BASE_URL="http://localhost:3000" 3 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_e3nGTeSuODBpgGnaKncucoi300fI60ZQna" 4 | NEXT_PUBLIC_STRIPE_SECRET_KEY="sk_test_51ErX3zJvB7fsxaM1HrkMSpk2gLRtczuCgNBCN0TucFgX78i9am4a6WKVKbGJqmtXO4dMODHorg2TAO8ChOLer9Sj00RF9GA3yj" 5 | AUTH0_SECRET='f2c4c4d5f29dc65c15ca6fa4535cbc4a34f497def9ea70c6f38b17556a3496de' 6 | AUTH0_BASE_URL="http://localhost:3000" 7 | AUTH0_ISSUER_BASE_URL="https://dev-kthrisfg.us.auth0.com/" 8 | AUTH0_CLIENT_ID="4Ye8LRQxPeV58drmikNMG8xBI1divs1n" 9 | AUTH0_CLIENT_SECRET="6R4z5Pj0hiBhu3bvWYxQWpwVFJ4BNShhG1BTitKEDkTGZ1LyTB0bdiACRUDvwQuR" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | # .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /components/Cart.js: -------------------------------------------------------------------------------- 1 | import { 2 | CartStyle, 3 | Card, 4 | EmptyStyle, 5 | CartWrapper, 6 | CardInfo, 7 | Checkout, 8 | } from "../styles/CartStyles"; 9 | import { Quantity } from "../styles/ProductDetails"; 10 | import { FaShoppingCart } from "react-icons/fa"; 11 | import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai"; 12 | //Import State 13 | import { useStateContext } from "../lib/context"; 14 | import getStripe from "../lib/getStripe"; 15 | 16 | export default function Cart() { 17 | const { cartItems, setShowCart, onAdd, onRemove, totalPrice } = 18 | useStateContext(); 19 | 20 | //Payment 21 | const handleCheckout = async () => { 22 | const stripePromise = await getStripe(); 23 | const response = await fetch("/api/stripe", { 24 | method: "POST", 25 | headers: { 26 | "Content-Type": "application/json", 27 | }, 28 | body: JSON.stringify(cartItems), 29 | }); 30 | const data = await response.json(); 31 | await stripePromise.redirectToCheckout({ sessionId: data.id }); 32 | }; 33 | 34 | return ( 35 | setShowCart(false)} 40 | > 41 | e.stopPropagation()} 48 | > 49 | {cartItems.length < 1 && ( 50 | 55 |

You have more shopping to do 😉

56 | 57 |
58 | )} 59 | {cartItems.length >= 1 && 60 | cartItems.map((item) => { 61 | return ( 62 | 68 | 69 | 70 |

{item.title}

71 |

{item.price}$

72 | 73 | Quantity 74 | 77 |

{item.quantity}

78 | 81 |
82 |
83 |
84 | ); 85 | })} 86 | 87 | {cartItems.length >= 1 && ( 88 |
89 |

Subtotal ${totalPrice}

90 | 91 |
92 | )} 93 |
94 |
95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /components/Nav.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { FiShoppingBag } from "react-icons/fi"; 3 | import { NavStyles, NavItems } from "../styles/NavStyles"; 4 | import Cart from "./Cart"; 5 | import { useStateContext } from "../lib/context"; 6 | const { AnimatePresence, motion } = require("framer-motion"); 7 | import User from "./User"; 8 | 9 | export default function Nav() { 10 | const { showCart, setShowCart, totalQuantities } = useStateContext(); 11 | 12 | return ( 13 | 14 | Styled. 15 | 16 | 17 |
setShowCart(true)}> 18 | {totalQuantities > 0 && ( 19 | 20 | {totalQuantities} 21 | 22 | )} 23 | 24 |

Cart

25 |
26 |
27 | {showCart && } 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/Product.js: -------------------------------------------------------------------------------- 1 | import { ProductStyles } from "../styles/ProductStyle"; 2 | import Link from "next/link"; 3 | 4 | export default function Product({ product }) { 5 | //Extract from props 6 | const { title, price, image, slug } = product.attributes; 7 | return ( 8 | 9 | 10 |
11 | {title} 12 |
13 | 14 |

{title}

15 |

${price}

16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/User.js: -------------------------------------------------------------------------------- 1 | import { FaUserCircle } from "react-icons/fa"; 2 | import { useRouter } from "next/router"; 3 | import { useUser } from "@auth0/nextjs-auth0"; 4 | import styled from "styled-components"; 5 | 6 | export default function User() { 7 | const route = useRouter(); 8 | const { user, error, isLoading } = useUser(); 9 | if (!user) 10 | return ( 11 |
route.push(`/api/auth/login`)}> 12 | 13 |

Login

14 |
15 | ); 16 | return ( 17 | route.push(`/profile`)}> 18 | {user.name} 19 |

{user.name}

20 |
21 | ); 22 | } 23 | 24 | const Profile = styled.div` 25 | img { 26 | border-radius: 50%; 27 | width: 1.5rem; 28 | height: 1.5rem; 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState } from "react"; 2 | 3 | const Context = createContext(); 4 | 5 | export const StateContext = ({ children }) => { 6 | //Ourt application state 7 | const [showCart, setShowCart] = useState(false); 8 | const [cartItems, setCartItems] = useState([]); 9 | const [qty, setQty] = useState(1); 10 | const [totalPrice, setTotalPrice] = useState(0); 11 | const [totalQuantities, setTotalQuantitites] = useState(0); 12 | 13 | //Increase product countity 14 | const increaseQty = () => { 15 | setQty((prevQty) => prevQty + 1); 16 | }; 17 | //Decrease product quantity 18 | const decreaseQty = () => { 19 | setQty((prevQty) => { 20 | if (prevQty - 1 < 1) return 1; 21 | return prevQty - 1; 22 | }); 23 | }; 24 | //Add Product To Cart 25 | const onAdd = (product, quantity) => { 26 | //Total Price 27 | setTotalPrice( 28 | (prevTotalPrice) => prevTotalPrice + product.price * quantity 29 | ); 30 | //Increase total quantity 31 | setTotalQuantitites( 32 | (prevTotalQuantities) => prevTotalQuantities + quantity 33 | ); 34 | //Check if product is in the cart 35 | const exist = cartItems.find((item) => item.slug === product.slug); 36 | if (exist) { 37 | setCartItems( 38 | cartItems.map((item) => 39 | item.slug === product.slug 40 | ? { ...exist, quantity: exist.quantity + quantity } 41 | : item 42 | ) 43 | ); 44 | } else { 45 | setCartItems([...cartItems, { ...product, quantity: quantity }]); 46 | } 47 | }; 48 | //Remove product 49 | const onRemove = (product) => { 50 | //Set Total Price 51 | setTotalPrice((prevTotalPrice) => prevTotalPrice - product.price); 52 | 53 | //Remove from total quantities 54 | setTotalQuantitites((prevTotalQuantities) => prevTotalQuantities - 1); 55 | 56 | //Check if product exists 57 | const exist = cartItems.find((item) => item.slug === product.slug); 58 | if (exist.quantity === 1) { 59 | setCartItems(cartItems.filter((item) => item.slug !== product.slug)); 60 | } else { 61 | setCartItems( 62 | cartItems.map((item) => 63 | item.slug === product.slug 64 | ? { ...exist, quantity: exist.quantity - 1 } 65 | : item 66 | ) 67 | ); 68 | } 69 | }; 70 | 71 | return ( 72 | 87 | {children} 88 | 89 | ); 90 | }; 91 | 92 | export const useStateContext = () => useContext(Context); 93 | -------------------------------------------------------------------------------- /lib/formatMoney.js: -------------------------------------------------------------------------------- 1 | export default function formatMoney(amount = 0) { 2 | const options = { 3 | style: "currency", 4 | currency: "USD", 5 | minimumFractionDigits: 2, 6 | }; 7 | // check if its a clean dollar amount 8 | if (amount % 100 === 0) { 9 | options.minimumFractionDigits = 0; 10 | } 11 | 12 | const formatter = Intl.NumberFormat("en-US", options); 13 | return formatter.format(amount / 100); 14 | } 15 | -------------------------------------------------------------------------------- /lib/getStripe.js: -------------------------------------------------------------------------------- 1 | import { loadStripe } from "@stripe/stripe-js"; 2 | 3 | let stripePromise; 4 | 5 | const getStripe = async () => { 6 | if (!stripePromise) { 7 | stripePromise = await loadStripe( 8 | `${process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}` 9 | ); 10 | } 11 | 12 | return stripePromise; 13 | }; 14 | 15 | export default getStripe; 16 | -------------------------------------------------------------------------------- /lib/query.js: -------------------------------------------------------------------------------- 1 | export const PRODUCT_QUERY = `query{ 2 | products{ 3 | data{ 4 | attributes{ 5 | description 6 | title 7 | slug 8 | price 9 | image{ 10 | data{ 11 | attributes{ 12 | formats 13 | } 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | `; 21 | 22 | export const GET_PRODUCT_QUERY = ` 23 | query getProducts($slug:String!){ 24 | products(filters: {slug :{eq: $slug}}){ 25 | data{ 26 | attributes{ 27 | title 28 | slug 29 | description 30 | price 31 | image{ 32 | data{ 33 | attributes{ 34 | formats 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | }`; 42 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | compiler: { 5 | // ssr and displayName are configured by default 6 | styledComponents: true, 7 | }, 8 | async redirects() { 9 | return [ 10 | { 11 | source: "/canceled", 12 | destination: "/", 13 | permanent: true, 14 | }, 15 | ]; 16 | }, 17 | }; 18 | 19 | module.exports = nextConfig; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@auth0/nextjs-auth0": "^1.9.0", 13 | "@stripe/react-stripe-js": "^1.8.1", 14 | "@stripe/stripe-js": "^1.31.0", 15 | "framer-motion": "^6.3.11", 16 | "graphql": "^16.5.0", 17 | "graphql-request": "^4.3.0", 18 | "next": "12.1.6", 19 | "react": "18.1.0", 20 | "react-dom": "18.1.0", 21 | "react-hot-toast": "^2.2.0", 22 | "react-icons": "^4.4.0", 23 | "react-loading-skeleton": "^3.1.0", 24 | "stripe": "^9.8.0", 25 | "styled-components": "^5.3.5", 26 | "urql": "^2.2.2" 27 | }, 28 | "devDependencies": { 29 | "eslint": "8.17.0", 30 | "eslint-config-next": "12.1.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import { createClient, Provider } from "urql"; 3 | import Nav from "../components/Nav"; 4 | import { StateContext } from "../lib/context"; 5 | import { UserProvider } from "@auth0/nextjs-auth0"; 6 | import { Toaster } from "react-hot-toast"; 7 | 8 | const client = createClient({ url: process.env.NEXT_PUBLIC_BACKEND_API }); 9 | 10 | function MyApp({ Component, pageProps }) { 11 | return ( 12 | 13 | 14 | 15 | 16 |