diff --git a/client/src/App.jsx b/client/src/App.jsx index c84b855..69b3d5d 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -14,6 +14,8 @@ import Retailer_home from "./pages/Retailer-home"; import Product from "./pages/Product"; import Cart from "./pages/Cart"; import ScrollToTop from "./components/ScrollToTop"; +import UserProfile from "./pages/User-profile"; +import PrivateRoute from "./components/PrivateRoute"; import './App.css' function App() { return ( @@ -22,6 +24,11 @@ function App() { }> } /> + + + + } /> } /> } /> } /> @@ -30,7 +37,11 @@ function App() { } /> } /> } /> - } /> + + + + } /> }> } /> diff --git a/client/src/assets/avatar.svg b/client/src/assets/avatar.svg new file mode 100644 index 0000000..c481f0b --- /dev/null +++ b/client/src/assets/avatar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/assets/empty.svg b/client/src/assets/empty.svg new file mode 100644 index 0000000..05ce350 --- /dev/null +++ b/client/src/assets/empty.svg @@ -0,0 +1 @@ +happy_music \ No newline at end of file diff --git a/client/src/assets/pen-clip.svg b/client/src/assets/pen-clip.svg new file mode 100644 index 0000000..3285c7c --- /dev/null +++ b/client/src/assets/pen-clip.svg @@ -0,0 +1,2 @@ + + diff --git a/client/src/assets/user-pen.svg b/client/src/assets/user-pen.svg new file mode 100644 index 0000000..1335243 --- /dev/null +++ b/client/src/assets/user-pen.svg @@ -0,0 +1,2 @@ + + diff --git a/client/src/components/Navbar/navbar.jsx b/client/src/components/Navbar/navbar.jsx index 28ec0ff..437c0ff 100644 --- a/client/src/components/Navbar/navbar.jsx +++ b/client/src/components/Navbar/navbar.jsx @@ -1,245 +1,268 @@ import React, { useEffect, useState } from "react"; -import { Link, NavLink,useNavigate } from "react-router-dom"; -import { Divide, Divide as Hamburger } from "hamburger-react"; +import { Link, NavLink, useNavigate } from "react-router-dom"; +import { Divide } from "hamburger-react"; import { stack as Menu } from "react-burger-menu"; import logo from "../../assets/logo-final.png"; -import logo2 from "../../assets/shoelogo.jpg"; import axios from "axios"; -import {useDispatch, useSelector} from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { logOut, setUsername } from "../../redux/auth-slice"; const Navbar = () => { + const [user, setUser] = useState(); + const [dropdownOpen, setDropdownOpen] = useState(false); // Added state for dropdown + const dispatch = useDispatch(); + const logged = useSelector((state) => state.auth.value.isAuth); + const userProfile = useSelector((state) => state.auth.value.username); + const navigate = useNavigate(); // Added useNavigate hook - const [user, setUser] = useState(); - const dispatch = useDispatch(); - const logged = useSelector((state)=>state.auth.value.isAuth); - const username = useSelector((state)=>state.auth.value.username); + useEffect(() => { + const checklogin = async () => { + try { + const { data } = await axios.get("http://localhost:3000/api/users/me", { + headers: { + "Content-Type": "application/json", + }, + withCredentials: true, + }); + dispatch(setUsername({ username: data.user.profilepic })); + } catch (e) { + if (e.response && e.response.status !== 200) { + dispatch(logOut()); + } + } + }; + if (logged) { + checklogin(); + } + }, [logged]); - useEffect(() => { - const checklogin = async () => { - try { - const { data } = await axios.get( - "http://localhost:3000/api/users/me", - { - headers: { - "Content-Type": "application/json", - }, - withCredentials: true, - } - ); - dispatch(setUsername({username:data.user.name})); - } catch (e) { - if(e.response.status !== 200){ - dispatch(logOut()); - } - } - }; + // Adding a dropdown on profile photo + const handleProfileClick = () => { + setDropdownOpen(!dropdownOpen); + }; - if(logged){ - checklogin(); - } - }, [logged]); + // used for preventing to go to /profile , /cart page without login + const handleNavigation = (path) => { + if (!logged) { + navigate("/login"); + } else { + navigate(path); + } + }; - var styles = { - bmBurgerButton: { - display: "none", - }, - bmCrossButton: { - height: "24px", - width: "24px", - }, - bmCross: { - background: "#bdc3c7", - }, - bmMenuWrap: { - position: "fixed", - height: "100%", - }, - bmMenu: { - background: "white", - padding: "2.5em 1.5em 0", - fontSize: "1.15em", - }, - bmMorphShape: { - fill: "#373a47", - }, - bmItemList: { - display: "flex", - flexDirection: "column", - color: "black", - padding: "0.8em", - }, - bmItem: { - display: "inline-block", - padding: "0.5em", - }, - bmOverlay: { - background: "rgba(0, 0, 0, 0.3)", - }, - }; + var styles = { + bmBurgerButton: { + display: "none", + }, + bmCrossButton: { + height: "24px", + width: "24px", + }, + bmCross: { + background: "#bdc3c7", + }, + bmMenuWrap: { + position: "fixed", + height: "100%", + }, + bmMenu: { + background: "white", + padding: "2.5em 1.5em 0", + fontSize: "1.15em", + }, + bmMorphShape: { + fill: "#373a47", + }, + bmItemList: { + display: "flex", + flexDirection: "column", + color: "black", + padding: "0.8em", + }, + bmItem: { + display: "inline-block", + padding: "0.5em", + }, + bmOverlay: { + background: "rgba(0, 0, 0, 0.3)", + }, + }; - const [toggled, setToggled] = useState(false); + const [toggled, setToggled] = useState(false); - return ( - <> - setToggled(false)} - > - { - setToggled(false); - }} - > - Home - - { - setToggled(false); - }} - > - Categories - - { - setToggled(false); - }} - > - Customize - - { - setToggled(false); - }} - > - Shop - - { - setToggled(false); - }} - > - Login - - - + + ); }; export default Navbar; diff --git a/client/src/components/PrivateRoute.jsx b/client/src/components/PrivateRoute.jsx new file mode 100644 index 0000000..ccdc424 --- /dev/null +++ b/client/src/components/PrivateRoute.jsx @@ -0,0 +1,12 @@ + +import React from 'react'; +import { Navigate } from 'react-router-dom'; +import { useSelector } from 'react-redux'; + +const PrivateRoute = ({ children }) => { + const isAuth = useSelector((state) => state.auth.value.isAuth); + + return isAuth ? children : ; +}; + +export default PrivateRoute; diff --git a/client/src/components/Product-card/index.jsx b/client/src/components/Product-card/index.jsx index 507f454..ec2bc38 100644 --- a/client/src/components/Product-card/index.jsx +++ b/client/src/components/Product-card/index.jsx @@ -1,60 +1,105 @@ -import { Link, NavLink } from "react-router-dom"; +import { Link } from "react-router-dom"; +import axios from "axios"; +import { useState, useEffect } from "react"; + const Productcard = ({ shoeImage, shoeName, brand, pId, price }) => { - return ( -
-
- - Product -
- - {brand} - -

- {shoeName} -

-
-

- {`₹${parseInt( - price * 0.9 + price * 0.2 * 0.9 - )}`} -

- -

- {`₹${price * 1.2}`} -

-
-
- - - - -
-
-
- + const [isInWishlist, setIsInWishlist] = useState(false); + + useEffect(() => { + const fetchWishlist = async () => { + try { + const response = await axios.get("http://localhost:3000/api/user/wishlist", { + withCredentials: true, + }); + const wishlistItems = response.data.wishlist; + const isProductInWishlist = wishlistItems.some(item => item.productId === pId); + setIsInWishlist(isProductInWishlist); + } catch (error) { + console.error("Error fetching wishlist", error); + } + }; + + fetchWishlist(); + }, [pId]); + + const handleWishlistClick = async () => { + try { + const response = await axios.post("http://localhost:3000/api/user/wishlist", { productId: pId }, { + headers: { + "Content-Type": "application/json", + }, + withCredentials: true, + }); + if (response.data.success) { + setIsInWishlist(true); + } + } catch (error) { + console.error("Error adding to wishlist", error); + } + }; + + return ( +
+ + Product +
+ {brand} +

+ {shoeName} +

+
+

+ {`₹${parseInt(price * 0.9 + price * 0.2 * 0.9)}`} +

+ +

+ {`₹${price * 1.2}`} +

+
+
+ + + +
+
- ); + + +
+ ); }; export default Productcard; diff --git a/client/src/components/Wishlist/index.jsx b/client/src/components/Wishlist/index.jsx new file mode 100644 index 0000000..bb0b700 --- /dev/null +++ b/client/src/components/Wishlist/index.jsx @@ -0,0 +1,90 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; + +const Wishlist = () => { + const [wishlist, setWishlist] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(5); + + useEffect(() => { + const fetchWishlist = async () => { + try { + const response = await axios.get("http://localhost:3000/api/user/wishlist", { + headers: { + "Content-Type": "application/json", + }, + withCredentials: true, + }); + setWishlist(response.data.wishlist); + } catch (error) { + console.error("Error fetching wishlist", error); + } + }; + + fetchWishlist(); + }, []); + + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = wishlist.slice(indexOfFirstItem, indexOfLastItem); + + const paginate = (pageNumber) => setCurrentPage(pageNumber); + + return ( +
+ +
+ {currentItems.map((item) => ( +
+ {item.product.name} +
+

{item.product.shoe.shoename}

+
+

₹ {item.product.shoe.price}

+ +

+ {`₹${item.product.shoe.price * 1.2}`} +

+
+
+ +
+
+ ))} +
+
+ + {Array.from({ length: Math.ceil(wishlist.length / itemsPerPage) }, (_, i) => i + 1).map( + (pageNumber) => ( + + ) + )} + +
+
+ ); +}; + +export default Wishlist; \ No newline at end of file diff --git a/client/src/pages/User-profile/index.jsx b/client/src/pages/User-profile/index.jsx new file mode 100644 index 0000000..bc9de74 --- /dev/null +++ b/client/src/pages/User-profile/index.jsx @@ -0,0 +1,290 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; +import Wishlist from "../../components/Wishlist"; + +const UserProfile = () => { + const [userDetails, setUserDetails] = useState(null); + const [activeSection, setActiveSection] = useState("account"); + const [editMode, setEditMode] = useState(false); + const [formData, setFormData] = useState({}); + const [successMessage, setSuccessMessage] = useState(""); + + const navigate = useNavigate(); + + useEffect(() => { + const fetchUserProfile = async () => { + try { + const { data } = await axios.get("http://localhost:3000/api/users/me", { + headers: { + "Content-Type": "application/json", + }, + withCredentials: true, + }); + setUserDetails(data.user); + setFormData(data.user); + } catch (error) { + console.error("Error fetching user profile:", error); + navigate("/login"); + } + }; + + + fetchUserProfile(); + + }, [navigate]); + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ ...formData, [name]: value }); + }; + + const handleSave = async () => { + try { + const { data } = await axios.patch("http://localhost:3000/api/users/me", formData, { + headers: { + "Content-Type": "application/json", + }, + withCredentials: true, + }); + setUserDetails(data.user); + setEditMode(false); + setSuccessMessage("Profile updated successfully!"); + setTimeout(() => setSuccessMessage(""), 3000); + } catch (error) { + console.error("Error updating profile:", error); + } + }; + + + if (!userDetails) { + return
Loading...
; + } + + const renderSection = () => { + switch (activeSection) { + + case "account": + return ( +
+
+

Account Details

+ +
+
+

Your Profile

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {editMode && ( + + )} +
+
+ ); + + + + + case "wishlist": + return ( + +
+

WishList Items

+ +
+ ); + + case "purchased-items": + return ( +
+

Purchased Items

+ Empty Purchased Items +
+ ); + + case "payment": + return ( +
+

Payment Methods

+ Empty Payment Methods +
+ ); + + case "address": + return ( +
+

Address Details

+
+ +
+ +
+

{userDetails.street}

+
+ +
+
+ +
+

{userDetails.city}

+
+ +
+
+ +
+

{userDetails.state}

+
+
+
+ +
+

{userDetails.pincode}

+
+
+
+
+ + ); + default: + return null; + } + }; + + const getButtonClass = (section) => { + const baseClass = "w-full block py-2 px-4 rounded-full font-bold"; + const activeClass = "bg-gray-300 text-gray-800"; + const hoverClass = "hover:bg-gray-300 hover:text-gray-800"; + const inactiveClass = "bg-base-100 text-gray-800"; + + return `${baseClass} ${section === activeSection ? activeClass : inactiveClass} ${hoverClass}`; + }; + + return ( +
+ {successMessage && ( +
+ {successMessage} +
+ )} +
+
+
+ Profile +

{userDetails.name}

+

{userDetails.email}

+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+
+
+
+ {renderSection()} +
+
+
+ ); +}; + +export default UserProfile; diff --git a/server/app.js b/server/app.js index e869dfc..7842849 100644 --- a/server/app.js +++ b/server/app.js @@ -6,7 +6,7 @@ import productrouter from "./routes/products.js"; import cors from 'cors'; import cookieParser from "cookie-parser"; import cartrouter from './routes/cart.js'; - +import wishlistrouter from "./routes/wishlist.js"; export const app = express(); @@ -26,4 +26,5 @@ app.use(express.json()); app.use(cookieParser()); app.use("/api/users", userrouter); app.use("/api/products", productrouter); -app.use("/api/cart", cartrouter); \ No newline at end of file +app.use("/api/cart", cartrouter); +app.use("/api/user",wishlistrouter); \ No newline at end of file diff --git a/server/controllers/user.js b/server/controllers/user.js index 83bca71..c66cd42 100644 --- a/server/controllers/user.js +++ b/server/controllers/user.js @@ -83,6 +83,38 @@ const getMyProfile = (req, res) => { }); }; +const updateUserProfile = async (req, res) => { + const { name, street, city, state, pincode } = req.body; + const email = req.user.email; + + try { + const updatedUser = await prisma.mainuser.update({ + where: { email }, + data: { + name, + street, + city, + state, + pincode: parseInt(pincode), + }, + }); + + res.status(200).json({ + success: true, + message: "Profile updated successfully", + user: updatedUser, + }); + } catch (err) { + console.error(err); + res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +}; + + + const registerdesigner = async (req, res) => { const { description, portfoliolink } = req.body; @@ -147,4 +179,4 @@ const registerretailer = async (req, res) => { } }; -export { login, signup, getMyProfile, registerdesigner, registerretailer }; +export { login, signup, getMyProfile, updateUserProfile, registerdesigner, registerretailer }; diff --git a/server/controllers/wishlist.js b/server/controllers/wishlist.js new file mode 100644 index 0000000..76b458c --- /dev/null +++ b/server/controllers/wishlist.js @@ -0,0 +1,97 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +const addToWishlist = async (req, res) => { + const { productId } = req.body; + const { email } = req.user; + + try { + const existingWishlistItem = await prisma.wishlist.findUnique({ + where: { + userId_productId: { + userId: email, + productId: productId, + }, + }, + }); + + if (existingWishlistItem) { + await prisma.wishlist.delete({ + where: { + userId_productId: { + userId: email, + productId: productId, + }, + }, + }); + + return res.status(200).json({ + success: true, + message: "Product removed from wishlist.", + }); + } + + + await prisma.wishlist.create({ + data: { + userId: email, + productId: productId, + }, + }); + + return res.status(200).json({ + success: true, + message: "Product added to wishlist." + }); + } catch (err) { + console.error(err); + res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +}; + +const getWishlist = async (req, res) => { + const { email } = req.user; + + try { + const wishlistItems = await prisma.wishlist.findMany({ + where: { + userId: email, + }, + include: { + product: { + include: { + shoe: { + select: { + shoename: true, + supplier: { + select: { + supplierName: true, + }, + }, + price: true, + shoeImage: true, + }, + }, + }, + }, + }, + }); + + return res.status(200).json({ + success: true, + wishlist: wishlistItems, + }); + } catch (err) { + console.error(err); + res.status(500).json({ + success: false, + message: "Internal Server Error" + }); + } +}; + +export { addToWishlist, getWishlist }; diff --git a/server/prisma/migrations/20240726152732_init/migration.sql b/server/prisma/migrations/20240726152732_init/migration.sql new file mode 100644 index 0000000..f490ee6 --- /dev/null +++ b/server/prisma/migrations/20240726152732_init/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE `Wishlist` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `userId` VARCHAR(191) NOT NULL, + `productId` INTEGER NOT NULL, + + UNIQUE INDEX `Wishlist_userId_productId_key`(`userId`, `productId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `Wishlist` ADD CONSTRAINT `Wishlist_productId_fkey` FOREIGN KEY (`productId`) REFERENCES `Product`(`productId`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `Wishlist` ADD CONSTRAINT `Wishlist_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `Mainuser`(`email`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 47673e6..c81562c 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -54,6 +54,17 @@ model Mainuser { designer Designer? orders Orders[] retailer Retailer? + wishlist Wishlist[] +} + +model Wishlist { + id Int @id @default(autoincrement()) + userId String + productId Int + product Product @relation(fields: [productId], references: [productId]) + user Mainuser @relation(fields: [userId], references: [email]) + + @@unique([userId, productId]) } model Orders { @@ -79,6 +90,7 @@ model Product { productImage String? cart Cart[] orders Orders[] + wishlist Wishlist[] design Design? @relation(fields: [designId], references: [designId]) shoe Shoe? @relation(fields: [shoeId], references: [shoeid]) } diff --git a/server/routes/user.js b/server/routes/user.js index 0dd65f3..3f4ffa2 100644 --- a/server/routes/user.js +++ b/server/routes/user.js @@ -1,5 +1,5 @@ import express from "express"; -import { login, signup, getMyProfile, registerdesigner, registerretailer } from "../controllers/user.js"; +import { login, signup, getMyProfile,updateUserProfile, registerdesigner, registerretailer } from "../controllers/user.js"; import { isAuthenticated } from "../middlewares/auth.js"; const router = express.Router(); @@ -11,6 +11,8 @@ router.post("/signup", signup); router.get("/me", isAuthenticated, getMyProfile); +router.patch("/me", isAuthenticated, updateUserProfile); + router.post("/designer", isAuthenticated, registerdesigner); router.post("/retailer", isAuthenticated, registerretailer); diff --git a/server/routes/wishlist.js b/server/routes/wishlist.js new file mode 100644 index 0000000..2c2ee5b --- /dev/null +++ b/server/routes/wishlist.js @@ -0,0 +1,10 @@ +import express from "express"; +import { addToWishlist, getWishlist } from "../controllers/wishlist.js"; +import { isAuthenticated } from "../middlewares/auth.js"; + +const router = express.Router(); + +router.post("/wishlist", isAuthenticated, addToWishlist); +router.get("/wishlist", isAuthenticated, getWishlist); + +export default router;