├── .gitignore
├── README.md
├── components
├── BuyButton.js
├── Footer.js
└── Header.js
├── context
└── AuthContext.js
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── account.js
├── index.js
├── login.js
├── products
│ └── [slug].js
└── success.js
├── products.json
├── public
├── NextJS.jpg
├── favicon.ico
├── user_avatar.png
└── vercel.svg
├── styles
├── BuyButton.module.css
├── Header.module.css
├── Home.module.css
├── Login.module.css
└── globals.css
└── utils
├── format.js
└── urls.js
/.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 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Next Magic Stripe Strapi Frontend
2 |
3 | ### Companion Video
4 | https://www.youtube.com/watch?v=385cpCpGRC0
5 |
6 |
7 | Sponsored by Magic https://magic.link/
8 |
9 | NextJS Frontend with Magic for Authentication, Stripe for Checkout Payment, Strapi for Product, Orders and User management
10 |
11 | Check /utils/urls for ENV Variables setup
12 |
13 | Backend at:
14 | https://github.com/GalloDaSballo/Next-Ecommerce-Backend
15 |
--------------------------------------------------------------------------------
/components/BuyButton.js:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router'
2 | import { loadStripe } from '@stripe/stripe-js'
3 | import { API_URL, STRIPE_PK } from '../utils/urls'
4 | import styles from '../styles/BuyButton.module.css'
5 | import { useContext } from "react";
6 | import AuthContext from "../context/AuthContext";
7 |
8 | const stripePromise = loadStripe(STRIPE_PK)
9 |
10 | export default function BuyButton ({ product }) {
11 | const { user, getToken } = useContext(AuthContext);
12 |
13 | const router = useRouter()
14 |
15 | const handleBuy = async (e) => {
16 | const stripe = await stripePromise
17 | const token = await getToken()
18 | console.log("handleBuy token", token)
19 | e.preventDefault()
20 | const res = await fetch(`${API_URL}/orders/`, {
21 | method: 'POST',
22 | body: JSON.stringify({product}),
23 | headers: {
24 | 'Content-type': 'application/json',
25 | 'Authorization': `Bearer ${token}`
26 | }
27 | })
28 | const session = await res.json()
29 | console.log("session", session)
30 |
31 | const result = await stripe.redirectToCheckout({
32 | sessionId: session.id,
33 | });
34 | }
35 |
36 | const redirectToLogin = async () => {
37 | router.push('/login')
38 | }
39 |
40 | return(
41 | <>
42 | {user &&
43 |
44 | }
45 | {!user &&
46 |
47 | }
48 | >
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | export default () => (
2 |
17 | )
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import Link from 'next/link'
3 | import { useRouter } from 'next/router'
4 | import AuthContext from "../context/AuthContext"
5 |
6 |
7 | import styles from '../styles/Header.module.css'
8 |
9 | export default () => {
10 |
11 | const router = useRouter()
12 | const isHome = router.pathname === "/"
13 |
14 | const { user } = useContext(AuthContext);
15 |
16 | const goBack = (event) => {
17 | event.preventDefault()
18 | router.back()
19 | }
20 |
21 |
22 |
23 | return (
24 |
25 | {!isHome &&
26 |
29 | }
30 |
39 |
40 |
41 | {user ? (
42 |
43 |
44 |
45 |
46 |
47 | ) : (
48 |
49 |
Log In
50 |
51 | )}
52 |
53 |
54 |
55 | )
56 | }
--------------------------------------------------------------------------------
/context/AuthContext.js:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useEffect } from "react";
2 | import { Magic } from "magic-sdk";
3 | import { MAGIC_PUBLIC_KEY } from "../utils/urls";
4 | import { useRouter } from "next/router";
5 |
6 | const AuthContext = createContext();
7 |
8 | let magic
9 |
10 | export const AuthProvider = (props) => {
11 | const [user, setUser] = useState(null);
12 | const router = useRouter();
13 |
14 | /**
15 | * Log the user in
16 | * @param {string} email
17 | */
18 | const loginUser = async (email) => {
19 | try {
20 | await magic.auth.loginWithMagicLink({ email });
21 | setUser({ email });
22 | router.push("/");
23 | } catch (err) {
24 | console.log(err);
25 | }
26 | };
27 |
28 | /**
29 | * Log the user out
30 | */
31 | const logoutUser = async () => {
32 | try {
33 | await magic.user.logout();
34 | setUser(null);
35 | router.push("/");
36 | } catch (err) {
37 | console.log(err);
38 | }
39 | };
40 |
41 | /**
42 | * If user is logged in, get data and display it
43 | */
44 | const checkUserLoggedIn = async () => {
45 | try {
46 | const isLoggedIn = await magic.user.isLoggedIn();
47 |
48 | if (isLoggedIn) {
49 | const { email } = await magic.user.getMetadata();
50 | setUser({ email });
51 | //Add this just for test
52 | const token = await getToken()
53 | console.log("checkUserLoggedIn token", token)
54 | }
55 | } catch (err) {
56 | console.log(err);
57 | }
58 | };
59 |
60 | /**
61 | * Retrieve Magic Issued Bearer Token
62 | * This allows User to make authenticated requests
63 | */
64 | const getToken = async () => {
65 | try{
66 | const token = await magic.user.getIdToken()
67 | return token
68 | } catch (err) {
69 | console.log(err)
70 | }
71 | }
72 |
73 | /**
74 | * Reload user login on app refresh
75 | */
76 | useEffect(() => {
77 | magic = new Magic(MAGIC_PUBLIC_KEY)
78 |
79 | checkUserLoggedIn()
80 | }, []);
81 |
82 | return (
83 |
84 | {props.children}
85 |
86 | );
87 | };
88 |
89 |
90 | export default AuthContext
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ecommerce-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@stripe/stripe-js": "^1.11.0",
12 | "magic-sdk": "^3.0.1",
13 | "next": "10.0.1",
14 | "react": "17.0.1",
15 | "react-dom": "17.0.1"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css'
2 | import Header from '../components/Header'
3 | import Footer from '../components/Footer'
4 |
5 | import { AuthProvider } from '../context/AuthContext'
6 |
7 | function MyApp({ Component, pageProps }) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default MyApp
20 |
--------------------------------------------------------------------------------
/pages/account.js:
--------------------------------------------------------------------------------
1 | import { useContext, useState, useEffect } from "react";
2 | import Link from 'next/link'
3 | import Head from 'next/head'
4 |
5 | import AuthContext from "../context/AuthContext";
6 | import { API_URL } from '../utils/urls'
7 |
8 | const useOrders = (user, getToken) => {
9 | const [orders, setOrders] = useState([])
10 | const [loading, setLoading] = useState(false)
11 | useEffect(() => {
12 | const fetchOrders = async () => {
13 | setLoading(true)
14 | if(user){
15 | try{
16 | const token = await getToken()
17 | const orderRes = await fetch(`${API_URL}/orders`, {
18 | headers: {
19 | 'Authorization': `Bearer ${token}`
20 | }
21 | })
22 | const data = await orderRes.json()
23 | setOrders(data)
24 | } catch(err){
25 | setOrders([])
26 | }
27 | }
28 | setLoading(false)
29 | }
30 |
31 | fetchOrders()
32 | }, [user])
33 |
34 |
35 |
36 | return {orders, loading}
37 | }
38 |
39 | export default () => {
40 |
41 | const { user, logoutUser, getToken} = useContext(AuthContext)
42 |
43 | const { orders, loading } = useOrders(user, getToken)
44 |
45 | if(!user){
46 | return (
47 |
48 |
Please Login or Register before accessing this page
49 |
Go Back
50 |
51 | )
52 | }
53 |
54 | return (
55 |
56 |
57 |
Your Account
58 |
59 |
60 |
Account Page
61 |
62 |
63 |
Your Orders
64 | {loading &&
Orders are Loading
}
65 | {orders.map(order => (
66 |
67 | {new Date(order.created_at).toLocaleDateString( 'en-EN' )} {order.product.name} ${order.total} {order.status}
68 |
69 | ))}
70 |
71 |
Logged in as {user.email}
72 |
Logout
73 |
74 | )
75 |
76 | }
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import Link from 'next/link'
3 | import styles from '../styles/Home.module.css'
4 |
5 | import { fromImageToUrl, API_URL } from '../utils/urls'
6 | import { twoDecimals } from '../utils/format'
7 |
8 | export default function Home({ products }) {
9 | return (
10 |
11 |
12 |
Build an Ecommerce with NextJS, Magic, Strapi and Stripe
13 |
14 |
15 |
16 | {products.map(product => (
17 |
31 | ))}
32 |
33 | )
34 | }
35 |
36 | export async function getStaticProps() {
37 | const product_res = await fetch(`${API_URL}/products/`)
38 | const products = await product_res.json()
39 |
40 | return {
41 | props: {
42 | products
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/pages/login.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { useContext, useState } from "react";
3 | import AuthContext from "../context/AuthContext";
4 | import styles from '../styles/Login.module.css'
5 |
6 | export default function Login() {
7 | const [input, setInput] = useState("");
8 | const { loginUser } = useContext(AuthContext);
9 |
10 | const handleSubmit = (e) => {
11 | e.preventDefault()
12 | loginUser(input)
13 | }
14 |
15 | return (
16 |
17 |
18 |
Login
19 |
23 |
24 |
25 | Login
26 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/pages/products/[slug].js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 |
3 | import BuyButton from '../../components/BuyButton'
4 |
5 | import { fromImageToUrl, API_URL } from '../../utils/urls'
6 | import { twoDecimals } from '../../utils/format'
7 |
8 | const Product = ({product}) => {
9 | return (
10 |
11 |
12 |
13 | {product.meta_title &&
14 |
{product.meta_title}
15 | }
16 | {product.meta_description &&
17 |
20 | }
21 |
22 |
23 |
24 |
{product.name}
25 |
})
26 |
{product.name}
27 |
28 |
${twoDecimals(product.price)}
29 |
30 |
31 | {product.content}
32 |
33 |
34 | )
35 | }
36 |
37 | export async function getStaticProps({params: {slug}}) {
38 | const product_res = await fetch(`${API_URL}/products/?slug=${slug}`)
39 | const found = await product_res.json()
40 |
41 | return {
42 | props: {
43 | product: found[0]
44 | }
45 | }
46 | }
47 |
48 | export async function getStaticPaths() {
49 | // Get external data from the file system, API, DB, etc.
50 | const products_res = await fetch(`${API_URL}/products`)
51 | const products = await products_res.json()
52 | return {
53 | paths: products.map(el => ({
54 | params: {slug: String(el.slug)}
55 | })),
56 | fallback: false
57 | };
58 | }
59 |
60 | export default Product
--------------------------------------------------------------------------------
/pages/success.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext } from 'react'
2 | import AuthContext from '../context/AuthContext'
3 | import { useRouter } from 'next/router'
4 | import { API_URL } from '../utils/urls'
5 |
6 | import Link from 'next/link'
7 |
8 | const useOrder = (session_id) => {
9 | const [order, setOrder] = useState(null)
10 | const [loading, setLoading] = useState(null)
11 |
12 | const { getToken, user } = useContext(AuthContext);
13 |
14 | useEffect(() => {
15 | if(user){
16 | const fetchOrder = async () => {
17 | setLoading(true)
18 | try{
19 | const token = await getToken()
20 | const res = await fetch(`${API_URL}/orders/confirm`, {
21 | method: 'POST',
22 | body: JSON.stringify({ checkout_session: session_id }),
23 | headers: {
24 | 'Content-type': 'application/json',
25 | 'Authorization': `Bearer ${token}`
26 | }
27 | })
28 |
29 | const data = await res.json()
30 | setOrder(data)
31 | } catch (err){
32 | setOrder(null)
33 | }
34 | setLoading(false)
35 | }
36 | fetchOrder()
37 | }
38 |
39 | }, [user])
40 |
41 | return { order, loading }
42 | }
43 |
44 | export default function Success(){
45 |
46 | const router = useRouter()
47 | const { session_id } = router.query
48 | const { order, loading } = useOrder(session_id)
49 |
50 | return (
51 |
52 |
Hold on!
53 | { loading &&
We're confirming your purchase!
}
54 | { !loading && order && (
55 |
Your order was processed successfully! View Orders
56 | )}
57 |
58 | )
59 | }
--------------------------------------------------------------------------------
/products.json:
--------------------------------------------------------------------------------
1 | [{"id":1,"name":"The Complete Strapi Course","content":"The complete strapi course, all you need to get started with Strapi ","meta_description":"The complete strapi course, all you need to get started with Strapi ","meta_title":"The Complete Strapi Course, available now - Entreprenerd Store","price":12.00,"published_at":"2020-11-07T14:34:31.643Z","created_at":"2020-11-07T14:34:03.087Z","updated_at":"2020-11-07T15:12:28.819Z","slug":"the-complete-strapi-course","image":{"id":1,"name":"Strapi cover image-01 (4).png","alternativeText":"","caption":"","width":750,"height":422,"formats":{"thumbnail":{"name":"thumbnail_Strapi cover image-01 (4).png","hash":"thumbnail_Strapi_cover_image_01_4_b7395cfee4","ext":".png","mime":"image/png","width":245,"height":138,"size":36.6,"path":null,"url":"/uploads/thumbnail_Strapi_cover_image_01_4_b7395cfee4.png"},"small":{"name":"small_Strapi cover image-01 (4).png","hash":"small_Strapi_cover_image_01_4_b7395cfee4","ext":".png","mime":"image/png","width":500,"height":281,"size":124.17,"path":null,"url":"/uploads/small_Strapi_cover_image_01_4_b7395cfee4.png"}},"hash":"Strapi_cover_image_01_4_b7395cfee4","ext":".png","mime":"image/png","size":218.9,"url":"/uploads/Strapi_cover_image_01_4_b7395cfee4.png","previewUrl":null,"provider":"local","provider_metadata":null,"created_at":"2020-11-07T14:31:08.753Z","updated_at":"2020-11-07T14:31:08.764Z"}},{"id":2,"name":"The NextJS with Strapi and Hooks Course","content":"This is a premium, highly tailored course to get your career to the next level","meta_description":"The Course that will take your skills to the next level","meta_title":"NextJS with Strapi and Hooks Course - Entreprenerd Store","price":497,"published_at":"2020-11-07T14:36:52.000Z","created_at":"2020-11-07T14:36:46.841Z","updated_at":"2020-11-07T15:12:32.878Z","slug":"the-next-js-with-strapi-and-hooks-course","image":{"id":2,"name":"NextJs Strapi Course.png","alternativeText":"","caption":"","width":1920,"height":1080,"formats":{"thumbnail":{"name":"thumbnail_NextJs Strapi Course.png","hash":"thumbnail_Next_Js_Strapi_Course_5b2a28fb11","ext":".png","mime":"image/png","width":245,"height":138,"size":36.62,"path":null,"url":"/uploads/thumbnail_Next_Js_Strapi_Course_5b2a28fb11.png"},"large":{"name":"large_NextJs Strapi Course.png","hash":"large_Next_Js_Strapi_Course_5b2a28fb11","ext":".png","mime":"image/png","width":1000,"height":563,"size":410.22,"path":null,"url":"/uploads/large_Next_Js_Strapi_Course_5b2a28fb11.png"},"medium":{"name":"medium_NextJs Strapi Course.png","hash":"medium_Next_Js_Strapi_Course_5b2a28fb11","ext":".png","mime":"image/png","width":750,"height":422,"size":245.16,"path":null,"url":"/uploads/medium_Next_Js_Strapi_Course_5b2a28fb11.png"},"small":{"name":"small_NextJs Strapi Course.png","hash":"small_Next_Js_Strapi_Course_5b2a28fb11","ext":".png","mime":"image/png","width":500,"height":281,"size":119.22,"path":null,"url":"/uploads/small_Next_Js_Strapi_Course_5b2a28fb11.png"}},"hash":"Next_Js_Strapi_Course_5b2a28fb11","ext":".png","mime":"image/png","size":1114.06,"url":"/uploads/Next_Js_Strapi_Course_5b2a28fb11.png","previewUrl":null,"provider":"local","provider_metadata":null,"created_at":"2020-11-07T14:35:57.746Z","updated_at":"2020-11-07T14:35:57.754Z"}}]
--------------------------------------------------------------------------------
/public/NextJS.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GalloDaSballo/Next-Ecommerce-Frontend/4f288ab81724f8e76cdb2617446b5ff47a22ca9c/public/NextJS.jpg
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GalloDaSballo/Next-Ecommerce-Frontend/4f288ab81724f8e76cdb2617446b5ff47a22ca9c/public/favicon.ico
--------------------------------------------------------------------------------
/public/user_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GalloDaSballo/Next-Ecommerce-Frontend/4f288ab81724f8e76cdb2617446b5ff47a22ca9c/public/user_avatar.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/styles/BuyButton.module.css:
--------------------------------------------------------------------------------
1 | .buy{
2 | background: #000;
3 | border-radius: 2px;
4 | color: #fff;
5 | border: none;
6 | padding: 9px 9px;
7 | font-size: 18px;
8 | font-weight: 700;
9 | }
--------------------------------------------------------------------------------
/styles/Header.module.css:
--------------------------------------------------------------------------------
1 | .title{
2 | text-align: center;
3 | line-height: 24px;
4 | }
5 |
6 | .nav {
7 | position: relative;
8 | }
9 |
10 | .back {
11 | position: absolute;
12 | top: 0;
13 | left: 0;
14 | line-height: 24px;
15 |
16 | cursor: pointer;
17 | }
18 |
19 | .auth {
20 | position: absolute;
21 | right: 0;
22 | top: 0;
23 | }
24 |
25 | .auth img {
26 | width: 25px;
27 | height: 25px;
28 | }
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .product{
2 | padding: 24px;
3 | font-size: 24px;
4 | }
5 |
6 | .product__Rows{
7 | display: flex;
8 | }
9 |
10 | .product__ColImg,
11 | .product__Col{
12 | flex: 1
13 | }
14 |
15 | .product__ColImg{
16 | max-width: 50px;
17 | margin-right: 25px;
18 | }
--------------------------------------------------------------------------------
/styles/Login.module.css:
--------------------------------------------------------------------------------
1 | .input{
2 | width: 100%;
3 | display: inline-block;
4 | margin-bottom: 12px;
5 | font-size: 24px;
6 | }
7 |
8 |
9 | .button{
10 | font-size: 24px;
11 | background: #000000;
12 | color: #ffffff;
13 | border: none;
14 | border-radius: 2px;
15 | display: inline-block;
16 | width: 100%;
17 | padding: 12px;
18 |
19 | margin-bottom: 24px;
20 | }
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: auto;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 |
8 | max-width: 600px;
9 | padding: 42px 24px;
10 | }
11 |
12 | a {
13 | color: inherit;
14 | text-decoration: none;
15 | }
16 |
17 | * {
18 | box-sizing: border-box;
19 | }
20 |
21 | img {
22 | max-width: 100%;
23 | }
24 |
25 | footer {
26 | text-align: center;
27 | }
28 |
29 | h1 {
30 | max-width: calc(100% - 110px);
31 | margin: auto;
32 | line-height: 33px;
33 | }
34 |
35 | .img__yt {
36 | max-width: 200px;
37 | margin-left: 20px;
38 | vertical-align: middle;
39 | }
--------------------------------------------------------------------------------
/utils/format.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Display the number to have 2 digits
3 | * @param {*} number
4 | */
5 | export const twoDecimals = (number) => parseFloat(number).toFixed(2);
--------------------------------------------------------------------------------
/utils/urls.js:
--------------------------------------------------------------------------------
1 | export const API_URL =
2 | process.env.NEXT_PUBLIC_API_URL || "http://localhost:1338"
3 |
4 | export const MAGIC_PUBLIC_KEY = process.env.NEXT_PUBLIC_MAGIC_PUBLIC_KEY || 'pk_test_42B2064C668798B5'
5 |
6 | export const STRIPE_PK = process.env.NEXT_PUBLIC_STRIPE_PK || 'pk_test_42B2064C668798B5'
7 |
8 | /**
9 | * Given a image object return the proper path to display it
10 | * Provides a default as well
11 | * @param {any} image
12 | */
13 | export const fromImageToUrl = (image) => {
14 | if (!image) {
15 | return "/vercel.svg"; //Or default image here
16 | }
17 | if (image.url.indexOf("/") === 0) {
18 | //It's a relative url, add API URL
19 | return `${API_URL}${image.url}`;
20 | }
21 |
22 | return image.url;
23 | };
--------------------------------------------------------------------------------