diff --git a/backend/package-lock.json b/backend/package-lock.json index 89efb5b..d03e198 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1512,6 +1512,7 @@ "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -1580,6 +1581,7 @@ "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -3652,6 +3654,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/backend/src/routes/cartRoutes.ts b/backend/src/routes/cartRoutes.ts index ee7a344..cadbe92 100644 --- a/backend/src/routes/cartRoutes.ts +++ b/backend/src/routes/cartRoutes.ts @@ -6,13 +6,15 @@ import { getCart, checkout, } from "../controllers/cart.controller"; +import { authenticate } from "../middleware/authMiddleware"; const router: Router = express.Router(); -router.post("/", addItem); -router.patch("/:itemId", updateQuantity); -router.delete("/:itemId", removeItem); -router.get("/", getCart); -router.post("/checkout", checkout); +//Protected routes +router.post("/", authenticate, addItem); +router.patch("/:itemId", authenticate, updateQuantity); +router.delete("/:itemId", authenticate, removeItem); +router.get("/", authenticate, getCart); +router.post("/checkout", authenticate, checkout); -export default router; +export default router; \ No newline at end of file diff --git a/frontend/.env b/frontend/.env deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 15b0fe0..8ab2fb0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -189,6 +189,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -866,6 +867,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -912,6 +914,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3833,7 +3836,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3854,7 +3856,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -3868,7 +3869,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3883,8 +3883,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", @@ -4035,8 +4034,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4184,6 +4182,7 @@ "integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -4194,6 +4193,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4204,6 +4204,7 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4254,6 +4255,7 @@ "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", @@ -4641,6 +4643,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4858,6 +4861,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -5336,8 +5340,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -5360,7 +5363,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -5459,6 +5463,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6178,6 +6183,7 @@ "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@asamuzakjp/dom-selector": "^6.7.2", "cssstyle": "^5.3.1", @@ -6613,7 +6619,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6989,6 +6994,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7003,6 +7009,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.10.6.tgz", "integrity": "sha512-w0mCL5vICUAZrh1DuHEdOWBjxdO62lvcO++jbzr8UhhYcTbFkpegLH9XX+7MadjTl/y0feoqwQ/zAnzkc/EGog==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -7119,6 +7126,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -7162,6 +7170,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7196,6 +7205,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -7773,7 +7783,8 @@ "version": "4.1.14", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -7892,6 +7903,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8023,6 +8035,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8205,6 +8218,7 @@ "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.1.14.tgz", "integrity": "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==", "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/runtime": "0.92.0", "fdir": "^6.5.0", @@ -8297,6 +8311,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 374c8fe..d43d4b1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,41 +9,37 @@ import Browse from "./pages/Browse"; import Sell from "./pages/Sell"; import ProductDetailsPage from "./pages/ProductDetail"; import Navbar from "./components/Navbar"; -import { CartProvider } from "./components/CartContext.jsx"; -import Home from "./pages/Home.jsx"; -import Cart from "./pages/Cart.jsx"; +import { CartProvider } from "./components/CartContext"; +import Cart from "./pages/Cart"; import Payment from "./pages/Payment"; import Dashboard from "./pages/Dashboard"; + const queryClient = new QueryClient(); const App = () => ( - - - -
- - - - } /> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - -
-
+ + + +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+
); -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/components/CartContext.jsx b/frontend/src/components/CartContext.jsx deleted file mode 100644 index a1f5945..0000000 --- a/frontend/src/components/CartContext.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { createContext, useReducer, useEffect, useState } from "react"; - -export const CartContext = createContext(); - -const initialState = { - cart: JSON.parse(localStorage.getItem("cart")) || [], -}; - -const reducer = (state, action) => { - switch (action.type) { - case "ADD_ITEM": - return { ...state, cart: [...state.cart, action.payload] }; - case "REMOVE_ITEM": - return { - ...state, - cart: state.cart.filter((_, i) => i !== action.payload), - }; - case "CLEAR_CART": - return { ...state, cart: [] }; - default: - return state; - } -}; - -export const CartProvider = ({ children }) => { - const [state, dispatch] = useReducer(reducer, initialState); - const [notification, setNotification] = useState(null); - - useEffect(() => { - localStorage.setItem("cart", JSON.stringify(state.cart)); - }, [state.cart]); - - return ( - - {children} - - ); -}; \ No newline at end of file diff --git a/frontend/src/components/CartContext.tsx b/frontend/src/components/CartContext.tsx new file mode 100644 index 0000000..19aa05c --- /dev/null +++ b/frontend/src/components/CartContext.tsx @@ -0,0 +1,257 @@ +import React, { createContext, useContext, useEffect, useReducer } from "react"; + +export interface CartItem { + _id?: string; + productId: string; + name: string; + price: number; + quantity: number; + image?: string; +} + +export interface CartState { + items: CartItem[]; +} + +export type CartAction = + | { type: "SET_CART"; payload: CartItem[] } + | { type: "ADD_ITEM"; payload: CartItem } + | { type: "REMOVE_ITEM_BY_INDEX"; payload: number } + | { type: "REMOVE_ITEM_BY_ID"; payload: string } + | { type: "UPDATE_QUANTITY"; payload: { itemId?: string; productId?: string; quantity: number } } + | { type: "CLEAR_CART" }; + +const initialState: CartState = { + items: JSON.parse(localStorage.getItem("cart") || "null") || [], +}; + +function reducer(state: CartState, action: CartAction): CartState { + switch (action.type) { + case "SET_CART": + return { ...state, items: action.payload }; + case "ADD_ITEM": { + const existingIndex = state.items.findIndex(i => i.productId === action.payload.productId); + if (existingIndex !== -1) { + const arr = [...state.items]; + arr[existingIndex] = { ...arr[existingIndex], quantity: arr[existingIndex].quantity + action.payload.quantity }; + return { ...state, items: arr }; + } + return { ...state, items: [...state.items, action.payload] }; + } + case "REMOVE_ITEM_BY_INDEX": { + const arr = [...state.items]; + arr.splice(action.payload, 1); + return { ...state, items: arr }; + } + case "REMOVE_ITEM_BY_ID": { + const arr = state.items.filter(i => i._id !== action.payload); + return { ...state, items: arr }; + } + case "UPDATE_QUANTITY": { + const { itemId, productId, quantity } = action.payload; + const arr = [...state.items]; + const idx = itemId ? arr.findIndex(i => i._id === itemId) : arr.findIndex(i => i.productId === productId); + if (idx === -1) return state; + arr[idx] = { ...arr[idx], quantity }; + return { ...state, items: arr }; + } + case "CLEAR_CART": + return { ...state, items: [] }; + default: + return state; + } +} + +interface CartContextValue { + state: CartState; + addItem: (item: CartItem) => Promise; + removeItemByIndex: (index: number) => Promise; + removeItemById: (id: string) => Promise; + updateQuantity: (params: { itemId?: string; productId?: string; quantity: number }) => Promise; + clearCart: () => Promise; + refreshFromServer: () => Promise; +} + +const CartContext = createContext(undefined); + +export const useCart = (): CartContextValue => { + const ctx = useContext(CartContext); + if (!ctx) throw new Error("useCart must be used within CartProvider"); + return ctx; +}; + +export const CartProvider: React.FC> = ({ children }) => { + const [state, dispatch] = React.useReducer(reducer, initialState); + + useEffect(() => { + try { + localStorage.setItem("cart", JSON.stringify(state.items)); + } catch (err) { + //ignore + } + }, [state.items]); + + const getAuthHeaders = () => { + const token = localStorage.getItem("accessToken"); + return token ? { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } : { "Content-Type": "application/json" }; + }; + + const callBackend = async (url: string, init?: RequestInit) => { + try { + const res = await fetch(url, init); + if (!res.ok) { + const text = await res.text().catch(() => ""); + const err = new Error(`Backend error ${res.status}: ${text}`); + (err as any).status = res.status; + throw err; + } + return res; + } catch (err) { + throw err; + } + }; + + const refreshFromServer = async () => { + const token = localStorage.getItem("accessToken"); + if (!token) return; // nothing to do + try { + const res = await callBackend("/api/cart", { method: "GET", headers: getAuthHeaders() }); + const cart = await res.json(); + const items: CartItem[] = (cart?.items || []).map((it: any) => ({ + _id: String((it as any)._id || ""), + productId: String((it as any).productId || ""), + name: it.name, + price: it.price, + quantity: it.quantity, + image: it.image, + })); + dispatch({ type: "SET_CART", payload: items }); + } catch (err) { + // ignore + } + }; + + const addItem = async (item: CartItem) => { + const token = localStorage.getItem("accessToken"); + if (token) { + try { + const body = JSON.stringify({ + productId: item.productId, + name: item.name, + price: item.price, + quantity: item.quantity, + }); + const res = await callBackend("/api/cart", { method: "POST", headers: getAuthHeaders(), body }); + const savedCart = await res.json(); + const items: CartItem[] = (savedCart?.items || []).map((it: any) => ({ + _id: String((it as any)._id || ""), + productId: String((it as any).productId || ""), + name: it.name, + price: it.price, + quantity: it.quantity, + image: it.image, + })); + dispatch({ type: "SET_CART", payload: items }); + return; + } catch (err: any) { + if ((err as any).status === 401 || (err as any).status === 403) { + // token invalid + } else { + // network/other error + } + } + } + // Fallback + dispatch({ type: "ADD_ITEM", payload: item }); + }; + + const removeItemByIndex = async (index: number) => { + const token = localStorage.getItem("accessToken"); + if (token) { + const item = state.items[index]; + if (item && item._id) { + try { + await callBackend(`/api/cart/${item._id}`, { method: "DELETE", headers: getAuthHeaders() }); + await refreshFromServer(); + return; + } catch { + // fallback to LOCAL remove + } + } + } + // local remove + dispatch({ type: "REMOVE_ITEM_BY_INDEX", payload: index }); + }; + + const removeItemById = async (id: string) => { + const token = localStorage.getItem("accessToken"); + if (token) { + try { + await callBackend(`/api/cart/${id}`, { method: "DELETE", headers: getAuthHeaders() }); + await refreshFromServer(); + return; + } catch { + // fallback + } + } + dispatch({ type: "REMOVE_ITEM_BY_ID", payload: id }); + }; + + const updateQuantity = async (params: { itemId?: string; productId?: string; quantity: number }) => { + const token = localStorage.getItem("accessToken"); + if (token && params.itemId) { + try { + await callBackend(`/api/cart/${params.itemId}`, { + method: "PATCH", + headers: getAuthHeaders(), + body: JSON.stringify({ quantity: params.quantity }), + }); + await refreshFromServer(); + return; + } catch { + // fallback + } + } + + // fallback: update local + dispatch({ type: "UPDATE_QUANTITY", payload: params }); + }; + + const clearCart = async () => { + const token = localStorage.getItem("accessToken"); + if (token) { + try { + await callBackend("/api/cart/checkout", { method: "POST", headers: getAuthHeaders() }); + // backend will clear cart + await refreshFromServer(); + return; + } catch { + // fallback + } + } + dispatch({ type: "CLEAR_CART" }); + }; + + useEffect(() => { + (async () => { + const token = localStorage.getItem("accessToken"); + if (token) { + await refreshFromServer(); + } + })(); + }, []); + + const value: CartContextValue = { + state, + addItem, + removeItemByIndex, + removeItemById, + updateQuantity, + clearCart, + refreshFromServer, + }; + + return {children}; +}; + +export default CartContext; \ No newline at end of file diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index da7b11f..9d1b33e 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,17 +1,18 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Link, useNavigate, useLocation } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { Menu, X, Moon, Sun } from "lucide-react"; +import { Menu, X, Moon, Sun, ShoppingCart } from "lucide-react"; +import { useCart } from "./CartContext"; const Navbar: React.FC = () => { const [isOpen, setIsOpen] = useState(false); - const getInitialTheme = () => - typeof document !== "undefined" && document.documentElement.classList.contains("dark") - ? ("dark" as const) - : ("light" as const); + const getInitialTheme = () => + typeof document !== "undefined" && document.documentElement.classList.contains("dark") + ? ("dark" as const) + : ("light" as const); - const [theme, setTheme] = useState<"light" | "dark">(getInitialTheme); + const [theme, setTheme] = useState<"light" | "dark">(getInitialTheme); const navigate = useNavigate(); const location = useLocation(); @@ -39,6 +40,23 @@ const Navbar: React.FC = () => { { name: "Sell", path: "/sell" }, ]; + // Cart integration + const { state } = useCart(); + const totalCount = state.items.reduce((sum, it) => sum + (it.quantity || 0), 0); + + const prevCountRef = useRef(totalCount); + const [bump, setBump] = useState(false); + + useEffect(() => { + const prev = prevCountRef.current; + if (totalCount > prev) { + setBump(true); + const t = setTimeout(() => setBump(false), 600); + return () => clearTimeout(t); + } + prevCountRef.current = totalCount; + }, [totalCount]); + return (