Skip to content

Commit 9641c84

Browse files
committed
Working ecommerce store with Stripe
1 parent 650d8b3 commit 9641c84

File tree

12 files changed

+479
-23
lines changed

12 files changed

+479
-23
lines changed

.env

+3-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
NEXT_PUBLIC_SANITY_TOKEN = skxjuOeFqGI0VuzMcThXkf1d7QTpygziADzD3J2eLUCtpsVfNsr4kBhmNtOQLqvsMSxDSSne1KQu3BaWiLCHzcntbdcU0pQoP5VrhrIEkfBN6cYt4ZnYG5EzkjOwr1yjkiLZm48AIu1sgCUUcXrL81sTYikwCk0yjAdIwPZKJRp4BjQmclUG
1+
NEXT_PUBLIC_SANITY_TOKEN = skxjuOeFqGI0VuzMcThXkf1d7QTpygziADzD3J2eLUCtpsVfNsr4kBhmNtOQLqvsMSxDSSne1KQu3BaWiLCHzcntbdcU0pQoP5VrhrIEkfBN6cYt4ZnYG5EzkjOwr1yjkiLZm48AIu1sgCUUcXrL81sTYikwCk0yjAdIwPZKJRp4BjQmclUG
2+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = pk_test_eP2cRwz7fGgtas9VeuDxzdYC
3+
NEXT_PUBLIC_STRIPE_SECRET_KEY = sk_test_DViS0er3bSbkW9WzaGNFvn4R

components/Cart.jsx

+139-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,143 @@
1-
import React from 'react'
1+
import React, { useRef } from "react";
2+
import Link from "next/link";
3+
import {
4+
AiOutlineMinus,
5+
AiOutlinePlus,
6+
AiOutlineShopping,
7+
AiOutlineLeft,
8+
} from "react-icons/ai";
9+
import { TiDeleteOutline } from "react-icons/ti";
10+
import toast from "react-hot-toast";
11+
12+
import { useStateContext } from "../context/StateContext";
13+
import { urlFor } from "../lib/client";
14+
import getStripe from "../lib/getStripe";
215

316
const Cart = () => {
17+
const cartRef = useRef();
18+
const {
19+
totalPrice,
20+
totalQuantities,
21+
cartItems,
22+
setShowCart,
23+
toggleCartItemQuantity,
24+
onRemove,
25+
} = useStateContext();
26+
27+
const handleCheckout = async () => {
28+
const stripe = await getStripe();
29+
30+
const response = await fetch("/api/stripe", {
31+
method: "POST",
32+
headers: {
33+
"Content-Type": "application/json",
34+
},
35+
body: JSON.stringify(cartItems),
36+
});
37+
38+
if (response.statusCode === 500) return;
39+
40+
const data = await response.json();
41+
42+
toast.loading("Redirecting...");
43+
44+
stripe.redirectToCheckout({ sessionId: data.id });
45+
};
46+
447
return (
5-
<div>Cart</div>
6-
)
7-
}
48+
<div className="cart-wrapper" ref={cartRef}>
49+
<div className="cart-container">
50+
<button
51+
type="button"
52+
className="cart-heading"
53+
onClick={() => setShowCart(false)}
54+
>
55+
<AiOutlineLeft />
56+
<span className="heading">Your Cart</span>
57+
<span className="cart-num-items">({totalQuantities} items)</span>
58+
</button>
59+
60+
{cartItems.length < 1 && (
61+
<div className="empty-cart">
62+
<AiOutlineShopping size={150} />
63+
<h3>Your shopping bag is empty.</h3>
64+
<Link href="/">
65+
<button
66+
type="button"
67+
className="btn"
68+
onClick={() => setShowCart(false)}
69+
>
70+
Continue Shopping
71+
</button>
72+
</Link>
73+
</div>
74+
)}
75+
76+
<div className="product-container">
77+
{cartItems.length >= 1 &&
78+
cartItems.map((item, index) => (
79+
<div className="product" key={item._id}>
80+
<img
81+
src={urlFor(item?.image[0])}
82+
className="cart-product-image"
83+
/>
84+
<div className="item-desc">
85+
<div className="flex top">
86+
<h5>{item.name}</h5>
87+
<h4>${item.price}</h4>
88+
</div>
89+
<div className="flex bottom">
90+
<div>
91+
<p className="quantity-desc">
92+
<span
93+
className="minus"
94+
onClick={() =>
95+
toggleCartItemQuantity(item._id, "dec")
96+
}
97+
>
98+
<AiOutlineMinus />
99+
</span>
100+
<span className="num" onClick="">
101+
{item.quantity}
102+
</span>
103+
<span
104+
className="plus"
105+
onClick={() =>
106+
toggleCartItemQuantity(item._id, "inc")
107+
}
108+
>
109+
<AiOutlinePlus />
110+
</span>
111+
</p>
112+
</div>
113+
<button
114+
type="button"
115+
className="remove-item"
116+
onClick={() => onRemove(item)}
117+
>
118+
<TiDeleteOutline />
119+
</button>
120+
</div>
121+
</div>
122+
</div>
123+
))}
124+
</div>
125+
{cartItems.length >= 1 && (
126+
<div className="cart-bottom">
127+
<div className="total">
128+
<h3>Subtotal:</h3>
129+
<h3>${totalPrice.toFixed(2)}</h3>
130+
</div>
131+
<div className="btn-container">
132+
<button type="button" className="btn" onClick={handleCheckout}>
133+
Pay with Stripe
134+
</button>
135+
</div>
136+
</div>
137+
)}
138+
</div>
139+
</div>
140+
);
141+
};
8142

9-
export default Cart
143+
export default Cart;

components/HeroBanner.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const HeroBanner = ({ heroBanner }) => {
1616
className="hero-banner-image"
1717
/>
1818
<div>
19-
<Link href={`/product/heroBanner.product`}>
19+
<Link href={`/product/${heroBanner.product}`}>
2020
<button type="button">{heroBanner.buttonText}</button>
2121
</Link>
2222
<div className="desc">

components/Navbar.jsx

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,27 @@ import React from "react";
22
import Link from "next/link";
33
import { AiOutlineShopping } from "react-icons/ai";
44

5+
import Cart from "./Cart";
6+
import { useStateContext } from "../context/StateContext";
7+
58
const Navbar = () => {
9+
const { showCart, setShowCart, totalQuantities } = useStateContext();
610
return (
711
<div className="navbar-container">
812
<p className="logo">
9-
<Link href='/'>ReCommerce</Link>
13+
<Link href="/">ReCommerce</Link>
1014
</p>
1115

12-
<button type='button' className="cart-icon">
16+
<button
17+
type="button"
18+
className="cart-icon"
19+
onClick={() => setShowCart(true)}
20+
>
1321
<AiOutlineShopping />
14-
<span className="cart-item-qty">1</span>
22+
<span className="cart-item-qty">{totalQuantities}</span>
1523
</button>
24+
25+
{showCart && <Cart />}
1626
</div>
1727
);
1828
};

context/StateContext.js

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { createContext, useContext, useState, useEffect } from "react";
2+
import { toast } from "react-hot-toast";
3+
4+
const Context = createContext();
5+
6+
export const StateContext = ({ children }) => {
7+
const [showCart, setShowCart] = useState(false);
8+
const [cartItems, setCartItems] = useState([]);
9+
const [totalPrice, setTotalPrice] = useState(0);
10+
const [totalQuantities, setTotalQuantities] = useState(0);
11+
const [qty, setQty] = useState(1);
12+
13+
let foundProduct;
14+
let index;
15+
16+
const incQty = () => {
17+
setQty((prevQty) => prevQty + 1);
18+
};
19+
20+
const decQty = () => {
21+
setQty((prevQty) => {
22+
if (prevQty - 1 < 1) return 1;
23+
24+
return prevQty - 1;
25+
});
26+
};
27+
28+
const onAdd = (product, quantity) => {
29+
const checkProductInCart = cartItems.find(
30+
(item) => item._id === product._id
31+
);
32+
33+
setTotalPrice(
34+
(prevTotalPrice) => prevTotalPrice + product.price * quantity
35+
);
36+
setTotalQuantities((prevTotalQuantities) => prevTotalQuantities + quantity);
37+
38+
if (checkProductInCart) {
39+
const updatedCartItems = cartItems.map((cartProduct) => {
40+
if (cartProduct._id === product._id)
41+
return {
42+
...cartProduct,
43+
quantity: cartProduct.quantity + quantity,
44+
};
45+
});
46+
47+
setCartItems(updatedCartItems);
48+
} else {
49+
product.quantity = quantity;
50+
setCartItems([...cartItems, { ...product }]);
51+
}
52+
toast.success(`${qty} ${product.name} added to the cart.`);
53+
};
54+
55+
const onRemove = (product) => {
56+
foundProduct = cartItems.find((item) => item._id === product._id);
57+
const newCartItems = cartItems.filter((item) => item._id !== product._id);
58+
59+
setTotalPrice(
60+
(prevTotalPrice) =>
61+
prevTotalPrice - foundProduct.price * foundProduct.quantity
62+
);
63+
64+
setTotalQuantities(
65+
(prevTotalQuantities) => prevTotalQuantities - foundProduct.quantity
66+
);
67+
setCartItems(newCartItems);
68+
};
69+
70+
const toggleCartItemQuantity = (id, value) => {
71+
foundProduct = cartItems.find((item) => item._id === id);
72+
index = cartItems.findIndex((product) => product._id === id);
73+
const newCartItems = cartItems.filter((item) => item._id !== id);
74+
75+
if (value === "inc") {
76+
setCartItems([
77+
...newCartItems,
78+
{ ...foundProduct, quantity: foundProduct.quantity + 1 },
79+
]);
80+
setTotalPrice((prevTotalPrice) => prevTotalPrice + foundProduct.price);
81+
setTotalQuantities((prevTotalQuantities) => prevTotalQuantities + 1);
82+
} else if (value === "dec") {
83+
if (foundProduct.quantity > 1) {
84+
setCartItems([
85+
...newCartItems,
86+
{ ...foundProduct, quantity: foundProduct.quantity - 1 },
87+
]);
88+
setTotalPrice((prevTotalPrice) => prevTotalPrice - foundProduct.price);
89+
setTotalQuantities((prevTotalQuantities) => prevTotalQuantities - 1);
90+
}
91+
}
92+
};
93+
94+
return (
95+
<Context.Provider
96+
value={{
97+
showCart,
98+
setShowCart,
99+
setCartItems,
100+
setTotalPrice,
101+
setTotalQuantities,
102+
cartItems,
103+
totalPrice,
104+
totalQuantities,
105+
qty,
106+
incQty,
107+
decQty,
108+
onAdd,
109+
onRemove,
110+
toggleCartItemQuantity,
111+
}}
112+
>
113+
{children}
114+
</Context.Provider>
115+
);
116+
};
117+
118+
export const useStateContext = () => useContext(Context);

lib/getStripe.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { loadStripe } from "@stripe/stripe-js";
2+
3+
let stripePromise;
4+
5+
const getStripe = () => {
6+
if (!stripePromise) {
7+
stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
8+
}
9+
10+
return stripePromise;
11+
};
12+
13+
export default getStripe;

lib/utils.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import confetti from "canvas-confetti";
2+
3+
export const runFireworks = () => {
4+
var duration = 5 * 1000;
5+
var animationEnd = Date.now() + duration;
6+
var defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
7+
8+
function randomInRange(min, max) {
9+
return Math.random() * (max - min) + min;
10+
}
11+
12+
var interval = setInterval(function () {
13+
var timeLeft = animationEnd - Date.now();
14+
15+
if (timeLeft <= 0) {
16+
return clearInterval(interval);
17+
}
18+
19+
var particleCount = 50 * (timeLeft / duration);
20+
// since particles fall down, start a bit higher than random
21+
confetti(
22+
Object.assign({}, defaults, {
23+
particleCount,
24+
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
25+
})
26+
);
27+
confetti(
28+
Object.assign({}, defaults, {
29+
particleCount,
30+
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
31+
})
32+
);
33+
}, 250);
34+
};

pages/_app.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import React from "react";
22

33
import { Layout } from "../components";
44
import "../styles/globals.css";
5+
import { StateContext } from "../context/StateContext";
6+
import { Toaster } from "react-hot-toast";
57

68
function MyApp({ Component, pageProps }) {
79
return (
8-
<Layout>
9-
<Component {...pageProps} />
10-
</Layout>
10+
<StateContext>
11+
<Layout>
12+
<Toaster />
13+
<Component {...pageProps} />
14+
</Layout>
15+
</StateContext>
1116
);
1217
}
1318

0 commit comments

Comments
 (0)