Removed redux, swapped to Zustand instead
This commit is contained in:
parent
0c45e9067d
commit
406890c2aa
Binary file not shown.
@ -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",
|
||||||
|
|||||||
@ -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>
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
|
||||||
@ -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
|
|
||||||
21
frontend/src/app/routes/cart/page.tsx
Normal file
21
frontend/src/app/routes/cart/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
frontend/src/app/routes/checkout/page.tsx
Normal file
0
frontend/src/app/routes/checkout/page.tsx
Normal file
26
frontend/src/components/CartTab/AddToCartButton.tsx
Normal file
26
frontend/src/components/CartTab/AddToCartButton.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
frontend/src/components/CartTab/CartProvider.tsx
Normal file
16
frontend/src/components/CartTab/CartProvider.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
154
frontend/src/components/CartTab/CartSidebar.tsx
Normal file
154
frontend/src/components/CartTab/CartSidebar.tsx
Normal 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;
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 /> */}
|
||||||
|
|||||||
0
frontend/src/hooks/use-cart.tsx
Normal file
0
frontend/src/hooks/use-cart.tsx
Normal 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']
|
|
||||||
75
frontend/src/lib/store/cart-store.ts
Normal file
75
frontend/src/lib/store/cart-store.ts
Normal 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',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
73
frontend/src/lib/stripe/checkout-form.tsx
Normal file
73
frontend/src/lib/stripe/checkout-form.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
frontend/src/lib/stripe/checkout.tsx
Normal file
38
frontend/src/lib/stripe/checkout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
0
frontend/src/lib/stripe/types.ts
Normal file
0
frontend/src/lib/stripe/types.ts
Normal file
8
frontend/src/lib/types/product.ts
Normal file
8
frontend/src/lib/types/product.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface Product {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
price: number;
|
||||||
|
desc: string;
|
||||||
|
image: string;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
32
frontend/src/store/cartStore.ts
Normal file
32
frontend/src/store/cartStore.ts
Normal 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);
|
||||||
|
},
|
||||||
|
}));
|
||||||
18
frontend/src/types/product.ts
Normal file
18
frontend/src/types/product.ts
Normal 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[];
|
||||||
|
}
|
||||||
@ -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
7
lib/store/cart-store.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export type CartItem = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
quantity: number;
|
||||||
|
imageUrl: string;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user