Removed redux, swapped to Zustand instead

This commit is contained in:
Jacob Delgado 2024-12-01 00:06:30 -08:00
parent 0c45e9067d
commit 406890c2aa
26 changed files with 597 additions and 246 deletions

Binary file not shown.

View File

@ -14,6 +14,9 @@
"@react-email/components": "^0.0.22", "@react-email/components": "^0.0.22",
"@react-email/tailwind": "^0.0.19", "@react-email/tailwind": "^0.0.19",
"@reduxjs/toolkit": "^2.3.0", "@reduxjs/toolkit": "^2.3.0",
"@stripe/react-stripe-js": "^3.0.0",
"@stripe/stripe-js": "^5.2.0",
"@types/stripe": "^8.0.417",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
@ -34,7 +37,8 @@
"sharp": "^0.33.5", "sharp": "^0.33.5",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0" "usehooks-ts": "^3.1.0",
"zustand": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.16.1", "@types/node": "^20.16.1",

View File

@ -1,18 +0,0 @@
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '../lib/Store'
export default function StoreProvider({
children
}: {
children: React.ReactNode
}) {
const storeRef = useRef<AppStore>()
if (!storeRef.current) {
// Create the store instance the first time this renders
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}

View File

@ -3,8 +3,7 @@ import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import Navbar from "@/components/Navbar/Navbar"; import Navbar from "@/components/Navbar/Navbar";
import Footer from "@/components/Footer/Footer"; import Footer from "@/components/Footer/Footer";
import StoreProvider from "./StoreProvider"; import { CartProvider } from '@/components/CartTab/CartProvider';
const inter = Inter({ subsets: ["latin"] }); const inter = Inter({ subsets: ["latin"] });
@ -19,14 +18,16 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<StoreProvider> <html lang="en">
<html lang="en"> <body
<body className={`${inter.className} bg-slate-700 w-full max-h-screen text-white`}> className={`${inter.className} bg-slate-700 w-full max-h-screen text-white`}
>
<CartProvider>
<Navbar /> <Navbar />
{children} {children}
<Footer /> <Footer />
</body> </CartProvider>
</html> </body>
</StoreProvider> </html>
); );
} }

View File

@ -1,42 +0,0 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface Product {
id: number;
title: string;
price: number;
quantity: number;
desc: string;
image: string;
}
const initialState = {
products: [],
}
export const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addToCart: (state: any, action: PayloadAction<Product>) => {
const item = state.products.find((p:Product) => p.id === action.payload.id);
if(item) {
item.quantity += action.payload.quantity;
} else {
state.products.push(action.payload);
}
},
removeFromCart: (state, action) => {
state.products = state.products.filter((p:Product) => p.id !== action.payload);
},
resetCart: (state) => {
state.products = [];
},
},
});
export const { addToCart, removeFromCart, resetCart } = cartSlice.actions;
export default cartSlice.reducer;
export const dynamic = 'force-dynamic'

View File

@ -1,37 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '@/lib/Store'
// Define a type for the slice state
export interface CounterState {
value: number
}
// Define the initial state using that type
const initialState: CounterState = {
value: 0
}
export const counterSlice = createSlice({
name: 'counter',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
increment: state => {
state.value += 1
},
decrement: state => {
state.value -= 1
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counter.value
export default counterSlice.reducer

View File

@ -0,0 +1,21 @@
// src/app/(routes)/cart/page.tsx
'use client';
import { useCartStore } from '@/lib/store/cart-store';
import StripeCheckout from '@/lib/stripe/checkout';
export default function CartPage() {
const { items, getTotalPrice } = useCartStore();
return (
<div>
<h1>Your Cart</h1>
{items.map(item => (
<div key={item.id}>
{item.title} - ${item.price} x {item.quantity}
</div>
))}
<StripeCheckout />
</div>
);
}

View File

@ -0,0 +1,26 @@
import { Button } from '@/components/ui/button'
import { useCartStore } from '@/lib/store/cart-store'
import { Product } from '../../lib/types/product'
interface AddToCartButtonProps {
product: Product
className?: string
}
export function AddToCartButton({ product, className }: AddToCartButtonProps) {
const addToCart = useCartStore((state) => state.addToCart)
const handleAddToCart = () => {
addToCart(product)
}
return (
<Button
onClick={handleAddToCart}
className={className}
variant="default"
>
Add to Cart
</Button>
)
}

View File

@ -0,0 +1,16 @@
// src/components/cart/CartProvider.tsx
'use client';
import { useEffect } from 'react';
import { useCartStore } from '@/lib/store/cart-store';
export function CartProvider({ children }: { children: React.ReactNode }) {
const { items } = useCartStore();
// Optional: Hydration fix for persistent stores
useEffect(() => {
useCartStore.persist.rehydrate();
}, []);
return <>{children}</>;
}

View File

@ -0,0 +1,154 @@
import React from 'react';
import { motion } from 'framer-motion';
import type { CartItem } from '@/lib/store/cart-store';
import Image from 'next/image';
interface CartSidebarProps {
items: CartItem[];
isOpen: boolean;
onClose: () => void;
updateQuantity: (id: string, quantity: number) => void;
removeItem: (id: string) => void;
}
const CartSidebar: React.FC<CartSidebarProps> = ({
isOpen,
onClose,
items,
updateQuantity,
removeItem,
}) => {
// Count occurrences of each item ID
const itemCounts = items.reduce((acc, item) => {
acc[item.id] = (acc[item.id] || 0) + 1;
return acc;
}, {} as Record<string, number>);
// Create unique items array with correct total quantities
const uniqueItems = Array.from(new Set(items.map(item => item.id)))
.map(id => {
const item = items.find(item => item.id === id)!;
return {
...item,
quantity: item.quantity * itemCounts[item.id]
};
});
// Update total calculation to use uniqueItems
const total = uniqueItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
// Add console.log to debug
const handleRemoveItem = (itemId: string) => {
console.log('Removing item:', itemId);
removeItem(itemId);
}
return (
<>
{/* Overlay */}
{isOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40"
onClick={onClose}
/>
)}
{/* Sidebar */}
<motion.div
className="fixed right-0 top-0 h-full w-80 bg-[#E8E4D9] shadow-lg z-50 text-black"
initial={{ x: '100%' }}
animate={{ x: isOpen ? 0 : '100%' }}
transition={{ type: 'tween', duration: 0.3 }}
>
<div className="p-4">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Your Cart</h2>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-full"
>
<span className="sr-only">Close cart</span>
<svg
className="w-6 h-6"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Cart Items Container */}
<div className="flex-1 overflow-y-auto max-h-[60vh]">
{uniqueItems.length === 0 ? (
<p className="text-gray-500 text-center">Your cart is empty</p>
) : (
uniqueItems.map((item) => (
<div key={item.id} className="flex items-center py-4 border-b">
<div className="w-20 h-20 flex-shrink-0 mr-4 relative bg-black/20">
<Image
src={`${process.env.NEXT_PUBLIC_UPLOAD_URL + item.image}`}
alt={item.title}
fill
className="object-cover"
sizes="80px"
/>
</div>
<div className="flex-1">
<h3 className="font-medium">{item.title}</h3>
<p className="text-gray-600">${item.price.toFixed(2)}</p>
<div className="flex items-center mt-2">
<button
className="px-2 py-1 border rounded"
onClick={() => {
const newQuantity = Math.max(0, item.quantity - 1);
updateQuantity(item.id, newQuantity);
}}
>
-
</button>
<span className="mx-2">{item.quantity}</span>
<button
className="px-2 py-1 border rounded"
onClick={() => {
updateQuantity(item.id, item.quantity + 1);
}}
>
+
</button>
<button
className="ml-4 text-red-500"
onClick={() => handleRemoveItem(item.id)}
>
Remove
</button>
</div>
</div>
</div>
))
)}
</div>
{/* Cart Footer */}
<div className="border-t mt-4 pt-4">
<div className="flex justify-between mb-4">
<span className="font-semibold">Total:</span>
<span className="font-semibold">${total.toFixed(2)}</span>
</div>
<button
className="w-full bg-[#F45B1E] text-white py-2 px-4 rounded-md hover:bg-gray-800"
>
Checkout
</button>
</div>
</div>
</motion.div>
</>
);
};
export default CartSidebar;

View File

@ -1,83 +1,83 @@
"use client"; // "use client";
import React from "react"; // import React from "react";
import Image from "next/image"; // import Image from "next/image";
import { MdDeleteOutline } from "react-icons/md"; // import { MdDeleteOutline } from "react-icons/md";
import { Button } from "@/components/ui/button"; // import { Button } from "@/components/ui/button";
import "./CartTab.css"; // import "./CartTab.css";
import { useSelector, useDispatch } from "react-redux"; // import { useSelector, useDispatch } from "react-redux";
import type { RootState } from "@/lib/Store"; // import type { RootState } from "@/lib/Store";
import { removeFromCart } from "@/app/redux/CartReducer"; // import { removeFromCart } from "@/app/redux/CartReducer";
import type { Product } from "@/app/redux/CartReducer"; // import type { Product } from "@/app/redux/CartReducer";
const CartTab = () => { // const CartTab = () => {
const cartItems = true; // const cartItems = true;
const dispatch = useDispatch(); // const dispatch = useDispatch();
// get the products from the cart // // get the products from the cart
const products = useSelector((state: RootState) => state.cart.products); // const products = useSelector((state: RootState) => state.cart.products);
const totalPrice = () => { // const totalPrice = () => {
let total = 0; // let total = 0;
products?.forEach((item: Product) => { // products?.forEach((item: Product) => {
total += item.price * item.quantity; // total += item.price * item.quantity;
}); // });
return total.toFixed(2); // return total.toFixed(2);
}; // };
// const data = [ // // const data = [
// { // // {
// id: 1, // // id: 1,
// title: "Product 1", // // title: "Product 1",
// oldPrice: 19, // // oldPrice: 19,
// price: 12, // // price: 12,
// isNew: true, // // isNew: true,
// desc: "Short sleeve t-shirt", // // desc: "Short sleeve t-shirt",
// image: // // image:
// "https://ih1.redbubble.net/image.2997405810.0746/ssrco,slim_fit_t_shirt,womens,101010:01c5ca27c6,front,square_product,600x600.jpg", // // "https://ih1.redbubble.net/image.2997405810.0746/ssrco,slim_fit_t_shirt,womens,101010:01c5ca27c6,front,square_product,600x600.jpg",
// }, // // },
// { // // {
// id: 2, // // id: 2,
// title: "Product 2", // // title: "Product 2",
// oldPrice: 19, // // oldPrice: 19,
// price: 12, // // price: 12,
// isNew: false, // // isNew: false,
// desc: "Short sleeve t-shirt", // // desc: "Short sleeve t-shirt",
// image: // // image:
// "https://ih1.redbubble.net/image.2997405810.0746/ssrco,slim_fit_t_shirt,flatlay,101010:01c5ca27c6,front,wide_portrait,750x1000-bg,f8f8f8.jpg", // // "https://ih1.redbubble.net/image.2997405810.0746/ssrco,slim_fit_t_shirt,flatlay,101010:01c5ca27c6,front,wide_portrait,750x1000-bg,f8f8f8.jpg",
// }, // // },
// ]; // // ];
return ( // return (
<div className="cart absolute p-5 text-slate-600 top-28 right-0 bg-slate-200"> // <div className="cart absolute p-5 text-slate-600 top-28 right-0 bg-slate-200">
{products?.map((item: Product) => ( // {products?.map((item: Product) => (
<div> // <div key={item.id}>
<div className="item flex gap-5 items-center mb-[30px] " key={item.id}> // <div className="item flex gap-5 items-center mb-[30px]">
<div className="relative w-[80px] h-[100px]"> // <div className="relative w-[80px] h-[100px]">
<Image // <Image
src={`${process.env.NEXT_PUBLIC_UPLOAD_URL + item.image}`} // src={`${process.env.NEXT_PUBLIC_UPLOAD_URL + item.image}`}
fill // fill
style={{ objectFit: "cover" }} // style={{ objectFit: "cover" }}
className="" // className=""
alt="item picture" // alt="item picture"
/> // />
</div> // </div>
<div className="details"> // <div className="details">
<h1>{item.title}</h1> // <h1>{item.title}</h1>
{/* <p className="text-sm">{item.desc?.substring(0, 100)}</p> */} // {/* <p className="text-sm">{item.desc?.substring(0, 100)}</p> */}
<div className="price">{item.quantity} x ${item.price}</div> // <div className="price">{item.quantity} x ${item.price}</div>
</div> // </div>
<MdDeleteOutline className="delete-icon mt-16" onClick={() => dispatch(removeFromCart(item.id))} /> // <MdDeleteOutline className="delete-icon mt-16" onClick={() => dispatch(removeFromCart(item.id))} />
</div> // </div>
<div className="total"> // <div className="total">
<span>SUBTOTAL</span> // <span>SUBTOTAL</span>
<span>${totalPrice()}</span> // <span>${totalPrice()}</span>
</div> // </div>
</div> // </div>
))} // ))}
<Button className="bg-[#F45B1E] text-white checkout rounded-none w-full">PROCEED TO CHECKOUT</Button> // <Button className="bg-[#F45B1E] text-white checkout rounded-none w-full">PROCEED TO CHECKOUT</Button>
{/* <span className="reset">Reset Cart</span> */} // {/* <span className="reset">Reset Cart</span> */}
</div> // </div>
); // );
}; // };
export default CartTab; // export default CartTab;

View File

@ -4,11 +4,9 @@ import { useState, useEffect } from "react";
import { useMediaQuery } from "@/util/useMediaQuery"; import { useMediaQuery } from "@/util/useMediaQuery";
import { FiShoppingCart } from "react-icons/fi"; import { FiShoppingCart } from "react-icons/fi";
import Image from "next/image"; import Image from "next/image";
import CartTab from "@/components/CartTab/CartTab"; import { useCartStore } from '@/lib/store/cart-store';
import { useSelector } from "react-redux";
import type { RootState } from "@/lib/Store";
import Products from "@/app/products/page"; import Products from "@/app/products/page";
import type { Product } from "@/app/redux/CartReducer"; import CartSidebar from '@/components/CartTab/CartSidebar';
const navMotion = { const navMotion = {
visible: { visible: {
@ -35,7 +33,11 @@ export default function Nav() {
// console.log(matches); // console.log(matches);
// get the products from the cart // get the products from the cart
const products = useSelector((state: RootState) => state.cart.products); // const products = useSelector((state: RootState) => state.cart.products);
const totalItems = useCartStore((state) => state.items.length);
const [isCartOpen, setIsCartOpen] = useState(false);
const { items } = useCartStore();
return ( return (
<nav className="relative mx-8 mb-10 flex justify-between items-center pt-12 font-medium md:mx-16 lg:mx-32 text-white"> <nav className="relative mx-8 mb-10 flex justify-between items-center pt-12 font-medium md:mx-16 lg:mx-32 text-white">
@ -83,22 +85,25 @@ export default function Nav() {
<a href="/">Home</a> <a href="/">Home</a>
<a href="/products">Shirts</a> <a href="/products">Shirts</a>
<a href="/about">About</a> <a href="/about">About</a>
<div className="flex relative items-center justify-center" onClick={() => setOpen(!open)}> <div
className="flex relative items-center justify-center"
onClick={() => setIsCartOpen(!isCartOpen)}
>
<FiShoppingCart className="text-lg lg:text-xl" /> <FiShoppingCart className="text-lg lg:text-xl" />
<span className="absolute text-white text-xs w-5 h-5 rounded-[50%] bg-orange-600 -top-2.5 -right-2.5 flex items-center justify-center "> <span className="absolute text-white text-xs w-5 h-5 rounded-[50%] bg-orange-600 -top-2.5 -right-2.5 flex items-center justify-center">
{products?.map((item: Product) => { {totalItems}
if (item.quantity > 0 && item.quantity !== undefined) {
return <div key={item.id}>{item.quantity}</div>;
} else {
return <div>0</div>;
}
})}
</span> </span>
</div> </div>
</div> </div>
)} )}
{open && <CartTab />} <CartSidebar
items={items}
isOpen={isCartOpen}
onClose={() => setIsCartOpen(false)}
updateQuantity={useCartStore.getState().updateQuantity}
removeItem={useCartStore.getState().removeFromCart}
/>
{!matches && ( {!matches && (
<div <div

View File

@ -9,9 +9,8 @@ import { MdFavoriteBorder } from "react-icons/md";
import { FaBalanceScale } from "react-icons/fa"; import { FaBalanceScale } from "react-icons/fa";
import { FaRegStar } from "react-icons/fa"; import { FaRegStar } from "react-icons/fa";
import { FaStar } from "react-icons/fa"; import { FaStar } from "react-icons/fa";
import { useSelector, useDispatch } from "react-redux"; import { useCartStore } from '@/lib/store/cart-store';
import { RootState } from "@/lib/Store"; import { AddToCartButton } from "@/components/CartTab/AddToCartButton";
import { addToCart } from "@/app/redux/CartReducer";
@ -33,8 +32,8 @@ const ProductPage = ({ productId }: any) => {
const [salePrice, setSalePrice] = useState(0); const [salePrice, setSalePrice] = useState(0);
const [category, setCategory] = useState(''); const [category, setCategory] = useState('');
// Redux // Replace Redux dispatch with Zustand store
const dispatch = useDispatch() const addToCart = useCartStore((state) => state.addToCart);
useEffect(() => { useEffect(() => {
@ -126,20 +125,16 @@ const ProductPage = ({ productId }: any) => {
+ +
</Button> </Button>
</div> </div>
<Button className="add bg-[#F45B1E] font-bold" <AddToCartButton
onClick={() => { product={{
dispatch(addToCart({ id: productId,
id: productId, title: name,
title: name, price: price,
price: price, desc: desc,
desc: desc, image: product,
image: product, quantity: quantity
quantity: quantity }}
})) />
}}
>
<MdOutlineAddShoppingCart /> ADD TO CART
</Button>
<div className="link"> <div className="link">
{/* <div className="item text-emerald-400"><MdFavoriteBorder /> ADD TO WISHLIST</div> */} {/* <div className="item text-emerald-400"><MdFavoriteBorder /> ADD TO WISHLIST</div> */}
{/* if in wishlist, then display <FaStar /> else <FaRegStar /> */} {/* if in wishlist, then display <FaStar /> else <FaRegStar /> */}

View File

View File

@ -1,28 +0,0 @@
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '@/app/redux/slice'
import cartReducer from '@/app/redux/CartReducer'
import { persistReducer, persistStore } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const persistConfig = {
key: 'cart',
storage,
}
const persistedReducer = persistReducer(persistConfig, cartReducer)
export const makeStore = () => {
return configureStore({
reducer: {
cart: cartReducer,
}
})
}
export const persistor = persistStore(makeStore());
// Infer the type of makeStore
export type AppStore = ReturnType<typeof makeStore>
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']

View File

@ -0,0 +1,75 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface CartItem {
id: string;
title: string;
price: number;
desc: string;
image: string;
quantity: number;
}
interface CartStore {
items: CartItem[];
updateQuantity: (id: string, quantity: number) => void;
getTotalPrice: ({ items }: { items: CartItem[] }) => number;
addToCart: (item: CartItem) => void;
clearCart: () => void;
removeFromCart: (itemId: string) => void;
// Add other cart operations as needed
}
export const useCartStore = create(
persist<CartStore>(
(set) => ({
items: [],
addToCart: (item) => set((state) => ({
items: [...state.items, item]
})),
getTotalPrice: ({ items }) => {
return items.reduce((total, item) => total + (item.price * item.quantity), 0);
},
clearCart: () => set({ items: [] }),
removeFromCart: (itemId) => set((state) => ({
items: state.items.filter(item => item.id !== itemId)
})),
updateQuantity: (id, quantity) =>
set((state) => {
if (quantity === 0) {
// Remove all items with this id if quantity is 0
return {
items: state.items.filter(item => item.id !== id)
};
}
// Count how many of this item exist
const itemCount = state.items.filter(item => item.id === id).length;
if (quantity > itemCount) {
// Add new items
const itemTemplate = state.items.find(item => item.id === id)!;
const newItems = Array(quantity - itemCount)
.fill(null)
.map(() => ({ ...itemTemplate, quantity: 1 }));
return {
items: [...state.items, ...newItems]
};
} else {
// Remove excess items
const itemsToKeep = state.items.filter(item => item.id !== id)
.concat(state.items.filter(item => item.id === id)
.slice(0, quantity));
return {
items: itemsToKeep
};
}
}),
}),
{
name: 'cart-storage',
}
)
);

View File

@ -0,0 +1,73 @@
'use client';
import React, { useState, FormEvent } from 'react';
import {
PaymentElement,
useStripe,
useElements
} from '@stripe/react-stripe-js';
import { StripePaymentElementOptions } from '@stripe/stripe-js';
import { useCartStore } from '@/lib/store/cart-store';
export default function CheckoutForm() {
const stripe = useStripe();
const elements = useElements();
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [message, setMessage] = useState<string | null>(null);
const { clearCart } = useCartStore();
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!stripe || !elements) {
return;
}
setIsProcessing(true);
try {
const { error } = await stripe.confirmPayment({
elements,
redirect: 'if_required',
confirmParams: {
return_url: `${window.location.origin}/payment-confirmation`,
},
});
if (error) {
setMessage(error.message || 'An unexpected error occurred.');
setIsProcessing(false);
} else {
clearCart();
setIsProcessing(false);
}
} catch (err) {
console.error('Payment confirmation error:', err);
setMessage('An unexpected error occurred.');
setIsProcessing(false);
}
};
const paymentElementOptions: StripePaymentElementOptions = {
layout: 'tabs',
};
return (
<form onSubmit={handleSubmit}>
<PaymentElement options={paymentElementOptions} />
<button
className="w-full py-2 bg-blue-500 text-white rounded"
disabled={isProcessing || !stripe || !elements}
type="submit"
>
{isProcessing ? 'Processing...' : 'Pay Now'}
</button>
{message && (
<div className="text-red-500 mt-2">
{message}
</div>
)}
</form>
);
}

View File

@ -0,0 +1,38 @@
'use client';
import React from 'react';
import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';
import CheckoutForm from './checkout-form';
import { useCartStore } from '@/lib/store/cart-store';
// Non-null assertion as we'll validate the key
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
export default function StripeCheckout() {
const { items, getTotalPrice } = useCartStore();
// Validate Stripe key at runtime
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
return <div>Stripe configuration error</div>;
}
const checkoutOptions = {
mode: 'payment' as const,
amount: Math.round(getTotalPrice({ items }) * 100), // Convert to cents
currency: 'usd',
};
return (
<Elements
stripe={stripePromise}
options={{
mode: 'payment',
amount: checkoutOptions.amount,
currency: checkoutOptions.currency
}}
>
<CheckoutForm />
</Elements>
);
}

View File

View File

@ -0,0 +1,8 @@
export interface Product {
id: string;
title: string;
price: number;
desc: string;
image: string;
quantity: number;
}

View File

@ -0,0 +1,32 @@
import { create } from 'zustand'
interface Product {
id: number;
name: string;
price: number;
quantity: number;
// add other product properties as needed
}
interface CartStore {
products: Product[];
addToCart: (product: Product) => void;
removeFromCart: (productId: number) => void;
getTotalItems: () => number;
}
export const useCartStore = create<CartStore>((set, get) => ({
products: [],
addToCart: (product) =>
set((state) => ({
products: [...state.products, product],
})),
removeFromCart: (productId) =>
set((state) => ({
products: state.products.filter((p) => p.id !== productId),
})),
getTotalItems: () => {
const { products } = get();
return products.reduce((total, item) => total + item.quantity, 0);
},
}));

View File

@ -0,0 +1,18 @@
import { Stripe } from '@stripe/stripe-js';
export interface StripeCheckoutOptions {
mode: 'payment' | 'subscription';
amount: number;
currency: string;
}
export interface PaymentIntentResponse {
clientSecret: string;
status: 'requires_payment_method' | 'requires_confirmation' | 'requires_action' | 'succeeded' | 'canceled';
}
export interface CreatePaymentIntentParams {
amount: number;
currency: string;
payment_method_types?: string[];
}

View File

@ -18,7 +18,10 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"],
"@/components/*": ["src/components/*"],
"@/types/*": ["src/types/*"],
"@/hooks/*": ["src/hooks/*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/StoreProvider.ts", "src/app/redux/store.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/StoreProvider.ts", "src/app/redux/store.ts"],

7
lib/store/cart-store.ts Normal file
View File

@ -0,0 +1,7 @@
export type CartItem = {
id: string;
name: string;
price: number;
quantity: number;
imageUrl: string;
};