From 2d910cb83700995b28865999104551af905ccefa Mon Sep 17 00:00:00 2001 From: Albin190 Date: Sun, 20 Apr 2025 12:34:11 -0700 Subject: [PATCH 1/2] add my front end file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit That’s amazing, Faraz! πŸ”₯ You're building a full **eCommerce backend** with **Login** and **Signup** β€” and you want to explain or write about it in **basic English**. Perfect! Here's a simple way to describe your project: --- ### πŸ›’ **My eCommerce Backend Project (in Basic English)** > I am building a full eCommerce backend using JavaScript. It has login and signup features. I want to add all basic features like a real eCommerce website. --- ### πŸ” **Authentication (Login & Signup)** - **Signup**: New users can register with name, email, and password. - **Login**: Registered users can log in to their account. - If the user is **admin**, they will go to the admin page. - If the user is **normal user**, they will go to the user dashboard. --- ### πŸ“¦ **Main Features I'm Adding** | Feature | Description (Simple English) | |--------------------------|---------------------------------------------------------------| | Add to Cart | Users can add products to the cart. | | Checkout | Users can checkout and place an order. | | Payment Form | A simple form for payment details. | | Wishlist | Users can save favorite products. | | Product Page | Shows all products. | | Product Details | Shows details like price, description, order number, etc. | | Admin Panel | Admin can see all users, orders, and manage products. | | Order History | Users can see past orders. | | Login System | Secure login for users and admins. | | Forgot Password | Users can reset password if they forget. | --- ### 🧠 **Technologies I’m Using** - **HTML / CSS / JS** - **Local Storage / IndexedDB** - **Vanilla JavaScript** (No frameworks yet) - **SweetAlert** for success messages - **Fully Responsive UI** --- --- src/client/App.css | 38 ++ src/client/App.js | 76 ++- src/client/App.test.js | 8 + src/client/components/Footer.jsx | 119 ++++ src/client/components/Header.jsx | 221 +++++++ src/client/context/CartContext.js | 81 +++ src/client/context/WishlistContext.js | 59 ++ src/client/dashboard/adminorders.jsx | 289 +++++++++ src/client/dashboard/adminproducts.jsx | 342 ++++++++++ src/client/dashboard/dashboard.jsx | 42 ++ src/client/dashboard/login.jsx | 417 +++++++++++++ src/client/dashboard/sidebar.jsx | 44 ++ src/client/dashboard/signup.jsx | 140 +++++ src/client/dashboard/topbar.jsx | 74 +++ src/client/dashboard/users.jsx | 298 +++++++++ src/client/index.css | 13 + src/client/index.js | 18 +- src/client/logo.svg | 1 + src/client/pages/AboutPage.jsx | 141 +++++ src/client/pages/CartPage.jsx | 227 +++++++ src/client/pages/CategoryPage.jsx | 50 ++ src/client/pages/CheckoutPage.jsx | 793 ++++++++++++++++++++++++ src/client/pages/ContactPage.jsx | 252 ++++++++ src/client/pages/HomePage.jsx | 508 +++++++++++++++ src/client/pages/LoginPage.jsx | 436 +++++++++++++ src/client/pages/NotFoundPage.jsx | 21 + src/client/pages/ProductDetailPage.jsx | 467 ++++++++++++++ src/client/pages/ProductsPage.jsx | 827 +++++++++++++++++++++++++ src/client/pages/WishlistPage.jsx | 210 +++++++ src/client/pages/tsconfig.json | 113 ++++ src/client/reportWebVitals.js | 13 + src/client/setupTests.js | 5 + 32 files changed, 6321 insertions(+), 22 deletions(-) create mode 100644 src/client/App.css create mode 100644 src/client/App.test.js create mode 100644 src/client/components/Footer.jsx create mode 100644 src/client/components/Header.jsx create mode 100644 src/client/context/CartContext.js create mode 100644 src/client/context/WishlistContext.js create mode 100644 src/client/dashboard/adminorders.jsx create mode 100644 src/client/dashboard/adminproducts.jsx create mode 100644 src/client/dashboard/dashboard.jsx create mode 100644 src/client/dashboard/login.jsx create mode 100644 src/client/dashboard/sidebar.jsx create mode 100644 src/client/dashboard/signup.jsx create mode 100644 src/client/dashboard/topbar.jsx create mode 100644 src/client/dashboard/users.jsx create mode 100644 src/client/index.css create mode 100644 src/client/logo.svg create mode 100644 src/client/pages/AboutPage.jsx create mode 100644 src/client/pages/CartPage.jsx create mode 100644 src/client/pages/CategoryPage.jsx create mode 100644 src/client/pages/CheckoutPage.jsx create mode 100644 src/client/pages/ContactPage.jsx create mode 100644 src/client/pages/HomePage.jsx create mode 100644 src/client/pages/LoginPage.jsx create mode 100644 src/client/pages/NotFoundPage.jsx create mode 100644 src/client/pages/ProductDetailPage.jsx create mode 100644 src/client/pages/ProductsPage.jsx create mode 100644 src/client/pages/WishlistPage.jsx create mode 100644 src/client/pages/tsconfig.json create mode 100644 src/client/reportWebVitals.js create mode 100644 src/client/setupTests.js diff --git a/src/client/App.css b/src/client/App.css new file mode 100644 index 00000000..74b5e053 --- /dev/null +++ b/src/client/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/client/App.js b/src/client/App.js index 52d7d064..41e3eacd 100644 --- a/src/client/App.js +++ b/src/client/App.js @@ -1,23 +1,59 @@ -import React, { Component } from 'react'; -import './app.css'; -import ReactImage from './react.png'; +// src/App.jsx +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { CartProvider } from "./context/CartContext"; +import { WishlistProvider } from "./context/WishlistContext"; +import Header from "./components/Header"; +import Footer from "./components/Footer"; +import HomePage from "./pages/HomePage"; +import ProductsPage from "./pages/ProductsPage"; +import ProductDetailPage from "./pages/ProductDetailPage"; +import CartPage from "./pages/CartPage"; +import LoginPage from "./pages/LoginPage"; +import CheckoutPage from "./pages/CheckoutPage"; +import WishlistPage from "./pages/WishlistPage"; +import AboutPage from "./pages/AboutPage"; +import ContactPage from "./pages/ContactPage"; +import NotFoundPage from "./pages/NotFoundPage"; +import CategoryPage from "./pages/CategoryPage"; +import AdminProducts from "./dashboard/adminproducts"; +import AdminOrders from "./dashboard/adminorders"; -export default class App extends Component { - state = { username: null }; +import { ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; - componentDidMount() { - fetch('/api/getUsername') - .then(res => res.json()) - .then(user => this.setState({ username: user.username })); - } - - render() { - const { username } = this.state; - return ( -
- {username ?

{`Hello ${username}`}

:

Loading.. please wait!

} - react -
- ); - } +function App() { + return ( + + + +
+
+
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ +
+ +
+
+
+ + ); } + +export default App; \ No newline at end of file diff --git a/src/client/App.test.js b/src/client/App.test.js new file mode 100644 index 00000000..1f03afee --- /dev/null +++ b/src/client/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/client/components/Footer.jsx b/src/client/components/Footer.jsx new file mode 100644 index 00000000..69576ad9 --- /dev/null +++ b/src/client/components/Footer.jsx @@ -0,0 +1,119 @@ +import { Link } from "react-router-dom" +import { Facebook, Twitter, Instagram, Youtube, Mail, Phone, MapPin } from "lucide-react" + +const Footer = () => { + const currentYear = new Date().getFullYear() + + return ( +
+
+
+ {/* Company Info */} +
+

ShopVista

+

Your one-stop destination for quality products at affordable prices.

+
+ + + Facebook + + + + Twitter + + + + Instagram + + + + YouTube + +
+
+ + {/* Quick Links */} +
+

Quick Links

+
    +
  • + + About Us + +
  • +
  • + + Contact Us + +
  • +
  • + + Blog + +
  • +
  • + + FAQs + +
  • +
  • + + Privacy Policy + +
  • +
  • + + Terms & Conditions + +
  • +
+
+ + {/* Contact Info */} +
+

Contact Us

+
    +
  • + + 123 Commerce Street, Shopville, SV 12345 +
  • +
  • + + +1 (555) 123-4567 +
  • +
  • + + support@shopvista.com +
  • +
+
+ + {/* Newsletter */} +
+

Newsletter

+

Subscribe to our newsletter for the latest updates and offers.

+
+ + +
+
+
+ + {/* Bottom Bar */} +
+

Β© {currentYear} ShopVista. All rights reserved.

+
+ Visa + Mastercard + PayPal + Apple Pay +
+
+
+
+ ) +} + +export default Footer diff --git a/src/client/components/Header.jsx b/src/client/components/Header.jsx new file mode 100644 index 00000000..362c46c1 --- /dev/null +++ b/src/client/components/Header.jsx @@ -0,0 +1,221 @@ +// src/components/Header.jsx +"use client"; + +import { useState, useEffect } from "react"; +import { Link, useLocation } from "react-router-dom"; +import { Search, ShoppingCart, Heart, User, Menu, X, Sun, Moon } from "lucide-react"; +import { useCart } from "../context/CartContext"; +import { useWishlist } from "../context/WishlistContext"; + +const Header = () => { + const { cartItems } = useCart(); + const { wishlistItems } = useWishlist(); + const [isScrolled, setIsScrolled] = useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [showSuggestions, setShowSuggestions] = useState(false); + const [darkMode, setDarkMode] = useState(false); + const location = useLocation(); + + const cartCount = cartItems.reduce((total, item) => total + item.quantity, 0); + const wishlistCount = wishlistItems.length; + + useEffect(() => { + console.log("Header cartItems:", cartItems); + console.log("Header wishlistItems:", wishlistItems); + }, [cartItems, wishlistItems]); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 10); + }; + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + useEffect(() => { + const handleClickOutside = () => setShowSuggestions(false); + document.addEventListener("click", handleClickOutside); + return () => document.removeEventListener("click", handleClickOutside); + }, []); + + const handleSearchFocus = (e) => { + e.stopPropagation(); + if (searchQuery.length > 0) { + setShowSuggestions(true); + } + }; + + const handleSearchChange = (e) => { + setSearchQuery(e.target.value); + if (e.target.value.length > 0) { + setShowSuggestions(true); + } else { + setShowSuggestions(false); + } + }; + + const toggleDarkMode = () => { + setDarkMode(!darkMode); + // Implement actual dark mode toggling here + }; + + const navLinks = [ + { name: "Home", href: "/" }, + { name: "Shop", href: "/products" }, + { name: "Categories", href: "/categories" }, + { name: "About", href: "/about" }, + { name: "Contact", href: "/contact" }, + { name: "AOrder", href: "/adminorders" }, + { name: "AProduct", href: "/adminproducts" }, + ]; + + return ( +
+
+

Free shipping on orders over $50 | Use code WELCOME10 for 10% off your first order

+
+
+
+ + ShopVista + + +
+
+ + e.stopPropagation()} + /> + {showSuggestions && ( +
+
No results found
+
+ )} +
+
+
+ + + + + + + + + + + +
+
+
+
+ + e.stopPropagation()} + /> + {showSuggestions && ( +
+
No results found
+
+ )} +
+
+
+ {isMobileMenuOpen && ( +
+
+ +
+ + + Account + + + + Wishlist ({wishlistCount}) + + +
+
+
+ )} +
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/src/client/context/CartContext.js b/src/client/context/CartContext.js new file mode 100644 index 00000000..fb080b3e --- /dev/null +++ b/src/client/context/CartContext.js @@ -0,0 +1,81 @@ +// src/context/CartContext.js +"use client"; + +import { createContext, useContext, useState, useEffect } from "react"; + +const CartContext = createContext(); + +export const CartProvider = ({ children }) => { + const [cartItems, setCartItems] = useState(() => { + const savedCart = localStorage.getItem("cartItems"); + return savedCart ? JSON.parse(savedCart) : []; + }); + + useEffect(() => { + localStorage.setItem("cartItems", JSON.stringify(cartItems)); + console.log("Cart items updated:", cartItems); // Debug log + }, [cartItems]); + + const addToCart = (product, quantity = 1, color = "Default", size = "Default") => { + setCartItems((prevItems) => { + const existingItem = prevItems.find( + (item) => item.id === product.id && item.color === color && item.size === size + ); + + if (existingItem) { + const updatedItems = prevItems.map((item) => + item.id === product.id && item.color === color && item.size === size + ? { ...item, quantity: item.quantity + quantity } + : item + ); + console.log("Updated existing item:", updatedItems); // Debug log + return updatedItems; + } + + const newItem = { + id: product.id, + name: product.name, + price: product.price, + image: product.image, + slug: product.slug, + quantity, + color, + size, + }; + const updatedItems = [...prevItems, newItem]; + console.log("Added new item:", updatedItems); // Debug log + return updatedItems; + }); + }; + + const updateQuantity = (id, color, size, newQuantity) => { + if (newQuantity < 1) return; + setCartItems((prevItems) => + prevItems.map((item) => + item.id === id && item.color === color && item.size === size + ? { ...item, quantity: newQuantity } + : item + ) + ); + }; + + const removeItem = (id, color, size) => { + setCartItems((prevItems) => + prevItems.filter( + (item) => !(item.id === id && item.color === color && item.size === size) + ) + ); + }; + + const clearCart = () => { + setCartItems([]); + }; + + return ( + + {children} + + ); +}; + +export const useCart = () => useContext(CartContext); \ No newline at end of file diff --git a/src/client/context/WishlistContext.js b/src/client/context/WishlistContext.js new file mode 100644 index 00000000..8eb31094 --- /dev/null +++ b/src/client/context/WishlistContext.js @@ -0,0 +1,59 @@ +// src/context/WishlistContext.js +"use client"; + +import { createContext, useContext, useState, useEffect } from "react"; + +const WishlistContext = createContext(); + +export const WishlistProvider = ({ children }) => { + const [wishlistItems, setWishlistItems] = useState(() => { + const savedWishlist = localStorage.getItem("wishlistItems"); + return savedWishlist ? JSON.parse(savedWishlist) : []; + }); + + useEffect(() => { + localStorage.setItem("wishlistItems", JSON.stringify(wishlistItems)); + console.log("Wishlist items updated:", wishlistItems); // Debug log + }, [wishlistItems]); + + const addToWishlist = (product) => { + setWishlistItems((prevItems) => { + if (prevItems.find((item) => item.id === product.id)) { + return prevItems; // Prevent duplicates + } + const newItem = { + id: product.id, + name: product.name, + price: product.price, + originalPrice: product.originalPrice || null, + color: "Default", // Default for HomePage; can be updated in ProductsPage + size: "Default", + inStock: true, // Assume in stock for simplicity + image: product.image, + slug: product.slug, + }; + console.log("Added to wishlist:", newItem); // Debug log + return [...prevItems, newItem]; + }); + }; + + const removeFromWishlist = (id) => { + setWishlistItems((prevItems) => { + const updatedItems = prevItems.filter((item) => item.id !== id); + console.log("Removed from wishlist, new items:", updatedItems); // Debug log + return updatedItems; + }); + }; + + const clearWishlist = () => { + setWishlistItems([]); + }; + + return ( + + {children} + + ); +}; + +export const useWishlist = () => useContext(WishlistContext); \ No newline at end of file diff --git a/src/client/dashboard/adminorders.jsx b/src/client/dashboard/adminorders.jsx new file mode 100644 index 00000000..7dcb0667 --- /dev/null +++ b/src/client/dashboard/adminorders.jsx @@ -0,0 +1,289 @@ +"use client" + +import { useState, useEffect } from "react" +import { Eye, Clock, CheckCircle, XCircle, TruckIcon } from "lucide-react" +import Swal from "sweetalert2" + +export default function Orders() { + const [orders, setOrders] = useState([]) + const [selectedOrder, setSelectedOrder] = useState(null) + const [showModal, setShowModal] = useState(false) + + useEffect(() => { + // Load orders from localStorage + const storedOrders = JSON.parse(localStorage.getItem("orders") || "[]") + setOrders(storedOrders) + + // Initialize with sample data if empty + if (storedOrders.length === 0) { + const sampleOrders = [ + { + id: "1", + customer: "John Doe", + email: "john@example.com", + date: "2023-04-15T10:30:00Z", + status: "delivered", + total: 1299.98, + items: [ + { id: "1", name: "Smartphone X", price: 999.99, quantity: 1 }, + { id: "3", name: "Wireless Headphones", price: 199.99, quantity: 1.5 }, + ], + }, + { + id: "2", + customer: "Jane Smith", + email: "jane@example.com", + date: "2023-04-16T14:20:00Z", + status: "processing", + total: 1499.99, + items: [{ id: "2", name: "Laptop Pro", price: 1499.99, quantity: 1 }], + }, + { + id: "3", + customer: "Bob Johnson", + email: "bob@example.com", + date: "2023-04-17T09:15:00Z", + status: "shipped", + total: 399.98, + items: [{ id: "3", name: "Wireless Headphones", price: 199.99, quantity: 2 }], + }, + ] + setOrders(sampleOrders) + localStorage.setItem("orders", JSON.stringify(sampleOrders)) + } + }, []) + + const handleViewOrder = (order) => { + setSelectedOrder(order) + setShowModal(true) + } + + const handleUpdateStatus = (orderId, newStatus) => { + const updatedOrders = orders.map((order) => { + if (order.id === orderId) { + return { ...order, status: newStatus } + } + return order + }) + + setOrders(updatedOrders) + localStorage.setItem("orders", JSON.stringify(updatedOrders)) + + // If we're updating the currently selected order, update that too + if (selectedOrder && selectedOrder.id === orderId) { + setSelectedOrder({ ...selectedOrder, status: newStatus }) + } + + Swal.fire({ + icon: "success", + title: "Order Updated", + text: `Order #${orderId} status changed to ${newStatus}`, + timer: 1500, + showConfirmButton: false, + }) + } + + const getStatusBadge = (status) => { + switch (status) { + case "pending": + return Pending + case "processing": + return Processing + case "shipped": + return Shipped + case "delivered": + return Delivered + case "cancelled": + return Cancelled + default: + return {status} + } + } + + const formatDate = (dateString) => { + const date = new Date(dateString) + return date.toLocaleDateString() + " " + date.toLocaleTimeString() + } + + return ( +
+
+

Orders

+
+ +
+
+ + + + + + + + + + + + + {orders.map((order) => ( + + + + + + + + + ))} + +
+ Order ID + + Customer + Date + Status + + Total + + Actions +
+
#{order.id}
+
+
{order.customer}
+
{order.email}
+
+
{formatDate(order.date)}
+
{getStatusBadge(order.status)} +
${order.total.toFixed(2)}
+
+ +
+
+
+ + {/* Order Details Modal */} + {showModal && selectedOrder && ( +
+
+
+

Order #{selectedOrder.id}

+ +
+ +
+
+
+

Customer

+

{selectedOrder.customer}

+

{selectedOrder.email}

+
+
+

Order Date

+

{formatDate(selectedOrder.date)}

+
+
+

Status

+
{getStatusBadge(selectedOrder.status)}
+
+
+
+ +
+

Items

+ + + + + + + + + + + {selectedOrder.items.map((item) => ( + + + + + + + ))} + + + + + + + +
+ Product + + Price + + Quantity + + Subtotal +
+
{item.name}
+
+
${item.price.toFixed(2)}
+
+
{item.quantity}
+
+
${(item.price * item.quantity).toFixed(2)}
+
+ Total: + ${selectedOrder.total.toFixed(2)}
+
+ +
+

Update Status

+
+ + + + +
+
+ +
+ +
+
+
+ )} +
+ ) +} diff --git a/src/client/dashboard/adminproducts.jsx b/src/client/dashboard/adminproducts.jsx new file mode 100644 index 00000000..7897504e --- /dev/null +++ b/src/client/dashboard/adminproducts.jsx @@ -0,0 +1,342 @@ +"use client" + +import { useState, useEffect } from "react" +import { Plus, Edit, Trash2, X } from "lucide-react" +import Swal from "sweetalert2" + +export default function AdminProducts() { + const [products, setProducts] = useState([]) + const [showModal, setShowModal] = useState(false) + const [formData, setFormData] = useState({ + id: "", + name: "", + description: "", + price: "", + category: "", + stock: "", + image: "", + }) + const [isEditing, setIsEditing] = useState(false) + + useEffect(() => { + // Load products from localStorage + const storedProducts = JSON.parse(localStorage.getItem("products") || "[]") + setProducts(storedProducts) + + // Initialize with sample data if empty + if (storedProducts.length === 0) { + const sampleProducts = [ + { + id: "1", + name: "Smartphone X", + description: "Latest smartphone with advanced features", + price: 999.99, + category: "Electronics", + stock: 50, + image: "/placeholder.svg?height=100&width=100", + }, + { + id: "2", + name: "Laptop Pro", + description: "High-performance laptop for professionals", + price: 1499.99, + category: "Electronics", + stock: 25, + image: "/placeholder.svg?height=100&width=100", + }, + { + id: "3", + name: "Wireless Headphones", + description: "Premium noise-cancelling headphones", + price: 199.99, + category: "Audio", + stock: 100, + image: "/placeholder.svg?height=100&width=100", + }, + ] + setProducts(sampleProducts) + localStorage.setItem("products", JSON.stringify(sampleProducts)) + } + }, []) + + const handleInputChange = (e) => { + const { name, value } = e.target + setFormData({ + ...formData, + [name]: name === "price" || name === "stock" ? Number.parseFloat(value) : value, + }) + } + + const handleSubmit = (e) => { + e.preventDefault() + + if (isEditing) { + // Update existing product + const updatedProducts = products.map((product) => (product.id === formData.id ? formData : product)) + setProducts(updatedProducts) + localStorage.setItem("products", JSON.stringify(updatedProducts)) + + Swal.fire({ + icon: "success", + title: "Product Updated", + text: `${formData.name} has been updated successfully`, + timer: 1500, + showConfirmButton: false, + }) + } else { + // Add new product + const newProduct = { + ...formData, + id: Date.now().toString(), + } + const updatedProducts = [...products, newProduct] + setProducts(updatedProducts) + localStorage.setItem("products", JSON.stringify(updatedProducts)) + + Swal.fire({ + icon: "success", + title: "Product Added", + text: `${formData.name} has been added successfully`, + timer: 1500, + showConfirmButton: false, + }) + } + + resetForm() + } + + const handleEdit = (product) => { + setFormData(product) + setIsEditing(true) + setShowModal(true) + } + + const handleDelete = (id) => { + Swal.fire({ + title: "Are you sure?", + text: "You won't be able to revert this!", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes, delete it!", + }).then((result) => { + if (result.isConfirmed) { + const updatedProducts = products.filter((product) => product.id !== id) + setProducts(updatedProducts) + localStorage.setItem("products", JSON.stringify(updatedProducts)) + + Swal.fire({ + icon: "success", + title: "Deleted!", + text: "The product has been deleted.", + timer: 1500, + showConfirmButton: false, + }) + } + }) + } + + const resetForm = () => { + setFormData({ + id: "", + name: "", + description: "", + price: "", + category: "", + stock: "", + image: "/placeholder.svg?height=100&width=100", + }) + setIsEditing(false) + setShowModal(false) + } + + return ( +
+
+

Products

+ +
+ +
+
+ + + + + + + + + + + + {products.map((product) => ( + + + + + + + + ))} + +
+ Product + + Category + + Price + + Stock + + Actions +
+
+
+ {product.name} +
+
+
{product.name}
+
{product.description.substring(0, 50)}...
+
+
+
+
{product.category}
+
+
${product.price.toFixed(2)}
+
+
{product.stock}
+
+ + +
+
+
+ + {/* Product Form Modal */} + {showModal && ( +
+
+
+

{isEditing ? "Edit Product" : "Add New Product"}

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ )} +
+ ) +} diff --git a/src/client/dashboard/dashboard.jsx b/src/client/dashboard/dashboard.jsx new file mode 100644 index 00000000..0eeb5adf --- /dev/null +++ b/src/client/dashboard/dashboard.jsx @@ -0,0 +1,42 @@ +"use client" + +import { useState } from "react" +import Sidebar from "./admin/sidebar" +import Topbar from "./admin/topbar" +import Products from "./admin/products" +import Orders from "./admin/orders" +import Users from "./admin/users" +import Chess from "./chess" + +export default function Dashboard({ user, onLogout }) { + const [activeTab, setActiveTab] = useState("products") + + const renderContent = () => { + switch (activeTab) { + case "products": + return + case "orders": + return + case "users": + return user.role === "admin" ? ( + + ) : ( +
You don't have permission to view this page
+ ) + case "chess": + return + default: + return + } + } + + return ( +
+ +
+ +
{renderContent()}
+
+
+ ) +} diff --git a/src/client/dashboard/login.jsx b/src/client/dashboard/login.jsx new file mode 100644 index 00000000..8a9661c8 --- /dev/null +++ b/src/client/dashboard/login.jsx @@ -0,0 +1,417 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { Eye, EyeOff, Facebook, Github } from "lucide-react"; +import Swal from 'sweetalert2'; + +const LoginPage = () => { + const [activeTab, setActiveTab] = useState("login"); + const [showLoginPassword, setShowLoginPassword] = useState(false); + const [showRegisterPassword, setShowRegisterPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + // Login form state + const [loginEmail, setLoginEmail] = useState(""); + const [loginPassword, setLoginPassword] = useState(""); + const [rememberMe, setRememberMe] = useState(false); + + // Register form state + const [name, setName] = useState(""); + const [registerEmail, setRegisterEmail] = useState(""); + const [registerPassword, setRegisterPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [terms, setTerms] = useState(false); + + const handleLoginSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + const response = await fetch("http://localhost:5000/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: loginEmail, password: loginPassword, rememberMe }), + signal: controller.signal, + }); + clearTimeout(timeoutId); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.message || "Login failed"); + } + localStorage.setItem("token", data.token); + localStorage.setItem("user", JSON.stringify(data.user)); + await Swal.fire({ + icon: 'success', + title: 'Login Successful', + text: `Welcome back, ${data.user.username}!`, + timer: 1500, + showConfirmButton: false, + }); + navigate("/dashboard"); + } catch (err) { + const errorMessage = err.name === 'AbortError' + ? 'Request timed out. Please check if the backend is running on http://localhost:5000.' + : err.message || 'Failed to connect to the server. Please check if the backend is running.'; + await Swal.fire({ + icon: 'error', + title: 'Login Failed', + text: errorMessage, + }); + } finally { + setLoading(false); + } + }; + + const handleRegisterSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + if (registerPassword !== confirmPassword) { + await Swal.fire({ + icon: 'error', + title: 'Validation Error', + text: 'Passwords do not match', + }); + setLoading(false); + return; + } + if (!terms) { + await Swal.fire({ + icon: 'error', + title: 'Validation Error', + text: 'You must agree to the terms', + }); + setLoading(false); + return; + } + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + const response = await fetch("http://localhost:5000/api/auth/signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: name, + email: registerEmail, + password: registerPassword, + }), + signal: controller.signal, + }); + clearTimeout(timeoutId); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.message || "Registration failed"); + } + localStorage.setItem("token", data.token); + localStorage.setItem("user", JSON.stringify(data.user)); + await Swal.fire({ + icon: 'success', + title: 'Registration Successful', + text: `Welcome, ${data.user.username}! Your account has been created.`, + timer: 1500, + showConfirmButton: false, + }); + navigate("/HomePage"); + } catch (err) { + const errorMessage = err.name === 'AbortError' + ? 'Request timed out. Please check if the backend is running on http://localhost:5000.' + : err.message || 'Failed to connect to the server. Please check if the backend is running.'; + await Swal.fire({ + icon: 'error', + title: 'Registration Failed', + text: errorMessage, + }); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+

Welcome to ShopVista

+

Sign in to your account or create a new one

+
+ +
+
+ + +
+ + {/* Login Tab */} + {activeTab === "login" && ( +
+
+ + setLoginEmail(e.target.value)} + required + disabled={loading} + /> +
+ +
+
+ + + Forgot password? + +
+
+ setLoginPassword(e.target.value)} + required + disabled={loading} + /> + +
+
+ +
+ setRememberMe(e.target.checked)} + disabled={loading} + /> + +
+ + +
+ )} + + {/* Register Tab */} + {activeTab === "register" && ( +
+
+ + setName(e.target.value)} + required + disabled={loading} + /> +
+ +
+ + setRegisterEmail(e.target.value)} + required + disabled={loading} + /> +
+ +
+ +
+ setRegisterPassword(e.target.value)} + required + disabled={loading} + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + required + disabled={loading} + /> + +
+
+ +
+ setTerms(e.target.checked)} + required + disabled={loading} + /> + +
+ + +
+ )} + +
+
+
+
+
+
+ Or continue with +
+
+ +
+ + + +
+
+
+
+
+
+ ); +}; + +export default LoginPage; \ No newline at end of file diff --git a/src/client/dashboard/sidebar.jsx b/src/client/dashboard/sidebar.jsx new file mode 100644 index 00000000..5d8e2895 --- /dev/null +++ b/src/client/dashboard/sidebar.jsx @@ -0,0 +1,44 @@ +"use client" + +import { ShoppingBag, ShoppingCart, Users, CastleIcon as ChessKnight } from "lucide-react" + +export default function Sidebar({ activeTab, setActiveTab, user }) { + const menuItems = [ + { id: "products", label: "Products", icon: ShoppingBag }, + { id: "orders", label: "Orders", icon: ShoppingCart }, + { id: "users", label: "Users", icon: Users, adminOnly: true }, + + ] + + return ( +
+
+

E-Commerce Dashboard

+
+ +
+ ) +} diff --git a/src/client/dashboard/signup.jsx b/src/client/dashboard/signup.jsx new file mode 100644 index 00000000..de4cd260 --- /dev/null +++ b/src/client/dashboard/signup.jsx @@ -0,0 +1,140 @@ +"use client" + +import { useState } from "react" +import Swal from "sweetalert2" + +export default function AdminSignup({ onSignup, toggleView }) { + const [name, setName] = useState("") + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + + const handleSubmit = (e) => { + e.preventDefault() + + if (password !== confirmPassword) { + Swal.fire({ + icon: "error", + title: "Passwords do not match", + text: "Please make sure your passwords match", + }) + return + } + + // Get existing users from localStorage + const users = JSON.parse(localStorage.getItem("users") || "[]") + + // Check if email already exists + if (users.some((user) => user.email === email)) { + Swal.fire({ + icon: "error", + title: "Email already exists", + text: "Please use a different email address", + }) + return + } + + // Create new user + const newUser = { + id: Date.now().toString(), + name, + email, + password, + role: "user", // Default role + createdAt: new Date().toISOString(), + } + + // Add user to localStorage + users.push(newUser) + localStorage.setItem("users", JSON.stringify(users)) + + Swal.fire({ + icon: "success", + title: "Account Created", + text: "Your account has been created successfully!", + timer: 1500, + showConfirmButton: false, + }) + + onSignup(newUser) + } + + return ( +
+

Create an Account

+
+
+ + setName(e.target.value)} + required + /> +
+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + setConfirmPassword(e.target.value)} + required + /> +
+
+ + +
+
+
+ ) +} diff --git a/src/client/dashboard/topbar.jsx b/src/client/dashboard/topbar.jsx new file mode 100644 index 00000000..bc54b257 --- /dev/null +++ b/src/client/dashboard/topbar.jsx @@ -0,0 +1,74 @@ +"use client" + +import { useState } from "react" +import { Bell, Menu, X, User } from "lucide-react" + +export default function Topbar({ user, onLogout }) { + const [showMobileMenu, setShowMobileMenu] = useState(false) + const [showProfileMenu, setShowProfileMenu] = useState(false) + + return ( +
+
+ + +
+ + +
+ + + {showProfileMenu && ( +
+
+
{user.name}
+
{user.email}
+
+ +
+ )} +
+
+
+ + {showMobileMenu && ( +
+ +
+ )} +
+ ) +} diff --git a/src/client/dashboard/users.jsx b/src/client/dashboard/users.jsx new file mode 100644 index 00000000..ff632916 --- /dev/null +++ b/src/client/dashboard/users.jsx @@ -0,0 +1,298 @@ +"use client" + +import { useState, useEffect } from "react" +import { Edit, Trash2, X, UserPlus } from "lucide-react" +import Swal from "sweetalert2" + +export default function Users() { + const [users, setUsers] = useState([]) + const [showModal, setShowModal] = useState(false) + const [formData, setFormData] = useState({ + id: "", + name: "", + email: "", + password: "", + role: "user", + }) + const [isEditing, setIsEditing] = useState(false) + + useEffect(() => { + // Load users from localStorage + const storedUsers = JSON.parse(localStorage.getItem("users") || "[]") + setUsers(storedUsers) + }, []) + + const handleInputChange = (e) => { + const { name, value } = e.target + setFormData({ + ...formData, + [name]: value, + }) + } + + const handleSubmit = (e) => { + e.preventDefault() + + if (isEditing) { + // Update existing user + const updatedUsers = users.map((user) => + user.id === formData.id ? { ...user, name: formData.name, email: formData.email, role: formData.role } : user, + ) + setUsers(updatedUsers) + localStorage.setItem("users", JSON.stringify(updatedUsers)) + + Swal.fire({ + icon: "success", + title: "User Updated", + text: `${formData.name} has been updated successfully`, + timer: 1500, + showConfirmButton: false, + }) + } else { + // Check if email already exists + if (users.some((user) => user.email === formData.email)) { + Swal.fire({ + icon: "error", + title: "Email already exists", + text: "Please use a different email address", + }) + return + } + + // Add new user + const newUser = { + ...formData, + id: Date.now().toString(), + createdAt: new Date().toISOString(), + } + const updatedUsers = [...users, newUser] + setUsers(updatedUsers) + localStorage.setItem("users", JSON.stringify(updatedUsers)) + + Swal.fire({ + icon: "success", + title: "User Added", + text: `${formData.name} has been added successfully`, + timer: 1500, + showConfirmButton: false, + }) + } + + resetForm() + } + + const handleEdit = (user) => { + setFormData({ + id: user.id, + name: user.name, + email: user.email, + password: "", + role: user.role, + }) + setIsEditing(true) + setShowModal(true) + } + + const handleDelete = (id) => { + Swal.fire({ + title: "Are you sure?", + text: "You won't be able to revert this!", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#3085d6", + cancelButtonColor: "#d33", + confirmButtonText: "Yes, delete it!", + }).then((result) => { + if (result.isConfirmed) { + const updatedUsers = users.filter((user) => user.id !== id) + setUsers(updatedUsers) + localStorage.setItem("users", JSON.stringify(updatedUsers)) + + Swal.fire({ + icon: "success", + title: "Deleted!", + text: "The user has been deleted.", + timer: 1500, + showConfirmButton: false, + }) + } + }) + } + + const resetForm = () => { + setFormData({ + id: "", + name: "", + email: "", + password: "", + role: "user", + }) + setIsEditing(false) + setShowModal(false) + } + + return ( +
+
+

Users

+ +
+ +
+
+ + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
Name + Email + Role + Created At + + Actions +
+
{user.name}
+
+
{user.email}
+
+ + {user.role} + + +
{new Date(user.createdAt).toLocaleDateString()}
+
+ + +
+
+
+ + {/* User Form Modal */} + {showModal && ( +
+
+
+

{isEditing ? "Edit User" : "Add New User"}

+ +
+ +
+
+ + +
+ +
+ + +
+ + {!isEditing && ( +
+ + +
+ )} + +
+ + +
+ +
+ + +
+
+
+
+ )} +
+ ) +} diff --git a/src/client/index.css b/src/client/index.css new file mode 100644 index 00000000..ec2585e8 --- /dev/null +++ b/src/client/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/src/client/index.js b/src/client/index.js index b597a442..edc4f4c8 100644 --- a/src/client/index.js +++ b/src/client/index.js @@ -1,5 +1,19 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import ReactDOM from 'react-dom/client'; +import './index.css'; import App from './App'; +import reportWebVitals from './reportWebVitals'; +import { CartProvider } from "./context/CartContext"; +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + + + +); -ReactDOM.render(, document.getElementById('root')); +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/src/client/logo.svg b/src/client/logo.svg new file mode 100644 index 00000000..9dfc1c05 --- /dev/null +++ b/src/client/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/client/pages/AboutPage.jsx b/src/client/pages/AboutPage.jsx new file mode 100644 index 00000000..f62d1d47 --- /dev/null +++ b/src/client/pages/AboutPage.jsx @@ -0,0 +1,141 @@ +import { Link } from "react-router-dom" +import { Users, Award, Clock, Heart } from 'lucide-react' + +const AboutPage = () => { + return ( +
+ {/* Page Header */} +
+

About ShopVista

+

+ We're on a mission to provide high-quality products at affordable prices while delivering an exceptional + shopping experience. +

+
+ + {/* Hero Section */} +
+
+

Our Story

+

+ Founded in 2020, ShopVista began with a simple idea: to create a shopping platform that puts customers first. + What started as a small online store has grown into a comprehensive marketplace offering thousands of + products across multiple categories. +

+

+ Our team is passionate about curating the best products from around the world and making them accessible to + everyone. We believe in quality, affordability, and exceptional customer service. +

+

+ Today, we serve customers across the country and continue to expand our offerings to meet the evolving needs + of our community. +

+
+
+ ShopVista Team +
+
+ + {/* Values Section */} +
+

Our Values

+
+
+
+ +
+

Customer First

+

+ We prioritize our customers' needs and strive to exceed their expectations in every interaction. +

+
+
+
+ +
+

Quality

+

+ We carefully select each product to ensure it meets our high standards for quality and durability. +

+
+
+
+ +
+

Efficiency

+

+ We value your time and work diligently to ensure fast processing, shipping, and customer support. +

+
+
+
+ +
+

Community

+

+ We believe in giving back and supporting the communities where our customers and team members live. +

+
+
+
+ + {/* Team Section */} +
+

Meet Our Team

+
+ {[ + { + name: "Sarah Johnson", + role: "Founder & CEO", + image: "https://images.pexels.com/photos/1681010/pexels-photo-1681010.jpeg", + }, + { + name: "Michael Chen", + role: "Head of Product", + image: "https://images.pexels.com/photos/1516680/pexels-photo-1516680.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1", + }, + { + name: "Emily Rodriguez", + role: "Customer Experience", + image: "https://images.pexels.com/photos/3841338/pexels-photo-3841338.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1", + }, + { + name: "David Kim", + role: "Operations Manager", + image: "https://images.pexels.com/photos/2072453/pexels-photo-2072453.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1", + }, + ].map((member, index) => ( +
+ {member.name} +
+

{member.name}

+

{member.role}

+
+
+ ))} +
+
+ + {/* CTA Section */} +
+

Ready to Start Shopping?

+

+ Explore our wide range of products and discover why thousands of customers choose ShopVista for their shopping + needs. +

+ + Shop Now + +
+
+ ) +} + +export default AboutPage diff --git a/src/client/pages/CartPage.jsx b/src/client/pages/CartPage.jsx new file mode 100644 index 00000000..11dbe828 --- /dev/null +++ b/src/client/pages/CartPage.jsx @@ -0,0 +1,227 @@ +"use client"; + +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { Minus, Plus, X, ShoppingBag, ArrowRight, CreditCard, Truck } from "lucide-react"; +import { useCart } from "../context/CartContext"; + +const CartPage = () => { + const { cartItems, updateQuantity, removeItem } = useCart(); + const [promoCode, setPromoCode] = useState(""); + const [promoApplied, setPromoApplied] = useState(false); + + const applyPromoCode = () => { + if (promoCode.toLowerCase() === "discount10") { + setPromoApplied(true); + } + }; + + // Calculate totals + const subtotal = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0); + const discount = promoApplied ? subtotal * 0.1 : 0; + const shipping = subtotal > 100 ? 0 : 4.99; + const tax = (subtotal - discount) * 0.07; + const total = subtotal - discount + shipping + tax; + + return ( +
+

Shopping Cart

+ + {cartItems.length === 0 ? ( +
+
+ +
+

Your cart is empty

+

Looks like you haven't added any products to your cart yet.

+ + Continue Shopping + +
+ ) : ( +
+ {/* Cart Items */} +
+
+
+

Cart Items ({cartItems.length})

+
+ {cartItems.map((item) => ( +
+
+ {item.name} +
+
+
+
+ + {item.name} + +
+ Color: {item.color} + β€’ + Size: {item.size} +
+
${item.price.toFixed(2)}
+
+
+
+ +
+ {item.quantity} +
+ +
+ +
+
+
+
+ ))} +
+
+
+
+ + + Continue Shopping + + + Proceed to Checkout + + +
+
+ + {/* Order Summary */} +
+
+
+

Order Summary

+
+
+ Subtotal + ${subtotal.toFixed(2)} +
+ {promoApplied && ( +
+ Discount (10%) + -${discount.toFixed(2)} +
+ )} +
+ Shipping + {shipping === 0 ? "Free" : `$${shipping.toFixed(2)}`} +
+
+ Tax (7%) + ${tax.toFixed(2)} +
+
+
+ Total + ${total.toFixed(2)} +
+
+
+
+ setPromoCode(e.target.value)} + disabled={promoApplied} + /> + +
+ {promoApplied && ( +
Promo code applied successfully!
+ )} + + Checkout + + +
+
+
+
+

We Accept

+
+
+ Visa +
+
+ Mastercard +
+
+ American Express +
+
+ PayPal +
+
+
+
+ + Free shipping on orders over $100 +
+
+ + Secure payment processing +
+
+
+
+
+
+ )} +
+ ); +}; + +export default CartPage; \ No newline at end of file diff --git a/src/client/pages/CategoryPage.jsx b/src/client/pages/CategoryPage.jsx new file mode 100644 index 00000000..3daee893 --- /dev/null +++ b/src/client/pages/CategoryPage.jsx @@ -0,0 +1,50 @@ +"use client" + +import { useParams } from "react-router-dom" +import { useState, useEffect } from "react" + +export default function CategoryPage() { + const { slug } = useParams() + const [products, setProducts] = useState([]) + + const categoryName = slug + ? slug + .split("-") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" ") + : "Category" + + useEffect(() => { + const mockProducts = Array.from({ length: 8 }, (_, i) => ({ + id: i + 1, + name: `${categoryName} Product ${i + 1}`, + price: Math.floor(Math.random() * 100) + 10, + image: `https://cart-mart.netlify.app/img/products/f1.jpg`, + })) + setProducts(mockProducts) + }, [slug]) + + return ( +
+
+

{categoryName}

+ +
+ {products.map((product) => ( +
+
+ {product.name} +
+

{product.name}

+

${product.price.toFixed(2)}

+
+ ))} +
+
+
+ ) +} diff --git a/src/client/pages/CheckoutPage.jsx b/src/client/pages/CheckoutPage.jsx new file mode 100644 index 00000000..92a317be --- /dev/null +++ b/src/client/pages/CheckoutPage.jsx @@ -0,0 +1,793 @@ +/* global emailjs */ +"use client"; + +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { Check, ChevronRight, ArrowLeft, ShieldCheck, Lock, Truck, CreditCard, Bitcoin } from "lucide-react"; +import { useCart } from "../context/CartContext"; + +const CheckoutPage = () => { + const { cartItems, clearCart } = useCart(); + const [step, setStep] = useState("shipping"); + const [orderComplete, setOrderComplete] = useState(false); + const [cryptoError, setCryptoError] = useState(""); + const [cryptoTxHash, setCryptoTxHash] = useState(""); + const [web3, setWeb3] = useState(null); + const [userAccount, setUserAccount] = useState(null); + + // Form state + const [email, setEmail] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [address, setAddress] = useState(""); + const [city, setCity] = useState(""); + const [state, setState] = useState(""); + const [zipCode, setZipCode] = useState(""); + const [country, setCountry] = useState("US"); + const [phone, setPhone] = useState(""); + const [paymentMethod, setPaymentMethod] = useState("credit-card"); + const [saveInfo, setSaveInfo] = useState(false); + + // Calculate totals + const subtotal = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0); + const shipping = subtotal > 100 ? 0 : 4.99; + const tax = subtotal * 0.07; + const total = subtotal + shipping + tax; + + // Initialize Web3 for MetaMask + useEffect(() => { + if (window.ethereum && window.Web3) { + const web3Instance = new window.Web3(window.ethereum); + setWeb3(web3Instance); + } else { + setCryptoError("MetaMask is not installed. Please install it to pay with crypto."); + } + }, []); + + // Connect MetaMask wallet + const connectMetaMask = async () => { + if (!web3) { + setCryptoError("Please install MetaMask to pay with Ethereum."); + return; + } + try { + const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); + setUserAccount(accounts[0]); + setCryptoError(""); + } catch (error) { + setCryptoError("Failed to connect MetaMask: " + error.message); + } + }; + + // Pay with MetaMask (Ethereum) + const payWithMetaMask = async () => { + if (!userAccount) { + setCryptoError("Please connect MetaMask first."); + return; + } + try { + const merchantAddress = "YOUR_ETHEREUM_WALLET_ADDRESS"; // Replace with your Ethereum wallet (e.g., 0x123...) + const amountEth = (total / 2000).toFixed(6); // Approx. $2000/ETH, adjust as needed + const amountWei = web3.utils.toWei(amountEth, "ether"); + + const tx = await web3.eth.sendTransaction({ + from: userAccount, + to: merchantAddress, + value: amountWei, + }); + setCryptoTxHash(tx.transactionHash); + setCryptoError(""); + return tx.transactionHash; + } catch (error) { + setCryptoError("Payment failed: " + error.message); + return null; + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (step === "shipping") { + setStep("payment"); + } else if (step === "payment") { + if (paymentMethod === "crypto") { + if (cryptoTxHash) { + setStep("review"); + } else { + setCryptoError("Please complete the crypto payment."); + } + } else { + setStep("review"); + } + } else if (step === "review") { + if (paymentMethod === "crypto" && !cryptoTxHash) { + setCryptoError("Crypto payment not completed."); + return; + } + setOrderComplete(true); + clearCart(); + + // Send email using EmailJS + const user = { + name: `${firstName} ${lastName}`, + email: email, + }; + emailjs.send("service_88hvpdg", "template_2je42ji", { + user_name: user.name, + user_email: user.email, + }).then((response) => { + console.log('Email sent successfully!', response.status, response.text); + }, (error) => { + console.error('Failed to send email:', error); + }); + } + }; + + if (cartItems.length === 0 && !orderComplete) { + return ( +
+

Your Cart is Empty

+

+ It looks like you haven't added any items to your cart yet. +

+ + Shop Now + +
+ ); + } + + if (orderComplete) { + return ( +
+
+
+ +
+

Order Confirmed!

+

+ Thank you for your purchase. Your order has been confirmed and will be shipped soon. +

+
+

Order Details

+
+ Order Number: + ORD-{Math.floor(100000 + Math.random() * 900000)} +
+
+ Date: + {new Date().toLocaleDateString()} +
+
+ Total: + ${total.toFixed(2)} +
+
+ Payment Method: + {paymentMethod === "credit-card" ? "Credit Card" : paymentMethod === "crypto" ? "Cryptocurrency" : paymentMethod} +
+ {cryptoTxHash && ( + + )} +
+
+ + Continue Shopping + + + View Order + +
+
+
+ ); + } + + return ( +
+ {/* Load Web3.js via CDN */} + + {/* Load EmailJS SDK via CDN */} + + +
+ + Home + + / + + Cart + + / + Checkout +
+ +

Checkout

+ +
+ {/* Main Content */} +
+ {/* Progress Steps */} +
+
+
+
+ 1 +
+
+

Shipping

+
+
+
+
+
+ 2 +
+
+

Payment

+
+
+
+
+
+ 3 +
+
+

Review

+
+
+
+
+ +
+ {/* Shipping Step */} + {step === "shipping" && ( +
+
+

Shipping Information

+
+
+
+ + setEmail(e.target.value)} + required + /> +
+
+
+
+ + setFirstName(e.target.value)} + required + /> +
+
+ + setLastName(e.target.value)} + required + /> +
+
+
+ + setAddress(e.target.value)} + required + /> +
+
+
+ + setCity(e.target.value)} + required + /> +
+
+ + +
+
+
+
+ + setZipCode(e.target.value)} + required + /> +
+
+ + +
+
+
+ + setPhone(e.target.value)} + required + /> +
+
+ setSaveInfo(e.target.checked)} + /> + +
+
+
+
+ + + Back to Cart + + +
+
+ )} + + {/* Payment Step */} + {step === "payment" && ( +
+
+

Payment Method

+ {cryptoError && ( +
{cryptoError}
+ )} +
+
+ setPaymentMethod("credit-card")} + className="h-4 w-4" + /> + +
+
+ Visa +
+
+
+
+ setPaymentMethod("paypal")} + className="h-4 w-4" + /> + +
+
+ setPaymentMethod("apple-pay")} + className="h-4 w-4" + /> + +
+
+ setPaymentMethod("crypto")} + className="h-4 w-4" + /> + +
+ {paymentMethod === "crypto" && ( +
+
+

Pay with Ethereum via MetaMask

+
+ + +
+ {cryptoTxHash && ( +

+ Transaction successful! TX Hash:{" "} + + {cryptoTxHash.slice(0, 6)}...{cryptoTxHash.slice(-4)} + +

+ )} +
+
+ + Crypto payments are secure and processed on the blockchain +
+
+ )} + {paymentMethod === "credit-card" && ( +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + Your payment information is secure and encrypted +
+
+ )} +
+
+
+ + +
+
+ )} + + {/* Review Step */} + {step === "review" && ( +
+
+

Review Your Order

+
+
+

Shipping Information

+
+

+ + {firstName} {lastName} + +

+

+ {address} +
+ {city}, {state} {zipCode} +
+ {country === "US" ? "United States" : country} +
+ {phone} +

+
+ +
+
+

Payment Method

+
+ {paymentMethod === "credit-card" && ( +
+ +
+

Credit Card

+

**** **** **** 1234

+
+
+ )} + {paymentMethod === "paypal" && ( +
+
+ PayPal +
+

PayPal

+
+ )} + {paymentMethod === "apple-pay" && ( +
+
+ Apple Pay +
+

Apple Pay

+
+ )} + {paymentMethod === "crypto" && ( +
+ +
+

Cryptocurrency

+

Paid with Ethereum

+
+
+ )} +
+ +
+
+

Order Items

+
+ {cartItems.map((item) => ( +
+
+ {item.name} +
+
+

{item.name}

+

+ {item.color} β€’ {item.size} β€’ Qty: {item.quantity} +

+
+
${(item.price * item.quantity).toFixed(2)}
+
+ ))} +
+
+
+
+
+ + +
+
+ )} +
+
+ + {/* Order Summary */} +
+
+
+

Order Summary

+
+
+ Subtotal + ${subtotal.toFixed(2)} +
+
+ Shipping + {shipping === 0 ? "Free" : `$${shipping.toFixed(2)}`} +
+
+ Tax (7%) + ${tax.toFixed(2)} +
+
+
+ Total + ${total.toFixed(2)} +
+
+
+
+
+
+
+ + Secure Checkout +
+
+ + Free shipping on orders over $100 +
+
+ + Multiple payment options +
+
+
+
+
+
+
+ ); +}; + +export default CheckoutPage; \ No newline at end of file diff --git a/src/client/pages/ContactPage.jsx b/src/client/pages/ContactPage.jsx new file mode 100644 index 00000000..ab0e38b5 --- /dev/null +++ b/src/client/pages/ContactPage.jsx @@ -0,0 +1,252 @@ +"use client" + +import { useState } from "react" +import { MapPin, Phone, Mail, Clock } from "lucide-react" + +const ContactPage = () => { + const [formData, setFormData] = useState({ + name: "", + email: "", + subject: "", + message: "", + }) + const [formStatus, setFormStatus] = useState(null) + + const handleChange = (e) => { + const { name, value } = e.target + setFormData((prev) => ({ + ...prev, + [name]: value, + })) + } + + const handleSubmit = (e) => { + e.preventDefault() + // In a real app, you would send the form data to your backend + console.log("Form submitted:", formData) + + // Simulate form submission + setFormStatus("success") + + // Reset form after submission + setFormData({ + name: "", + email: "", + subject: "", + message: "", + }) + + // Clear success message after 5 seconds + setTimeout(() => { + setFormStatus(null) + }, 5000) + } + + return ( +
+ {/* Page Header */} +
+

Contact Us

+

+ Have questions or feedback? We'd love to hear from you. Our team is here to help. +

+
+ +
+ {/* Contact Information */} +
+
+

Get In Touch

+ +
+
+
+ +
+
+

Our Location

+

123 Commerce Street

+

Shopville, SV 12345

+

United States

+
+
+ +
+
+ +
+
+

Phone Number

+

+1 (555) 123-4567

+

Mon-Fri, 9am-6pm EST

+
+
+ +
+
+ +
+
+

Email Address

+

support@shopvista.com

+

sales@shopvista.com

+
+
+ +
+
+ +
+
+

Business Hours

+

Monday - Friday: 9am - 6pm

+

Saturday: 10am - 4pm

+

Sunday: Closed

+
+
+
+
+
+ + {/* Contact Form */} +
+
+

Send Us a Message

+ + {formStatus === "success" && ( +
+ Thank you for your message! We'll get back to you as soon as possible. +
+ )} + +
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
+ + {/* Map Section */} +
+

Our Location

+
+ {/* In a real app, you would embed a Google Map or other map service here */} +
+ +
+
+
+ + {/* FAQ Section */} +
+

Frequently Asked Questions

+
+ {[ + { + question: "What are your shipping options?", + answer: + "We offer standard shipping (3-5 business days), express shipping (1-2 business days), and same-day delivery in select areas. Shipping is free for orders over $50.", + }, + { + question: "How can I track my order?", + answer: + "Once your order ships, you'll receive a tracking number via email. You can also track your order by logging into your account and viewing your order history.", + }, + { + question: "What is your return policy?", + answer: + "We accept returns within 30 days of purchase. Items must be in original condition with tags attached. Please visit our Returns page for more information.", + }, + { + question: "Do you ship internationally?", + answer: + "Yes, we ship to select countries internationally. International shipping rates and delivery times vary by location. Please check our Shipping page for details.", + }, + ].map((faq, index) => ( +
+

{faq.question}

+

{faq.answer}

+
+ ))} +
+
+
+ ) +} + +export default ContactPage diff --git a/src/client/pages/HomePage.jsx b/src/client/pages/HomePage.jsx new file mode 100644 index 00000000..cb6e4727 --- /dev/null +++ b/src/client/pages/HomePage.jsx @@ -0,0 +1,508 @@ +"use client"; + +import { Link } from "react-router-dom"; +import { ArrowRight, Star, ShoppingCart, Heart, Bitcoin } from "lucide-react"; +import { useCart } from "../context/CartContext"; +import { useWishlist } from "../context/WishlistContext"; +import { toast } from "react-toastify"; +import { useState } from "react"; +import axios from "axios"; + +const HomePage = () => { + const { addToCart } = useCart(); + const { wishlistItems, addToWishlist, removeFromWishlist } = useWishlist(); + const [isModalOpen, setIsModalOpen] = useState(false); // Control modal visibility + const [txId, setTxId] = useState(""); // Store user-entered TXID + const [txStatus, setTxStatus] = useState(null); // Store transaction details + + // Blockchair API key (WARNING: Exposed in frontend for demo only) + const BLOCKCHAIR_API_KEY = "YOUR_BLOCKCHAIR_API_KEY"; // Replace with your key + + const featuredProducts = [ + { + id: 1, + name: "Premium Cotton T-Shirt", + price: 29.99, + originalPrice: 39.99, + rating: 4.5, + reviewCount: 120, + image: "https://plus.unsplash.com/premium_photo-1675186049366-64a655f8f537?q=80&w=1374&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", + isNew: true, + isSale: true, + slug: "premium-cotton-t-shirt", + }, + { + id: 2, + name: "Slim Fit Jeans", + price: 59.99, + originalPrice: null, + rating: 4.8, + reviewCount: 85, + image: "https://cart-mart.netlify.app/img/products/f2.jpg", + isNew: true, + isSale: false, + slug: "slim-fit-jeans", + }, + { + id: 3, + name: "Leather Crossbody Bag", + price: 79.99, + originalPrice: 99.99, + rating: 4.7, + reviewCount: 64, + image: "https://cart-mart.netlify.app/img/products/f3.jpg", + isNew: false, + isSale: true, + slug: "leather-crossbody-bag", + }, + { + id: 4, + name: "Wireless Headphones", + price: 129.99, + originalPrice: 149.99, + rating: 4.9, + reviewCount: 210, + image: "https://cart-mart.netlify.app/img/products/f6.jpg", + isNew: false, + isSale: true, + slug: "wireless-headphones", + }, + ]; + + const categories = [ + { + id: 1, + name: "Men's Fashion", + image: "https://cart-mart.netlify.app/img/shop/19.jpg", + count: 120, + slug: "mens-fashion", + }, + { + id: 2, + name: "Women's Fashion", + image: "https://cart-mart.netlify.app/img/shop/17.jpg", + count: 150, + slug: "womens-fashion", + }, + { + id: 3, + name: "Accessories", + image: "https://cart-mart.netlify.app/img/shop/20.jpg", + count: 80, + slug: "accessories", + }, + { + id: 4, + name: "Footwear", + image: "https://cart-mart.netlify.app/img/products/f7.jpg", + count: 95, + slug: "footwear", + }, + { + id: 5, + name: "Electronics", + image: "https://cart-mart.netlify.app/img/shop/21.jpg", + count: 110, + slug: "electronics", + }, + { + id: 6, + name: "Home & Living", + image: "https://cart-mart.netlify.app/img/shop/16.jpg", + count: 75, + slug: "home-living", + }, + ]; + + const handleAddToCart = (product) => { + addToCart(product, 1, "Default", "Default"); + toast.success(`${product.name} has been added to your cart!`); + }; + + const handleToggleWishlist = (product) => { + const isInWishlist = wishlistItems.find((item) => item.id === product.id); + if (isInWishlist) { + removeFromWishlist(product.id); + toast.info(`${product.name} removed from wishlist.`); + } else { + addToWishlist(product); + toast.success(`${product.name} added to wishlist!`); + } + }; + + const handleVerifyTransaction = async () => { + if (!txId) { + toast.error("Please enter a Transaction ID."); + return; + } + try { + const response = await axios.get( + `https://api.blockchair.com/bitcoin/transactions/${txId}?key=${BLOCKCHAIR_API_KEY}` + ); + const txData = response.data.data[txId]; + setTxStatus({ + status: txData.status, + amount: txData.amount, + time: txData.time, + }); + toast.success(`Transaction status: ${txData.status}`); + } catch (error) { + toast.error("Failed to verify transaction. Check the Transaction ID."); + setTxStatus(null); + } + }; + + const openModal = () => setIsModalOpen(true); + const closeModal = () => { + setIsModalOpen(false); + setTxId(""); + setTxStatus(null); + }; + + return ( + <> + {/* Hero Section */} +
+
+
+
+
+

+ Summer Collection 2025 +

+

+ Discover the latest trends and styles for the summer season. +

+
+
+ + Shop Now + + + Explore Collections + +
+
+
+ {[1, 2, 3, 4].map((i) => ( +
+ {`Customer +
+ ))} +
+
+ 500+ happy customers +
+
+
+
+
+ Summer Collection +
+
+
+
+ 30% +
+
+

Summer Sale

+

Use code: SUMMER30

+
+
+
+
+
+
+
+
+
+ + {/* Categories Section */} +
+
+
+

Shop by Category

+

Browse our wide range of products by category

+
+
+ {categories.map((category) => ( + +
+ {category.name} +
+
+

{category.name}

+

{category.count} Products

+
+
+
+ + ))} +
+
+
+ + {/* Featured Products Section */} +
+
+
+
+

Featured Products

+

Handpicked products for you

+
+
+ + View All Products β†’ + + +
+
+
+ {featuredProducts.map((product) => { + const isInWishlist = wishlistItems.find((item) => item.id === product.id); + return ( +
+
+ + {product.name} + +
+ {product.isNew && ( + New + )} + {product.isSale && ( + Sale + )} +
+
+ + +
+
+
+ +

+ {product.name} +

+ +
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ ({product.reviewCount}) +
+
+
+ ${product.price.toFixed(2)} + {product.originalPrice && ( + + ${product.originalPrice.toFixed(2)} + + )} +
+
+
+
+ ); + })} +
+
+
+ + {/* Crypto Verification Modal */} + {isModalOpen && ( +
+
+

Verify Bitcoin Payment

+

+ Enter a Bitcoin Transaction ID (TXID) to check its status on the blockchain. +

+ setTxId(e.target.value)} + className="w-full px-4 py-2 border rounded-md mb-4" + /> + + {txStatus && ( +
+

Transaction Status:

+

{txStatus.status}

+ {txStatus.status === "confirmed" && ( + <> +

Amount: {txStatus.amount} BTC

+

Time: {new Date(txStatus.time).toLocaleString()}

+ + )} +
+ )} + +
+
+ )} + + {/* Special Offers Section */} +
+
+
+

Special Offers

+

Limited time deals you don't want to miss

+
+
+
+
+ Summer Sale +
+
+
+ + Limited Time + +

Summer Sale

+

+ Up to 50% off on summer essentials. Refresh your wardrobe today! +

+
+
+ + Shop Now + + + Learn More + +
+
+
+
+
+ New Arrivals +
+
+
+ + Just Launched + +

New Arrivals

+

+ Discover our latest collection of premium products just for you. +

+
+
+ + Explore Now + + + View Collection + +
+
+
+
+
+
+ + {/* Newsletter Section */} +
+
+
+

Join Our Newsletter

+

+ Subscribe to our newsletter and be the first to know about new products, special offers, and exclusive + discounts. +

+
+ + +
+

+ By subscribing, you agree to our Privacy Policy and consent to receive updates from our company. +

+
+
+
+ + ); +}; + +export default HomePage; \ No newline at end of file diff --git a/src/client/pages/LoginPage.jsx b/src/client/pages/LoginPage.jsx new file mode 100644 index 00000000..97fd4ff2 --- /dev/null +++ b/src/client/pages/LoginPage.jsx @@ -0,0 +1,436 @@ +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { Eye, EyeOff, Facebook, Github } from "lucide-react"; +import Swal from 'sweetalert2'; + +const LoginPage = () => { + const [activeTab, setActiveTab] = useState("login"); + const [showLoginPassword, setShowLoginPassword] = useState(false); + const [showRegisterPassword, setShowRegisterPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + // Login form state + const [loginEmail, setLoginEmail] = useState(""); + const [loginPassword, setLoginPassword] = useState(""); + const [rememberMe, setRememberMe] = useState(false); + + // Register form state + const [name, setName] = useState(""); + const [registerEmail, setRegisterEmail] = useState(""); + const [registerPassword, setRegisterPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [terms, setTerms] = useState(false); + + const handleLoginSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + const response = await fetch("http://localhost:5000/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: loginEmail, password: loginPassword, rememberMe }), + signal: controller.signal, + }); + clearTimeout(timeoutId); + if (response.status === 404) { + throw new Error('Login endpoint not found. Please check if the backend is running on http://localhost:5000.'); + } + const data = await response.json(); + if (!response.ok) { + throw new Error(data.message || "Login failed"); + } + localStorage.setItem("token", data.token); + localStorage.setItem("user", JSON.stringify(data.user)); + await Swal.fire({ + icon: 'success', + title: 'Login Successful', + text: `Welcome back, ${data.user.username}!`, + timer: 1500, + showConfirmButton: false, + }); + navigate("/HomePage"); // Changed from /LoginPage to /HomePage + } catch (err) { + const errorMessage = err.name === 'AbortError' + ? 'Request timed out. Please check if the backend is running on http://localhost:5000.' + : err.message || 'Failed to connect to the server. Please check if the backend is running.'; + await Swal.fire({ + icon: 'error', + title: 'Login Failed', + text: errorMessage, + }); + } finally { + setLoading(false); + } + }; + + const handleRegisterSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + if (registerPassword !== confirmPassword) { + await Swal.fire({ + icon: 'error', + title: 'Validation Error', + text: 'Passwords do not match', + }); + setLoading(false); + return; + } + if (!terms) { + await Swal.fire({ + icon: 'error', + title: 'Validation Error', + text: 'You must agree to the terms', + }); + setLoading(false); + return; + } + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + const response = await fetch("http://localhost:5000/api/auth/signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: name, + email: registerEmail, + password: registerPassword, + }), + signal: controller.signal, + }); + clearTimeout(timeoutId); + if (response.status === 404) { + throw new Error('Signup endpoint not found. Please check if the backend is running on http://localhost:5000.'); + } + const text = await response.text(); + let data; + try { + data = JSON.parse(text); + } catch (jsonError) { + console.error('Response text:', text); + throw new Error('Invalid server response. Please check if the backend is configured correctly.'); + } + if (!response.ok) { + throw new Error(data.message || "Registration failed"); + } + localStorage.setItem("token", data.token); + localStorage.setItem("user", JSON.stringify(data.user)); + await Swal.fire({ + icon: 'success', + title: 'Registration Successful', + text: `Welcome, ${data.user.username}! Please log in.`, + timer: 1500, + showConfirmButton: false, + }); + setActiveTab("login"); // Switch to login tab instead of navigating + setName(""); + setRegisterEmail(""); + setRegisterPassword(""); + setConfirmPassword(""); + setTerms(false); + } catch (err) { + const errorMessage = err.name === 'AbortError' + ? 'Request timed out. Please check if the backend is running on http://localhost:5000.' + : err.message || 'Failed to connect to the server. Please check if the backend is running.'; + await Swal.fire({ + icon: 'error', + title: 'Registration Failed', + text: errorMessage, + }); + console.error('Signup error:', err); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+

Welcome to ShopVista

+

Sign in to your account or create a new one

+
+ +
+
+ + +
+ + {/* Login Tab */} + {activeTab === "login" && ( +
+
+ + setLoginEmail(e.target.value)} + required + disabled={loading} + /> +
+ +
+
+ + + Forgot password? + +
+
+ setLoginPassword(e.target.value)} + required + disabled={loading} + /> + +
+
+ +
+ setRememberMe(e.target.checked)} + disabled={loading} + /> + +
+ + +
+ )} + + {/* Register Tab */} + {activeTab === "register" && ( +
+
+ + setName(e.target.value)} + required + disabled={loading} + /> +
+ +
+ + setRegisterEmail(e.target.value)} + required + disabled={loading} + /> +
+ +
+ +
+ setRegisterPassword(e.target.value)} + required + disabled={loading} + /> + +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + required + disabled={loading} + /> + +
+
+ +
+ setTerms(e.target.checked)} + required + disabled={loading} + /> + +
+ + +
+ )} + +
+
+
+
+
+
+ Or continue with +
+
+ +
+ + + +
+
+
+
+
+
+ ); +}; + +export default LoginPage; \ No newline at end of file diff --git a/src/client/pages/NotFoundPage.jsx b/src/client/pages/NotFoundPage.jsx new file mode 100644 index 00000000..bcd41166 --- /dev/null +++ b/src/client/pages/NotFoundPage.jsx @@ -0,0 +1,21 @@ +import { Link } from "react-router-dom" + +const NotFoundPage = () => { + return ( +
+

404

+

Page Not Found

+

+ The page you are looking for might have been removed, had its name changed, or is temporarily unavailable. +

+ + Back to Homepage + +
+ ) +} + +export default NotFoundPage diff --git a/src/client/pages/ProductDetailPage.jsx b/src/client/pages/ProductDetailPage.jsx new file mode 100644 index 00000000..50b69f83 --- /dev/null +++ b/src/client/pages/ProductDetailPage.jsx @@ -0,0 +1,467 @@ +"use client"; + +import { useState } from "react"; +import { useParams, Link } from "react-router-dom"; +import { Star, ShoppingCart, Heart, Share2, Truck, RotateCcw, Shield, Minus, Plus, Check } from "lucide-react"; +import { useCart } from "../context/CartContext"; + +// Mock product data +const product = { + id: 1, + name: "Premium Cotton T-Shirt", + price: 29.99, + originalPrice: 39.99, + rating: 4.5, + reviewCount: 120, + images: [ + "https://cart-mart.netlify.app/img/products/f2.jpg", + "https://cart-mart.netlify.app/img/products/f3.jpg", + "https://cart-mart.netlify.app/img/products/n2.jpg", + "https://cart-mart.netlify.app/img/products/f6.jpg ", + ], + colors: [ + { name: "Black", value: "#000000" }, + { name: "White", value: "#ffffff" }, + { name: "Gray", value: "#808080" }, + { name: "Navy", value: "#000080" }, + ], + sizes: ["XS", "S", "M", "L", "XL", "XXL"], + description: + "Our Premium Cotton T-Shirt is crafted from 100% organic cotton for exceptional comfort and durability. The classic fit and soft fabric make it perfect for everyday wear, while the reinforced stitching ensures it will last through countless washes. Available in multiple colors, this versatile t-shirt is a must-have addition to any wardrobe.", + features: ["100% organic cotton", "Classic fit", "Reinforced stitching", "Pre-shrunk fabric", "Machine washable"], + details: { + material: "100% Organic Cotton", + fit: "Classic Fit", + care: "Machine wash cold, tumble dry low", + origin: "Ethically made in Portugal", + }, + stock: 25, + isNew: true, + isSale: true, + category: "Clothing", + brand: "FashionBrand", + slug: "premium-cotton-t-shirt", + relatedProducts: [ + { + id: 2, + name: "Slim Fit Jeans", + price: 59.99, + image: "https://cart-mart.netlify.app/img/shop/1.jpg", + slug: "slim-fit-jeans", + }, + { + id: 8, + name: "Denim Jacket", + price: 69.99, + image: "https://cart-mart.netlify.app/img/shop/4.jpg", + slug: "denim-jacket", + }, + { + id: 12, + name: "Graphic T-Shirt", + price: 24.99, + image: "https://cart-mart.netlify.app/img/products/f6.jpg", + slug: "graphic-t-shirt", + }, + { + id: 9, + name: "Casual Sneakers", + price: 59.99, + image: "https://cart-mart.netlify.app/img/shop/3.jpg", + slug: "casual-sneakers", + }, + ], +}; + +const ProductDetailPage = () => { + const { slug } = useParams(); + const { addToCart } = useCart(); + const [selectedImage, setSelectedImage] = useState(0); + const [selectedColor, setSelectedColor] = useState(product.colors[0]); + const [selectedSize, setSelectedSize] = useState(product.sizes[2]); // Default to M + const [quantity, setQuantity] = useState(1); + const [activeTab, setActiveTab] = useState("description"); + + const incrementQuantity = () => { + if (quantity < product.stock) { + setQuantity(quantity + 1); + } + }; + + const decrementQuantity = () => { + if (quantity > 1) { + setQuantity(quantity - 1); + } + }; + + const handleAddToCart = () => { + addToCart(product, quantity, selectedColor.name, selectedSize); + alert(`${product.name} (${selectedColor.name}, ${selectedSize}) has been added to your cart!`); + }; + + const handleAddRelatedToCart = (relatedProduct) => { + addToCart(relatedProduct, 1, "Default", "Default"); + alert(`${relatedProduct.name} has been added to your cart!`); + }; + + return ( +
+ {/* Breadcrumbs */} +
+ + Home + + / + + Products + + / + {product.name} +
+ +
+ {/* Product Images */} +
+
+ {`${product.name} +
+ {product.isNew && ( + New + )} + {product.isSale && ( + Sale + )} +
+
+
+ {product.images.map((image, index) => ( + + ))} +
+
+ + {/* Product Info */} +
+
+

{product.name}

+
+
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ ({product.reviewCount} reviews) +
+ SKU: PRD-{product.id.toString().padStart(5, "0")} +
+
+ +
+ ${product.price} + {product.originalPrice && ( + ${product.originalPrice} + )} + {product.isSale && ( + + Save ${(product.originalPrice - product.price).toFixed(2)} + + )} +
+ +
+
+

Color

+
+ {product.colors.map((color) => ( + + ))} +
+

Selected: {selectedColor.name}

+
+ +
+

Size

+
+ {product.sizes.map((size) => ( + + ))} +
+ + Size Guide + +
+ +
+

Quantity

+
+ +
+ {quantity} +
+ + {product.stock} available +
+
+
+ +
+ + +
+ +
+
+ +
+

Free Shipping

+

On orders over $50

+
+
+
+ +
+

Free Returns

+

Within 30 days

+
+
+
+ +
+

Secure Payment

+

Encrypted transactions

+
+
+
+ +
+
+
+
+ + {/* Product Details Tabs */} +
+
+
+ + + +
+
+ +
+ {activeTab === "description" && ( +
+
+

About the Product

+

{product.description}

+

Key Features

+
    + {product.features.map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ Product lifestyle +
+
+ )} + + {activeTab === "details" && ( +
+

Product Specifications

+
+
+

Material

+

{product.details.material}

+
+
+

Fit

+

{product.details.fit}

+
+
+

Care

+

{product.details.care}

+
+
+

Origin

+

{product.details.origin}

+
+
+

Brand

+

{product.brand}

+
+
+

Category

+

{product.category}

+
+
+
+ )} + + {activeTab === "reviews" && ( +
+

Customer Reviews

+

No reviews yet. Be the first to review this product.

+ +
+ )} +
+
+ + {/* Related Products */} +
+
+

You May Also Like

+ + View All β†’ + +
+ +
+ {product.relatedProducts.map((relatedProduct) => ( +
+ + {relatedProduct.name} + +
+ +

{relatedProduct.name}

+ +
+ ${relatedProduct.price} + +
+
+
+ ))} +
+
+
+ ); +}; + +export default ProductDetailPage; \ No newline at end of file diff --git a/src/client/pages/ProductsPage.jsx b/src/client/pages/ProductsPage.jsx new file mode 100644 index 00000000..1e1a2e5f --- /dev/null +++ b/src/client/pages/ProductsPage.jsx @@ -0,0 +1,827 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { Star, ShoppingCart, Heart, Filter, X, ChevronDown, SlidersHorizontal, Grid, List } from "lucide-react"; +import { useCart } from "../context/CartContext"; + +// Mock product data +const products = [ + { + id: 1, + name: "Premium Cotton T-Shirt", + price: 29.99, + originalPrice: 39.99, + rating: 4.5, + reviewCount: 120, + image: "https://cart-mart.netlify.app/img/products/n8.jpg", + isNew: true, + isSale: true, + slug: "premium-cotton-t-shirt", + category: "Clothing", + colors: ["Black", "White", "Gray"], + brand: "FashionBrand", + }, + { + id: 2, + name: "Slim Fit Jeans", + price: 59.99, + originalPrice: null, + rating: 4.8, + reviewCount: 85, + image: "https://cart-mart.netlify.app/img/products/n2.jpg", + isNew: true, + isSale: false, + slug: "slim-fit-jeans", + category: "Clothing", + colors: ["Blue", "Black"], + brand: "DenimCo", + }, + { + id: 3, + name: "Leather Crossbody Bag", + price: 79.99, + originalPrice: 99.99, + rating: 4.7, + reviewCount: 64, + image: "https://cart-mart.netlify.app/img/products/n5.jpg", + isNew: false, + isSale: true, + slug: "leather-crossbody-bag", + category: "Accessories", + colors: ["Brown", "Black", "Tan"], + brand: "LuxLeather", + }, + { + id: 4, + name: "Wireless Headphones", + price: 129.99, + originalPrice: 149.99, + rating: 4.9, + reviewCount: 210, + image: "https://cart-mart.netlify.app/img/shop/23.jpg", + isNew: false, + isSale: true, + slug: "wireless-headphones", + category: "Electronics", + colors: ["Black", "Silver", "Rose Gold"], + brand: "SoundTech", + }, + { + id: 5, + name: "Smart Watch", + price: 199.99, + originalPrice: null, + rating: 4.6, + reviewCount: 178, + image: "https://cart-mart.netlify.app/img/shop/10.jpg", + isNew: true, + isSale: false, + slug: "smart-watch", + category: "Electronics", + colors: ["Black", "Silver"], + brand: "TechGear", + }, + { + id: 6, + name: "Running Shoes", + price: 89.99, + originalPrice: 109.99, + rating: 4.4, + reviewCount: 156, + image: "https://cart-mart.netlify.app/img/shop/14.jpg", + isNew: false, + isSale: true, + slug: "running-shoes", + category: "Footwear", + colors: ["Blue", "Black", "Red"], + brand: "SportStep", + }, +]; + +// Filter options +const categories = [ + { id: "clothing", label: "Clothing" }, + { id: "footwear", label: "Footwear" }, + { id: "accessories", label: "Accessories" }, + { id: "electronics", label: "Electronics" }, +]; + +const colors = [ + { id: "black", label: "Black" }, + { id: "white", label: "White" }, + { id: "gray", label: "Gray" }, + { id: "blue", label: "Blue" }, + { id: "brown", label: "Brown" }, + { id: "red", label: "Red" }, + { id: "silver", label: "Silver" }, +]; + +const brands = [ + { id: "fashionbrand", label: "FashionBrand" }, + { id: "denimco", label: "DenimCo" }, + { id: "luxleather", label: "LuxLeather" }, + { id: "soundtech", label: "SoundTech" }, + { id: "techgear", label: "TechGear" }, + { id: "sportstep", label: "SportStep" }, +]; + +const ProductsPage = () => { + const { addToCart } = useCart(); // Access addToCart from CartContext + const [filteredProducts, setFilteredProducts] = useState(products); + const [priceRange, setPriceRange] = useState([0, 200]); + const [selectedCategories, setSelectedCategories] = useState([]); + const [selectedColors, setSelectedColors] = useState([]); + const [selectedBrands, setSelectedBrands] = useState([]); + const [sortOption, setSortOption] = useState("featured"); + const [viewMode, setViewMode] = useState("grid"); + const [isMobileFilterOpen, setIsMobileFilterOpen] = useState(false); + const [colorSelections, setColorSelections] = useState({}); // Track selected color for each product + + // Apply filters + useEffect(() => { + let result = [...products]; + + // Filter by price + result = result.filter((product) => product.price >= priceRange[0] && product.price <= priceRange[1]); + + // Filter by category + if (selectedCategories.length > 0) { + result = result.filter((product) => selectedCategories.includes(product.category.toLowerCase())); + } + + // Filter by color + if (selectedColors.length > 0) { + result = result.filter((product) => product.colors.some((color) => selectedColors.includes(color.toLowerCase()))); + } + + // Filter by brand + if (selectedBrands.length > 0) { + result = result.filter((product) => selectedBrands.includes(product.brand.toLowerCase())); + } + + // Apply sorting + switch (sortOption) { + case "price-low": + result.sort((a, b) => a.price - b.price); + break; + case "price-high": + result.sort((a, b) => b.price - a.price); + break; + case "newest": + result.sort((a, b) => (a.isNew === b.isNew ? 0 : a.isNew ? -1 : 1)); + break; + case "rating": + result.sort((a, b) => b.rating - a.rating); + break; + case "featured": + default: + // Keep original order for featured + break; + } + + setFilteredProducts(result); + }, [priceRange, selectedCategories, selectedColors, selectedBrands, sortOption]); + + const toggleCategory = (categoryId) => { + setSelectedCategories((prev) => + prev.includes(categoryId) ? prev.filter((id) => id !== categoryId) : [...prev, categoryId] + ); + }; + + const toggleColor = (colorId) => { + setSelectedColors((prev) => (prev.includes(colorId) ? prev.filter((id) => id !== colorId) : [...prev, colorId])); + }; + + const toggleBrand = (brandId) => { + setSelectedBrands((prev) => (prev.includes(brandId) ? prev.filter((id) => id !== brandId) : [...prev, brandId])); + }; + + const clearAllFilters = () => { + setPriceRange([0, 200]); + setSelectedCategories([]); + setSelectedColors([]); + setSelectedBrands([]); + setSortOption("featured"); + setColorSelections({}); + }; + + const hasActiveFilters = () => { + return ( + selectedCategories.length > 0 || + selectedColors.length > 0 || + selectedBrands.length > 0 || + priceRange[0] > 0 || + priceRange[1] < 200 + ); + }; + + const handleAddToCart = (product, color) => { + if (!color) { + alert("Please select a color before adding to cart."); + return; + } + addToCart(product, 1, color, "Default"); // Add product with quantity 1, selected color, and default size + }; + + const handleColorSelect = (productId, color) => { + setColorSelections((prev) => ({ + ...prev, + [productId]: color, + })); + }; + + return ( +
+ {/* Page Header */} +
+

All Products

+
+ + Home + + / + Products +
+
+ + {/* Mobile Filter Button */} +
+ + +
+ + +
+ + +
+
+
+ + {/* Mobile Filter Sidebar */} + {isMobileFilterOpen && ( +
+
+
+

Filters

+ +
+ +
+ {/* Mobile Price Range */} +
+

Price Range

+
+ setPriceRange([priceRange[0], Number.parseInt(e.target.value)])} + className="w-full" + /> +
+ ${priceRange[0]} + ${priceRange[1]} +
+
+
+ + {/* Mobile Categories */} +
+

Categories

+
+ {categories.map((category) => ( +
+ toggleCategory(category.id)} + /> + +
+ ))} +
+
+ + {/* Mobile Colors */} +
+

Colors

+
+ {colors.map((color) => ( +
+ toggleColor(color.id)} + /> + +
+ ))} +
+
+ + {/* Mobile Brands */} +
+

Brands

+
+ {brands.map((brand) => ( +
+ toggleBrand(brand.id.toLowerCase())} + /> + +
+ ))} +
+
+ +
+ + +
+
+
+
+ )} + +
+ {/* Desktop Sidebar Filters */} +
+
+ {/* Active Filters */} + {hasActiveFilters() && ( +
+
+

Active Filters

+ +
+
+ {selectedCategories.map((cat) => { + const category = categories.find((c) => c.id === cat); + return category ? ( +
+ {category.label} + toggleCategory(cat)} /> +
+ ) : null; + })} + + {selectedColors.map((col) => { + const color = colors.find((c) => c.id === col); + return color ? ( +
+ {color.label} + toggleColor(col)} /> +
+ ) : null; + })} + + {selectedBrands.map((br) => { + const brand = brands.find((b) => b.id.toLowerCase() === br); + return brand ? ( +
+ {brand.label} + toggleBrand(br)} /> +
+ ) : null; + })} + + {(priceRange[0] > 0 || priceRange[1] < 200) && ( +
+ ${priceRange[0]} - ${priceRange[1]} + setPriceRange([0, 200])} /> +
+ )} +
+
+ )} + + {/* Price Range */} +
+

Price Range

+
+ setPriceRange([priceRange[0], Number.parseInt(e.target.value)])} + className="w-full mb-6" + /> +
+ ${priceRange[0]} + ${priceRange[1]} +
+
+
+ + {/* Categories */} +
+

Categories

+
+ {categories.map((category) => ( +
+ toggleCategory(category.id)} + /> + +
+ ))} +
+
+ + {/* Colors */} +
+

Colors

+
+ {colors.map((color) => ( +
+ toggleColor(color.id)} + /> + +
+ ))} +
+
+ + {/* Brands */} +
+

Brands

+
+ {brands.map((brand) => ( +
+ toggleBrand(brand.id.toLowerCase())} + /> + +
+ ))} +
+
+
+
+ + {/* Main Content */} +
+ {/* Desktop Sort and View Controls */} +
+
+ + {filteredProducts.length} products + {hasActiveFilters() && ( + + )} +
+ +
+ + +
+ + +
+
+
+ + {/* Products Grid/List */} + {filteredProducts.length === 0 ? ( +
+
+ +
+

No products found

+

Try adjusting your filters or search criteria

+ +
+ ) : viewMode === "grid" ? ( +
+ {filteredProducts.map((product) => ( +
+
+ + {product.name} + + + {/* Product badges */} +
+ {product.isNew && ( + New + )} + {product.isSale && ( + Sale + )} +
+ + {/* Quick actions */} +
+ +
+
+ +
+ +

+ {product.name} +

+ + + {/* Rating */} +
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ ({product.reviewCount}) +
+ + {/* Color Selection */} +
+ {product.colors.map((color) => ( +
+ + {/* Price and Add to Cart */} +
+
+ ${product.price.toFixed(2)} + {product.originalPrice && ( + + ${product.originalPrice.toFixed(2)} + + )} +
+ +
+
+
+ ))} +
+ ) : ( +
+ {filteredProducts.map((product) => ( +
+
+ + {product.name} + + + {/* Product badges */} +
+ {product.isNew && ( + New + )} + {product.isSale && ( + Sale + )} +
+
+ +
+ +

+ {product.name} +

+ + + {/* Rating */} +
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ ({product.reviewCount}) +
+ +

+ {product.category} β€’ {product.brand} β€’ {product.colors.join(", ")} +

+ + {/* Color Selection */} +
+ {product.colors.map((color) => ( +
+ + {/* Price and Actions */} +
+
+ ${product.price.toFixed(2)} + {product.originalPrice && ( + + ${product.originalPrice.toFixed(2)} + + )} +
+
+ + +
+
+
+
+ ))} +
+ )} + + {/* Pagination */} +
+
+ + + + + +
+
+
+
+
+ ); +}; + +export default ProductsPage; \ No newline at end of file diff --git a/src/client/pages/WishlistPage.jsx b/src/client/pages/WishlistPage.jsx new file mode 100644 index 00000000..4f3224d0 --- /dev/null +++ b/src/client/pages/WishlistPage.jsx @@ -0,0 +1,210 @@ +// src/pages/WishlistPage.jsx +"use client"; + +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { Heart, ShoppingCart, X, Trash2 } from "lucide-react"; +import { useCart } from "../context/CartContext"; +import { useWishlist } from "../context/WishlistContext"; +import { toast } from "react-toastify"; + +const WishlistPage = () => { + const { addToCart } = useCart(); + const { wishlistItems, removeFromWishlist, clearWishlist } = useWishlist(); + const [selectedItems, setSelectedItems] = useState([]); + + const toggleSelectItem = (id) => { + setSelectedItems((prev) => + prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id] + ); + }; + + const selectAll = () => { + if (selectedItems.length === wishlistItems.length) { + setSelectedItems([]); + } else { + setSelectedItems(wishlistItems.map((item) => item.id)); + } + }; + + const removeSelected = () => { + selectedItems.forEach((id) => removeFromWishlist(id)); + setSelectedItems([]); + toast.info(`${selectedItems.length} item(s) removed from wishlist.`); + }; + + const handleAddToCart = (item) => { + addToCart( + { + id: item.id, + name: item.name, + price: item.price, + image: item.image, + slug: item.slug, + }, + 1, + item.color, + item.size + ); + toast.success(`${item.name} added to cart!`); + }; + + return ( +
+

My Wishlist

+ + {wishlistItems.length === 0 ? ( +
+
+ +
+

Your wishlist is empty

+

Save items you love to your wishlist and review them anytime.

+ + Start Shopping + +
+ ) : ( + <> +
+
+ 0} + onChange={selectAll} + /> + +
+ +
+ {selectedItems.length > 0 && ( + + )} + +
+
+ +
+
+
+ {wishlistItems.map((item) => ( +
+
+ toggleSelectItem(item.id)} + /> +
+ +
+ {item.name} +
+ +
+
+
+ + {item.name} + +
+ Color: {item.color} + β€’ + Size: {item.size} +
+
+ ${item.price.toFixed(2)} + {item.originalPrice && ( + + ${item.originalPrice.toFixed(2)} + + )} +
+
+ {item.inStock ? ( + In Stock + ) : ( + Out of Stock + )} +
+
+ +
+ + + +
+
+
+
+ ))} +
+
+
+ +
+ + Continue Shopping + + + + View Cart + +
+ + )} +
+ ); +}; + +export default WishlistPage; \ No newline at end of file diff --git a/src/client/pages/tsconfig.json b/src/client/pages/tsconfig.json new file mode 100644 index 00000000..f3228d54 --- /dev/null +++ b/src/client/pages/tsconfig.json @@ -0,0 +1,113 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "libReplacement": true, /* Enable lib replacement. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/src/client/reportWebVitals.js b/src/client/reportWebVitals.js new file mode 100644 index 00000000..5253d3ad --- /dev/null +++ b/src/client/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/src/client/setupTests.js b/src/client/setupTests.js new file mode 100644 index 00000000..8f2609b7 --- /dev/null +++ b/src/client/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; From ee9fe5b912568b4da20291a42cfea5d4dfedc725 Mon Sep 17 00:00:00 2001 From: Albin190 Date: Sun, 20 Apr 2025 12:40:30 -0700 Subject: [PATCH 2/2] ADD BACKEND FILE USE NODE EXPRESS AND MONGODB OR BLOCKCHAIN MAKE LOGIN AND SIGNUP SYSTEM IN ECOMMERCE WEBSITE --- src/client/backend/modules.user.js | 32 ++++++++++++++ src/client/backend/routes/auth.js | 70 ++++++++++++++++++++++++++++++ src/client/backend/server.js | 46 ++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 src/client/backend/modules.user.js create mode 100644 src/client/backend/routes/auth.js create mode 100644 src/client/backend/server.js diff --git a/src/client/backend/modules.user.js b/src/client/backend/modules.user.js new file mode 100644 index 00000000..d3eda968 --- /dev/null +++ b/src/client/backend/modules.user.js @@ -0,0 +1,32 @@ +const mongoose = require('mongoose'); + +const userSchema = new mongoose.Schema({ + username: { + type: String, + required: true, + trim: true, + minlength: 3 + }, + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true + }, + password: { + type: String, + required: true, + minlength: 8 + }, + isAdmin: { + type: Boolean, + default: false + } +}, { + timestamps: true, + collection: 'signup' +}); + +const User = mongoose.model('User', userSchema); +module.exports = User; \ No newline at end of file diff --git a/src/client/backend/routes/auth.js b/src/client/backend/routes/auth.js new file mode 100644 index 00000000..6e2f42bb --- /dev/null +++ b/src/client/backend/routes/auth.js @@ -0,0 +1,70 @@ +const express = require('express'); +const router = express.Router(); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const User = require('../modules.user'); + +router.post('/signup', async (req, res) => { + const { username, email, password } = req.body; + console.log('Signup request:', { username, email, password: '****' }); + try { + if (!username || !email || !password) { + return res.status(400).json({ message: 'All fields are required' }); + } + if (password.length < 8) { + return res.status(400).json({ message: 'Password must be at least 8 characters' }); + } + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res.status(400).json({ message: 'Email already exists' }); + } + const hashedPassword = await bcrypt.hash(password, 10); + const user = new User({ + username, + email, + password: hashedPassword, + }); + await user.save(); + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET || 'your-secret-key', { + expiresIn: '1h', + }); + res.status(201).json({ + token, + user: { id: user._id, username, email, isAdmin: user.isAdmin }, + }); + } catch (error) { + console.error('Signup error:', error.message); + res.status(500).json({ message: 'Server error', error: error.message }); + } +}); + +router.post('/login', async (req, res) => { + const { email, password, rememberMe } = req.body; + console.log('Login request:', { email, password: '****', rememberMe }); + try { + if (!email || !password) { + return res.status(400).json({ message: 'Email and password are required' }); + } + const user = await User.findOne({ email }); + if (!user) { + return res.status(400).json({ message: 'Invalid email or password' }); + } + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + return res.status(400).json({ message: 'Invalid email or password' }); + } + const expiresIn = rememberMe ? '7d' : '1h'; + const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET || 'your-secret-key', { + expiresIn, + }); + res.json({ + token, + user: { id: user._id, username: user.username, email, isAdmin: user.isAdmin }, + }); + } catch (error) { + console.error('Login error:', error.message); + res.status(500).json({ message: 'Server error', error: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/src/client/backend/server.js b/src/client/backend/server.js new file mode 100644 index 00000000..a44e54ed --- /dev/null +++ b/src/client/backend/server.js @@ -0,0 +1,46 @@ +const express = require('express'); +const mongoose = require('mongoose'); +const cors = require('cors'); +const dotenv = require('dotenv'); +const rateLimit = require('express-rate-limit'); +const authRoutes = require('./routes/auth'); + +dotenv.config(); +const app = express(); + +// Update CORS to allow frontend port (e.g., 5173 or 3000) +app.use(cors({ origin: ['http://localhost:3000', 'http://localhost:5173'], credentials: true })); +app.use(express.json()); + +app.use((req, res, next) => { + console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); + next(); +}); + +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 5, + message: { message: 'Too many login attempts, please try again after 15 minutes' }, +}); +app.use('/api/auth/login', loginLimiter); + +app.use('/api/auth', authRoutes); + +app.get('/', (req, res) => { + res.send('Server is running...'); +}); + +mongoose.connect(process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/auth', { + useNewUrlParser: true, + useUnifiedTopology: true, +}) + .then(() => console.log('βœ… MongoDB Connected to auth database')) + .catch((error) => { + console.error('❌ MongoDB Connection Error:', error.message); + process.exit(1); + }); + +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => { + console.log(`πŸš€ Server is running on http://localhost:${PORT}`); +}); \ No newline at end of file