added redux support for cart and products. Need to update cart based on redux state and add cache
This commit is contained in:
parent
c046213dca
commit
86dd53cb28
Binary file not shown.
@ -13,6 +13,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@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",
|
||||||
"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",
|
||||||
@ -25,6 +26,8 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
|
"react-redux": "^9.1.2",
|
||||||
|
"redux": "^5.0.1",
|
||||||
"resend": "^3.5.0",
|
"resend": "^3.5.0",
|
||||||
"sass": "^1.80.5",
|
"sass": "^1.80.5",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
|
|||||||
18
frontend/src/app/StoreProvider.tsx
Normal file
18
frontend/src/app/StoreProvider.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
'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,6 +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";
|
||||||
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
@ -18,6 +19,7 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
|
<StoreProvider>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`${inter.className} bg-slate-700 w-full max-h-screen text-white`}>
|
<body className={`${inter.className} bg-slate-700 w-full max-h-screen text-white`}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
@ -25,5 +27,6 @@ export default function RootLayout({
|
|||||||
<Footer />
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
</StoreProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
40
frontend/src/app/redux/CartReducer.ts
Normal file
40
frontend/src/app/redux/CartReducer.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { createSlice } 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: any) => {
|
||||||
|
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;
|
||||||
37
frontend/src/app/redux/slice.ts
Normal file
37
frontend/src/app/redux/slice.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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
|
||||||
@ -4,35 +4,43 @@ 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 } from "react-redux";
|
||||||
|
import type { RootState } from "@/lib/Store";
|
||||||
|
|
||||||
|
|
||||||
|
import type { Product } from "@/app/redux/CartReducer";
|
||||||
|
|
||||||
const CartTab = () => {
|
const CartTab = () => {
|
||||||
const cartItems = true;
|
const cartItems = true;
|
||||||
|
|
||||||
const data = [
|
// get the products from the cart
|
||||||
{
|
const products = useSelector((state: RootState) => state.cart.products);
|
||||||
id: 1,
|
|
||||||
title: "Product 1",
|
// const data = [
|
||||||
oldPrice: 19,
|
// {
|
||||||
price: 12,
|
// id: 1,
|
||||||
isNew: true,
|
// title: "Product 1",
|
||||||
desc: "Short sleeve t-shirt",
|
// oldPrice: 19,
|
||||||
image:
|
// price: 12,
|
||||||
"https://ih1.redbubble.net/image.2997405810.0746/ssrco,slim_fit_t_shirt,womens,101010:01c5ca27c6,front,square_product,600x600.jpg",
|
// isNew: true,
|
||||||
},
|
// desc: "Short sleeve t-shirt",
|
||||||
{
|
// image:
|
||||||
id: 2,
|
// "https://ih1.redbubble.net/image.2997405810.0746/ssrco,slim_fit_t_shirt,womens,101010:01c5ca27c6,front,square_product,600x600.jpg",
|
||||||
title: "Product 2",
|
// },
|
||||||
oldPrice: 19,
|
// {
|
||||||
price: 12,
|
// id: 2,
|
||||||
isNew: false,
|
// title: "Product 2",
|
||||||
desc: "Short sleeve t-shirt",
|
// oldPrice: 19,
|
||||||
image:
|
// price: 12,
|
||||||
"https://ih1.redbubble.net/image.2997405810.0746/ssrco,slim_fit_t_shirt,flatlay,101010:01c5ca27c6,front,wide_portrait,750x1000-bg,f8f8f8.jpg",
|
// isNew: false,
|
||||||
},
|
// desc: "Short sleeve t-shirt",
|
||||||
];
|
// image:
|
||||||
|
// "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">
|
<div className="cart absolute p-5 text-slate-600 top-28 right-0">
|
||||||
{data?.map((item) => (
|
{products?.map((item: Product) => (
|
||||||
<div className="item flex gap-5 items-center mb-[30px] " key={item.id}>
|
<div className="item flex gap-5 items-center mb-[30px] " key={item.id}>
|
||||||
<div className="relative w-[80px] h-[100px]">
|
<div className="relative w-[80px] h-[100px]">
|
||||||
<Image
|
<Image
|
||||||
@ -46,7 +54,7 @@ const CartTab = () => {
|
|||||||
<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">1 x ${item.price}</div>
|
<div className="price">{item.quantity} x ${item.price}</div>
|
||||||
</div>
|
</div>
|
||||||
<MdDeleteOutline className="delete-icon mt-16" />
|
<MdDeleteOutline className="delete-icon mt-16" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,6 +9,11 @@ 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 { RootState } from "@/lib/Store";
|
||||||
|
import { addToCart } from "@/app/redux/CartReducer";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const ProductPage = ({ productId }: any) => {
|
const ProductPage = ({ productId }: any) => {
|
||||||
// console.log(productId);
|
// console.log(productId);
|
||||||
@ -28,6 +33,9 @@ const ProductPage = ({ productId }: any) => {
|
|||||||
const [salePrice, setSalePrice] = useState(0);
|
const [salePrice, setSalePrice] = useState(0);
|
||||||
const [category, setCategory] = useState('');
|
const [category, setCategory] = useState('');
|
||||||
|
|
||||||
|
// Redux
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@ -76,7 +84,6 @@ const ProductPage = ({ productId }: any) => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="product-image">
|
<div className="product-image">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
<div className="mainImage">
|
<div className="mainImage">
|
||||||
@ -103,18 +110,34 @@ const ProductPage = ({ productId }: any) => {
|
|||||||
<Button
|
<Button
|
||||||
className="bg-slate-600 text-xl"
|
className="bg-slate-600 text-xl"
|
||||||
onClick={() => setQuantity((prev) => (prev === 1 ? 1 : prev - 1))}
|
onClick={() => setQuantity((prev) => (prev === 1 ? 1 : prev - 1))}
|
||||||
|
// onClick={() => dispatch(decrement())}
|
||||||
|
// aria-label="Decrement value"
|
||||||
>
|
>
|
||||||
-
|
-
|
||||||
</Button>
|
</Button>
|
||||||
<div>{quantity}</div>
|
<div>{quantity}</div>
|
||||||
|
{/* <div>{count}</div> */}
|
||||||
<Button
|
<Button
|
||||||
className="bg-slate-600 text-lg"
|
className="bg-slate-600 text-lg"
|
||||||
onClick={() => setQuantity((prev) => prev + 1)}
|
onClick={() => setQuantity((prev) => prev + 1)}
|
||||||
|
// aria-label="Increment value"
|
||||||
|
// onClick={() => dispatch(increment())}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button className="add bg-[#F45B1E] font-bold">
|
<Button className="add bg-[#F45B1E] font-bold"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(addToCart({
|
||||||
|
id: productId,
|
||||||
|
title: name,
|
||||||
|
price: price,
|
||||||
|
desc: desc,
|
||||||
|
image: product,
|
||||||
|
quantity: quantity
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
>
|
||||||
<MdOutlineAddShoppingCart /> ADD TO CART
|
<MdOutlineAddShoppingCart /> ADD TO CART
|
||||||
</Button>
|
</Button>
|
||||||
<div className="link">
|
<div className="link">
|
||||||
|
|||||||
16
frontend/src/lib/Store.ts
Normal file
16
frontend/src/lib/Store.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit'
|
||||||
|
import counterReducer from '@/app/redux/slice'
|
||||||
|
import cartReducer from '@/app/redux/CartReducer'
|
||||||
|
export const makeStore = () => {
|
||||||
|
return configureStore({
|
||||||
|
reducer: {
|
||||||
|
cart: cartReducer,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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']
|
||||||
7
frontend/src/lib/hooks.ts
Normal file
7
frontend/src/lib/hooks.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { useDispatch, useSelector, useStore } from 'react-redux'
|
||||||
|
import type { AppDispatch, AppStore, RootState } from './Store'
|
||||||
|
|
||||||
|
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||||
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||||
|
export const useAppStore = useStore.withTypes<AppStore>()
|
||||||
@ -21,6 +21,6 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/StoreProvider.ts", "src/app/redux/store.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user