├── src ├── index.css ├── images │ ├── news.jpg │ ├── shopping2.jpg │ ├── shopping3.jpg │ ├── shopping4.jpg │ └── viva-img.jpg ├── api │ ├── order.js │ └── product.js ├── main.jsx ├── components │ ├── Footer.css │ ├── Footer.jsx │ ├── NewsSearch.css │ ├── Form.css │ ├── Form.jsx │ ├── Nav.jsx │ ├── NewsSearchBar.jsx │ ├── NewsDisplay.jsx │ └── ProductList.jsx ├── App.css ├── pages │ ├── Product.jsx │ ├── SearchNews.jsx │ ├── Admin │ │ ├── ViewProduct.jsx │ │ └── AddOrEditProduct.jsx │ ├── Cart.css │ └── Cart.jsx ├── App.jsx └── assets │ └── react.svg ├── .gitignore ├── vite.config.js ├── index.html ├── package.json ├── eslint.config.js ├── public └── vite.svg └── README.md /src/index.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss" -------------------------------------------------------------------------------- /src/images/news.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sri91524/capstoneproject-frontend/HEAD/src/images/news.jpg -------------------------------------------------------------------------------- /src/images/shopping2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sri91524/capstoneproject-frontend/HEAD/src/images/shopping2.jpg -------------------------------------------------------------------------------- /src/images/shopping3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sri91524/capstoneproject-frontend/HEAD/src/images/shopping3.jpg -------------------------------------------------------------------------------- /src/images/shopping4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sri91524/capstoneproject-frontend/HEAD/src/images/shopping4.jpg -------------------------------------------------------------------------------- /src/images/viva-img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sri91524/capstoneproject-frontend/HEAD/src/images/viva-img.jpg -------------------------------------------------------------------------------- /src/api/order.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const BASE_URL=import.meta.env.VITE_API_BASE_URL 3 | 4 | //Post Order 5 | export async function createOrder(){ 6 | const res = await axios.post(`${BASE_URL}/api/product`,{products}); 7 | return res.data; 8 | } -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.jsx' 5 | import { BrowserRouter} from 'react-router-dom'; 6 | 7 | createRoot(document.getElementById('root')).render( 8 | 9 | 10 | 11 | 12 | , 13 | ) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env 26 | 27 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from "@tailwindcss/vite"; 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), tailwindcss()], 8 | server: { 9 | proxy: { 10 | "/api": { 11 | target: "http://localhost:4000", 12 | changeOrigin: true, 13 | }, 14 | }, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/Footer.css: -------------------------------------------------------------------------------- 1 | footer{ 2 | display:flex; 3 | justify-content: center; 4 | align-items: center; 5 | text-align: center; 6 | background-color: black; 7 | color: white; 8 | gap: 30px; 9 | font-size: 1em; 10 | padding: 5px; 11 | margin-bottom: 10px; 12 | } 13 | 14 | footer a{ 15 | color:white; 16 | text-decoration: none; 17 | } 18 | 19 | footer a:hover{ 20 | text-decoration: underline; 21 | } -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import '../components/Footer.css' 3 | 4 | function Footer(){ 5 | 6 | return( 7 | 14 | ) 15 | } 16 | 17 | export default Footer; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Viva Fashions 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@heroicons/react": "^1.0.6", 14 | "@tailwindcss/vite": "^4.0.15", 15 | "axios": "^1.8.4", 16 | "react": "^19.0.0", 17 | "react-dom": "^19.0.0", 18 | "react-icons": "^5.5.0", 19 | "react-router-dom": "^7.4.0", 20 | "tailwindcss": "^4.0.15" 21 | }, 22 | "devDependencies": { 23 | "@eslint/js": "^9.21.0", 24 | "@types/react": "^19.0.10", 25 | "@types/react-dom": "^19.0.4", 26 | "@vitejs/plugin-react": "^4.3.4", 27 | "eslint": "^9.21.0", 28 | "eslint-plugin-react-hooks": "^5.1.0", 29 | "eslint-plugin-react-refresh": "^0.4.19", 30 | "globals": "^15.15.0", 31 | "vite": "^6.2.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | 6 | export default [ 7 | { ignores: ['dist'] }, 8 | { 9 | files: ['**/*.{js,jsx}'], 10 | languageOptions: { 11 | ecmaVersion: 2020, 12 | globals: globals.browser, 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | ecmaFeatures: { jsx: true }, 16 | sourceType: 'module', 17 | }, 18 | }, 19 | plugins: { 20 | 'react-hooks': reactHooks, 21 | 'react-refresh': reactRefresh, 22 | }, 23 | rules: { 24 | ...js.configs.recommended.rules, 25 | ...reactHooks.configs.recommended.rules, 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | 'react-refresh/only-export-components': [ 28 | 'warn', 29 | { allowConstantExport: true }, 30 | ], 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /src/api/product.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | const BASE_URL=import.meta.env.VITE_API_BASE_URL 3 | 4 | //create new product 5 | export async function createProduct(product){ 6 | try{ 7 | const res = await axios.post(`${BASE_URL}/api/product`,product); 8 | return res.data; 9 | }catch(error){ 10 | console.log(`api error -${error}`); 11 | } 12 | } 13 | 14 | //Get all products 15 | export async function getAllProducts(){ 16 | const res = await axios.get(`${BASE_URL}/api/product`); 17 | return res.data; 18 | } 19 | 20 | //Get a specific product 21 | export async function getProduct(prodId){ 22 | const res = await axios.get(`${BASE_URL}/api/product/${prodId}`); 23 | return res.data; 24 | } 25 | 26 | //update a product 27 | export async function updateProduct(prodId, product){ 28 | const res = await axios.patch(`${BASE_URL}/api/product/${prodId}`,product); 29 | return res.data; 30 | } 31 | 32 | //Delete a product 33 | export async function deleteProduct(prodId){ 34 | await axios.delete(`${BASE_URL}/api/product/${prodId}`); 35 | } -------------------------------------------------------------------------------- /src/components/NewsSearch.css: -------------------------------------------------------------------------------- 1 | .search-container { 2 | display: flex; 3 | justify-content: flex-end; /* Aligns the form to the right */ 4 | padding-right: 20px; /* Optional: Add some padding to the right */ 5 | } 6 | 7 | .form-control { 8 | height: 20px; /* Adjust height of form controls */ 9 | padding: 0 6px; /* Adjust padding to maintain the reduced height */ 10 | font-size: 12px; /* Adjust font size for consistency */ 11 | line-height: 24px; /* Vertically center text inside the controls */ 12 | } 13 | 14 | .search-content { 15 | display: flex; 16 | gap: 16px; /* Add space between controls */ 17 | align-items: center; 18 | } 19 | 20 | input[type="text"], select { 21 | height: 32px; /* Ensure controls have the same height */ 22 | padding: 0 12px; /* Adjust padding */ 23 | font-size: 14px; /* Consistent font size */ 24 | } 25 | 26 | input[type="submit"] { 27 | height: 32px; 28 | padding: 0 12px; 29 | font-size: 14px; 30 | cursor: pointer; 31 | background-color: rgb(212, 175, 55); /* Button color */ 32 | border: none; 33 | border-radius: 4px; 34 | } -------------------------------------------------------------------------------- /src/components/Form.css: -------------------------------------------------------------------------------- 1 | .form-container { 2 | display: flex; 3 | justify-content: flex-end; /* Aligns the form to the right */ 4 | padding-right: 20px; /* Optional: Add some padding to the right */ 5 | } 6 | 7 | .form-content { 8 | display: flex; 9 | align-items: center; /* Vertically aligns the items in the form */ 10 | gap: 10px; /* Adds spacing between the form elements */ 11 | flex-wrap: nowrap; /* Prevents wrapping of form controls */ 12 | } 13 | 14 | .form-control { 15 | height: 20px; /* Reduced height for form controls */ 16 | padding: 0 6px; /* Reduced padding to maintain the reduced height */ 17 | font-size: 12px; /* Adjust font size for consistency */ 18 | line-height: 24px; /* Vertically center text inside the controls */ 19 | } 20 | 21 | input[type="submit"] { 22 | height: 24px; /* Ensure the button has the same height as form controls */ 23 | padding: 0 6px; /* Padding for the button */ 24 | cursor: pointer; 25 | font-size: 12px; /* Same font size for consistency */ 26 | line-height: 24px; /* Ensure text is vertically aligned inside the button */ 27 | display: inline-flex; /* Ensures the button stays inline with other items */ 28 | align-items: center; /* Vertically center the button text */ 29 | background-color: rgb(212, 175, 55); /* Button color */ 30 | border: none; /* Optional: Remove button border */ 31 | border-radius: 4px; /* Optional: Add rounded corners */ 32 | } -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Form.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import './Form.css' 3 | 4 | function Form(props){ 5 | 6 | const [formData, setFormData] = useState({category:"",searchterm:""}); 7 | //onchanging form, form data is set 8 | const handleChange = (e) =>{ 9 | setFormData({...formData, [e.target.name]:e.target.value}); 10 | } 11 | //category and search term will be passed as props to product.jsx page 12 | const handleSubmit = (e) =>{ 13 | e.preventDefault(); 14 | props.productsearch(formData.category,formData.searchterm); 15 | } 16 | 17 | 18 | return( 19 |
20 |
21 | 32 | 33 | 40 | 41 | 46 |
47 |
48 | ) 49 | }; 50 | 51 | export default Form; -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .nav{ 4 | display: flex; 5 | justify-content:right; 6 | align-items: center; 7 | text-align: center; 8 | background-color: black; 9 | color:white; 10 | gap:30px; 11 | font-size: 1em; 12 | 13 | padding: 5px; 14 | margin-bottom: 10px; 15 | } 16 | 17 | .headertext{ 18 | color:rgb(212, 175, 55); 19 | text-align: left; 20 | align-items: left; 21 | font-weight: bold; 22 | margin: 0; 23 | font-size: 24px; 24 | flex: 1; 25 | } 26 | .cartcountbadge{ 27 | position: absolute; 28 | top: -2px; 29 | right: 110px; 30 | background-color: rgb(212, 175, 55); 31 | color: white; 32 | border-radius: 50%; 33 | width: 20px; 34 | height: 20px; 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | font-size: 12px; 39 | margin-right: 30px; 40 | } 41 | 42 | 43 | .img-container{ 44 | display: flex; 45 | flex-wrap: wrap; 46 | gap:20px; 47 | justify-content:flex-start; 48 | flex-direction: row; 49 | padding: 20px; 50 | margin:10px; 51 | } 52 | 53 | .title{ 54 | font-family: 'Roboto', sans-serif; 55 | font-size: 0.8rem; 56 | text-align: center; 57 | align-items: center; 58 | } 59 | 60 | .hover-image { 61 | transition: transform 0.5s ease; /* Smooth transition for the transform property */ 62 | } 63 | 64 | .hover-image:hover { 65 | transform: scale(1.1) !important; /* Slightly scale up the image on hover */ 66 | } 67 | 68 | .full-width-image img { 69 | width: 100%; /* Ensures the image spans the full width */ 70 | height: 50px; /* Fixed height of the image */ 71 | object-fit:contain; /* Ensures the image fills the space without distorting */ 72 | border-radius: 50%; 73 | } -------------------------------------------------------------------------------- /src/pages/Product.jsx: -------------------------------------------------------------------------------- 1 | import Form from '../components/Form'; 2 | import ProductList from '../components/ProductList'; 3 | import Cart from './Cart' 4 | import { useState,useEffect } from 'react'; 5 | const BASE_URL=import.meta.env.VITE_API_BASE_URL 6 | 7 | function Product({addToCart}){ 8 | 9 | //to store products in state 10 | const [productResult, setProductResult] = useState([]); 11 | //To set cart items in state 12 | const [cart, setCart] = useState([]); 13 | //to toggle cart visibility 14 | const[isCartOpen, setIsCartOpen] = useState(false); 15 | 16 | // to get list of products from db based on category and search filter 17 | const getProducts = async(prodcategory, prodsearch) =>{ 18 | try{ 19 | const response = await fetch(`${BASE_URL}/api/product?category=${prodcategory}&search=${prodsearch}`); 20 | const data = await response.json(); 21 | 22 | if (!response.ok) { 23 | throw new Error(`HTTP error! Status: ${response.status}`); 24 | } 25 | console.log(data); 26 | 27 | setProductResult(data); 28 | 29 | }catch(e){ 30 | console.error(e); 31 | } 32 | } 33 | //Initial load of products without filter 34 | useEffect(() =>{ 35 | getProducts('',''); 36 | },[]) 37 | 38 | 39 | 40 | return ( 41 | <> 42 |
43 |
44 | { 45 | productResult && productResult.length > 0? 46 | productResult.map(product => 47 | ): //Passing addToCart function to ProductList 48 | ( 49 |

No matching products found for your selection.

50 | ) 51 | } 52 |
53 | 54 | 55 | ) 56 | } 57 | 58 | export default Product; -------------------------------------------------------------------------------- /src/components/Nav.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import {Link} from 'react-router-dom' 3 | import Cart from '../pages/Cart' 4 | import '../App.css' 5 | import bannerImage from '../images/viva-img.jpg' 6 | 7 | function Nav({ cart, setCart }){ 8 | 9 | const [isCartOpen, setIsCartOpen] = useState(false); 10 | 11 | //on close button click, isCartOpen is set to false 12 | //on menu cart click, isCartOpen is set to true 13 | const toggleCart = () =>{ 14 | setIsCartOpen(!isCartOpen); 15 | } 16 | return( 17 | 18 |
19 | 20 |
21 |
22 | bannerImage 23 |
24 |

VIVA Fashions

25 | 26 |
Home
27 | 28 | 29 |
Manage Product
30 | 31 |
32 |
Cart
33 | {cart.length > 0 && ( 34 | {cart.length} 35 | )} 36 |
37 | {isCartOpen && 38 |
39 | 40 | 41 |
42 | } 43 | 44 |
News Article
45 | 46 |
47 |
48 | ) 49 | 50 | } 51 | 52 | export default Nav; -------------------------------------------------------------------------------- /src/components/NewsSearchBar.jsx: -------------------------------------------------------------------------------- 1 | import {useState} from 'react'; 2 | import './NewsSearch.css'; 3 | 4 | function NewsSearchBar(props){ 5 | 6 | const [searchData, setSearchData] = useState({category:"",searchterm:""}); 7 | 8 | const handleChange = (e) =>{ 9 | setSearchData({...searchData, [e.target.name]:e.target.value}); 10 | } 11 | 12 | const handleSubmit = (e) =>{ 13 | e.preventDefault(); 14 | props.newssearch(searchData.category, searchData.searchterm); 15 | } 16 | 17 | return( 18 |
19 | 20 | 36 | 37 | 39 | 40 |
41 | ) 42 | } 43 | export default NewsSearchBar; -------------------------------------------------------------------------------- /src/pages/SearchNews.jsx: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react' 2 | import NewsSearchBar from '../components/NewsSearchBar'; 3 | import NewsDisplay from '../components/NewsDisplay'; 4 | import globalNewsImage from '../images/news.jpg'; 5 | 6 | function SearchNews(){ 7 | const apiKey = import.meta.env.VITE_NEWS_API_KEY; 8 | const [news, setNews] = useState(null); 9 | 10 | const getNewsArticle = async(newscategory, searchterm) =>{ 11 | try{ 12 | let url= `https://api.thenewsapi.com/v1/news/all?api_token=${apiKey}&search=${searchterm}&language=en&categories=${newscategory}`; 13 | 14 | const response = await fetch(url); 15 | const data = await response.json(); 16 | setNews(data.data); 17 | console.log(news); 18 | }catch(e){ 19 | console.error(e); 20 | } 21 | }; 22 | 23 | useEffect(() =>{ 24 | getNewsArticle('sports',''); 25 | },[]) 26 | return( 27 | <> 28 | Global News 29 | 30 |
31 | { 32 | news && news.length >0? 33 | news.map((news) => 34 | ): 35 | ( 36 |
37 |

38 | Cannot find related articles for your search!! 39 |

40 |
41 | ) 42 | } 43 |
44 | 45 | ) 46 | } 47 | export default SearchNews; -------------------------------------------------------------------------------- /src/components/NewsDisplay.jsx: -------------------------------------------------------------------------------- 1 | function NewsDisplay({news}) { 2 | 3 | const formattedPubDate = new Date(news.published_at).toLocaleDateString("en-CA"); 4 | 5 | const loaded = () => { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 |

13 |

{news.title}

14 |

Published date: {formattedPubDate}

15 | 16 |

17 |
18 | {news.source} 23 |

{news.description}

24 | Read More.. 25 | 26 |
27 |
28 |
29 |
30 | ); 31 | }; 32 | 33 | const loading = () => { 34 | return ( 35 |

Loading....

36 | ) 37 | }; 38 | 39 | return news ? loaded() : loading(); 40 | } 41 | 42 | export default NewsDisplay; -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import {Route, Routes} from 'react-router-dom'; 2 | import {useState, useEffect} from 'react' 3 | import './App.css' 4 | 5 | import Cart from './pages/Cart' 6 | import Product from './pages/Product' 7 | import ViewProduct from './pages/Admin/ViewProduct'; 8 | import AddOrEditProduct from './pages/Admin/AddOrEditProduct'; 9 | import SearchNews from './pages/SearchNews'; 10 | import Nav from './components/Nav' 11 | import Footer from './components/Footer' 12 | 13 | function App() { 14 | const [cart, setCart] = useState(() => { 15 | // Initialize cart from localStorage when app loads 16 | const savedCart = localStorage.getItem('cart'); 17 | return savedCart ? JSON.parse(savedCart) : []; 18 | }); 19 | 20 | // Save cart to localStorage whenever it changes 21 | useEffect(() => { 22 | if (cart.length > 0) { 23 | localStorage.setItem('cart', JSON.stringify(cart)); 24 | } 25 | }, [cart]); 26 | 27 | //In order to retrieve cart in nav bar on click on cart menu 28 | //it is passed from app component to nav & product 29 | const addToCart = (product) =>{ 30 | //check if item already exist in cart 31 | let itemExist = cart.some(item => item.prodname === product.prodname); 32 | 33 | //if item not exist, then add product to cart 34 | if(!itemExist){ 35 | setCart(prevCart => [...prevCart,product]); 36 | } 37 | else{ 38 | alert("Item already exist in the cart"); 39 | } 40 | } 41 | 42 | return( 43 |
44 |
56 | ) 57 | } 58 | 59 | export default App; 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛍️ Online Shopping App - Capstone Project 2 | 3 | [![GitHub Repo stars](https://img.shields.io/github/stars/sri91524/capstoneproject-frontend)](https://github.com/sri91524/capstoneproject-frontend) 4 | 5 | An online shopping application built as a capstone project for the RTT-60-2024 Software Engineering class. This responsive web app allows users to browse, search, manage, and shop for products — complete with cart functionality and backend CRUD operations. 6 | 7 | --- 8 | 9 | ## 🚀 Live Demo 10 | 11 | 👉 [Visit the Live Site](https://sri-react-onlineshopping.netlify.app/) 12 | 13 | --- 14 | 15 | ## 🛠️ Technologies Used 16 | 17 | - **Frontend:** React + Vite, TailwindCSS, Heroicons 18 | - **API & Data:** Axios, MongoDB, Mongoose 19 | - **Hooks:** `useState`, `useEffect` 20 | - **Tools:** NewsAPI (with `.env` for API key security) 21 | 22 | --- 23 | 24 | ## 📦 Features & Functionality 25 | 26 | ### 🔍 Browsing & Search 27 | - Search products by **category** or **keyword** 28 | - Results update dynamically using API queries 29 | 30 | ### 🖼️ Product Display 31 | - Clean product listing with details 32 | - **Add to Cart** functionality from product card 33 | 34 | ### 🛒 Shopping Cart 35 | - Add / remove / delete items from cart 36 | - Cart updates quantity, price, and total dynamically 37 | - Item count visible in the navbar 38 | 39 | ### ✏️ Product Management (Admin) 40 | - Add, edit, or delete products 41 | - Form-based interface for input 42 | - Data stored in MongoDB via Mongoose 43 | 44 | ### 📰 News API Integration 45 | - News articles fetched using [TheNewsAPI](https://www.thenewsapi.com/) 46 | - Category/keyword-based search 47 | - Displayed in a separate section 48 | 49 | --- 50 | 51 | ## 🧠 Architecture & Approach 52 | 53 | - **State Management:** 54 | `useState` combined with `localStorage` to persist cart data across pages 55 | 56 | - **Component Flow:** 57 | State initialized in `App.js` and passed through components: 58 | `App` → `ProductPage` → `ProductList` → `Cart` → `Nav` 59 | 60 | - **Conditional Rendering:** 61 | Based on `isCartOpen` state, the cart overlay is toggled on/off 62 | 63 | - **API Interaction:** 64 | - Axios used for HTTP requests 65 | - Custom interface for CRUD operations 66 | - NewsAPI integrated securely via `.env` 67 | 68 | - **Error Handling:** 69 | - Robust `try...catch` blocks for API calls 70 | - User feedback for invalid entries or API failures 71 | 72 | --- -------------------------------------------------------------------------------- /src/components/ProductList.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | function ProductList({product, addToCart}){ 4 | 5 | const handleAddToCart = () =>{ 6 | addToCart(product); 7 | } 8 | 9 | const loaded = () =>{ 10 | //As cart state need to be maintained in parent component 11 | //addToCart functionality added in Product page and passed 12 | //prop addToCart to ProductList 13 | //Then useState cart will be passed to cart component 14 | 15 | return( 16 |
17 |
18 |
19 | product 21 |
22 |
23 |
24 |

25 | {product.prodname} 26 |

27 |

28 | {`Size - ${product.size}`} 29 |

30 |

31 | { 32 | product.price && product.price["$numberDecimal"] 33 | ? `$${product.price["$numberDecimal"]}` 34 | : ""} 35 |

36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 | ) 44 | } 45 | 46 | const loading = () => { 47 | return

Loading...

48 | } 49 | 50 | return product ? loaded() : loading(); 51 | } 52 | 53 | export default ProductList; -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/Admin/ViewProduct.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import {Link, useNavigate} from 'react-router-dom'; 3 | import {createProduct, 4 | getAllProducts, 5 | deleteProduct 6 | } from '../../../src/api/product' 7 | import { PencilIcon, TrashIcon } from '@heroicons/react/solid'; 8 | 9 | function ViewProduct(){ 10 | const[products, setProducts] = useState([]); 11 | const [message, setMessage] = useState(null); 12 | 13 | const navigate = useNavigate(); 14 | 15 | //Fetch all products from database thro' interface product.js in src/api 16 | useEffect(() =>{ 17 | const getAllProd = async() =>{ 18 | try{ 19 | const allProd = await getAllProducts(); 20 | setProducts(allProd); 21 | } catch(error){ 22 | console.error("Failed to get products:", error); 23 | } 24 | } 25 | getAllProd(); 26 | },[]); 27 | 28 | const handleEdit = (product) =>{ 29 | navigate(`/admin/addoreditproduct/${product._id}`); 30 | } 31 | 32 | //delete product from admin console 33 | const handleDelete = async(productId) => { 34 | 35 | const isConfirmed = window.confirm('Are you sure want to delete this product?'); 36 | if(!isConfirmed) return; 37 | try{ 38 | await deleteProduct(productId); 39 | setProducts(prevProducts => prevProducts.filter(product => product._id !== productId)); 40 | setMessage({type:'success', text:'Product deleted successfully'}); 41 | 42 | }catch(error){ 43 | console.error("Failed to delete product:", error); 44 | setMessage({type: 'error', text: 'Failed to delete product. Please try again.'}) 45 | } 46 | } 47 | 48 | return( 49 | <> 50 |
51 |

Admin Product DashBoard

52 |
53 |
54 | 55 | 56 | 57 |
58 |
59 | {/*show success or error message*/} 60 | {message && 61 | ( 62 |
63 | {message.text} 64 |
65 | ) 66 | } 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {products.map((product, index) =>( 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 100 | 101 | )) 102 | } 103 | 104 | 105 |
ImageProduct NameProduct DescCategorySizePriceActions
84 | {product.name} 85 | {product.prodname}{product.proddesc}{product.category.charAt(0).toUpperCase() + product.category.slice(1)}{product.size}${product.price["$numberDecimal"]} 92 | 93 |    96 | 99 |
106 |
107 | 108 | 109 | 110 | ) 111 | } 112 | 113 | export default ViewProduct; -------------------------------------------------------------------------------- /src/pages/Cart.css: -------------------------------------------------------------------------------- 1 | /* Close button */ 2 | .close-cart { 3 | position: absolute; /* Make it absolutely positioned inside the cart container */ 4 | top: 10px; /* Position it at the top */ 5 | right: 10px; /* Position it at the right */ 6 | background-color: black; 7 | color: white; 8 | border: none; 9 | border-radius: 50%; 10 | padding: 5px 10px; 11 | cursor: pointer; 12 | font-size: 16px; 13 | z-index: 101; /* Ensure it appears above other elements */ 14 | } 15 | .close-cart:hover { 16 | background-color: #d4af37; 17 | } 18 | 19 | .cart-container{ 20 | position: fixed; 21 | top:0; 22 | right:0; 23 | width:400px; 24 | height:80%; 25 | padding-bottom: 120px; /* Add padding for the fixed summary at the bottom */ 26 | background-color:#f4f4f4; 27 | box-shadow: -2px 0 2px rgba(0,0,0,0.5); 28 | transition: right 0.3s ease; 29 | overflow-y: auto; 30 | padding:20px; 31 | box-sizing: border-box; 32 | z-index:100; 33 | } 34 | 35 | .cart-item{ 36 | display:flex; 37 | justify-content: space-between; 38 | margin-bottom: 15px; 39 | padding:10px; 40 | background-color: #fff; 41 | flex-grow: 1; 42 | } 43 | 44 | .cart-container ul{ 45 | list-style: none; 46 | padding:0; 47 | } 48 | 49 | .cart-container li{ 50 | border-bottom: 1px solid #ccc; 51 | padding: 10px 0; 52 | } 53 | .cart-container h2 { 54 | color: black; /* Set text color to black for the title */ 55 | } 56 | .cart-container h3{ 57 | margin:0; 58 | font-size:18px; 59 | color:black; 60 | } 61 | 62 | .cart-container p{ 63 | margin: 5px 0; 64 | color:black; 65 | } 66 | 67 | .cart-item{ 68 | display:flex; 69 | justify-content: space-between; 70 | margin-bottom:15px; 71 | background-color: #fff; 72 | padding: 10px; 73 | border-radius:8px; 74 | box-shadow:0 2px 5px rgba(0,0,0,0.1); 75 | } 76 | 77 | .cart-item-image-container { 78 | width: 25%; /* Set the width of the container holding the image to 25% */ 79 | display: flex; /* Flexbox to align the image */ 80 | justify-content: center; /* Center the image within its container */ 81 | } 82 | 83 | .cart-item-image{ 84 | width: 80px; 85 | height: 80px; 86 | border-radius:5px; 87 | } 88 | 89 | .cart-item-details-container { 90 | width: 75%; /* Set the width of the container holding the details to 75% */ 91 | padding-left: 15px; /* Optional: Add some space between image and details */ 92 | } 93 | 94 | .cart-item-details{ 95 | flex-grow:1; 96 | margin-left:15px; 97 | } 98 | 99 | .cart-item-title{ 100 | font-size:18px; 101 | font-weight: bold; 102 | } 103 | 104 | .cart-item-size, 105 | .cart-item-price{ 106 | font-size:14px; 107 | } 108 | 109 | .cart-item-quantity{ 110 | display:flex; 111 | align-items: center; 112 | gap:10px; 113 | margin-top: 10px; 114 | } 115 | 116 | 117 | .delete-btn:hover { 118 | background-color: #d4af37; 119 | } 120 | 121 | .quantity-btn { 122 | padding: 5px 10px; 123 | font-size: 8px; 124 | cursor: pointer; 125 | background-color: #333; /* Dark background */ 126 | color: white; /* White text */ 127 | border: none; 128 | border-radius: 5px; 129 | transition: background-color 0.3s ease; 130 | } 131 | 132 | .quantity-btn:hover { 133 | background-color: #d4af37; /* Hover effect */ 134 | } 135 | 136 | .quantity-input { 137 | text-align: center; 138 | font-size: 9px; 139 | padding: 4px; 140 | border: 1px solid #ccc; 141 | border-radius: 5px; 142 | color:black; 143 | } 144 | 145 | .cart-item button { 146 | margin-left: 10px; /* Add margin to the delete button for separation */ 147 | } 148 | 149 | .carttitle{ 150 | font-family: 'Roboto', sans-serif; 151 | font-size: 0.7rem; 152 | text-align: left; 153 | } 154 | 155 | .cart-item .cartprice { 156 | font-family: 'Roboto', sans-serif; 157 | font-size: 0.7rem; 158 | text-align: left; 159 | color: #d4af37 !important; 160 | font-weight: bold; 161 | } 162 | 163 | /* Cart summary styling */ 164 | .cart-summary { 165 | padding: 20px; 166 | background-color: #d4d4d4; 167 | border-radius: 8px; 168 | margin-top: 20px; 169 | } 170 | 171 | .cart-summary p { 172 | margin: 10px 0; 173 | } 174 | .textsubtotal{ 175 | color:rgb(212, 175, 55); 176 | text-align: center; 177 | align-items: left; 178 | margin: 0; 179 | font-size: 1rem; 180 | flex: 1; 181 | } 182 | .cart-subtotal { 183 | font-family: 'Roboto', sans-serif; 184 | font-size: 1rem; 185 | text-align: left; 186 | color: #d4af37 !important; 187 | font-weight: bold; 188 | } 189 | 190 | .checkout-button-container { 191 | display: flex; 192 | justify-content: center; /* Horizontally center the button */ 193 | align-items: center; /* Vertically center the button */ 194 | width: 100%; /* Ensure the container takes up full width */ 195 | margin-top: 20px; /* Optional: add margin if you want some space above the button */ 196 | } 197 | 198 | .cart-summary .terms-container { 199 | display: flex; 200 | align-items: center; 201 | margin: 10px 0; 202 | } 203 | 204 | .cart-summary .terms-container input { 205 | margin-right: 10px; 206 | } 207 | .subtotal-container { 208 | display: flex; 209 | justify-content: space-between; 210 | align-items: center; 211 | margin-bottom: 10px; 212 | } 213 | 214 | .cart-summary { 215 | position: fixed; 216 | bottom: 0; 217 | right: 0; 218 | width:400px; 219 | background-color:#d4d4d4; 220 | overflow-y: auto; 221 | padding: 20px; 222 | border-top: 1px solid #ddd; 223 | z-index: 10; 224 | } 225 | 226 | 227 | 228 | .textsubtotal { 229 | margin-right: 8px; 230 | } 231 | 232 | 233 | -------------------------------------------------------------------------------- /src/pages/Admin/AddOrEditProduct.jsx: -------------------------------------------------------------------------------- 1 | import {useState, useEffect} from 'react'; 2 | import {Link, useParams } from 'react-router-dom'; 3 | import {createProduct, getProduct, updateProduct} from '../../api/product'; 4 | 5 | function AddOrEditProduct(){ 6 | const [newProduct, setNewProduct] = useState({ 7 | prodname: '', 8 | proddesc:'', 9 | category:'', 10 | size:'', 11 | price:'', 12 | image:'' 13 | }); 14 | const [successMessage, setSuccessMessage] = useState(''); 15 | const [error, setError] = useState({}); 16 | const {productId} = useParams(); //Get productId from url 17 | 18 | const formControlClass = 'mt-1 p-2 border border-gray-300 text-sm rounded-md w-full focus:outline-none'; 19 | 20 | const handleChange = (e) =>{ 21 | const{name, value} = e.target; 22 | setNewProduct((prevProduct) =>({ 23 | ...prevProduct, 24 | [name]: value 25 | })); 26 | }; 27 | 28 | const validateForm = () => { 29 | const newError = {}; 30 | 31 | // Validate Price (ensure it's a positive number) 32 | if (!newProduct.price || newProduct.price <= 0) { 33 | newError.price = 'Price must be a positive number'; 34 | } 35 | //if in case need to validate image url 36 | // const imageUrlPattern = /\.(jpg|jpeg|png|gif|bmp|webp)$/i; // Regex for image URL validation 37 | // // Validate Image URL 38 | // if (!newProduct.image || !/^https?:\/\/[^\s]+$/.test(newProduct.image)) { 39 | // newError.image = 'Please provide a valid URL'; 40 | // } else if (!imageUrlPattern.test(newProduct.image)) { 41 | // newError.image = 'Please provide a valid image URL (jpg, jpeg, png, gif, bmp, webp)'; 42 | // } 43 | 44 | return newError; // Return the error object 45 | }; 46 | 47 | useEffect(() =>{ 48 | if(productId){ 49 | const fetchProduct = async() =>{ 50 | try{ 51 | const product = await getProduct(productId); 52 | setNewProduct(product); 53 | }catch(error){ 54 | console.error('Error fetching product', error); 55 | setError({general: 'Failed to load product details'}) 56 | } 57 | } 58 | fetchProduct(); 59 | } 60 | },[productId]); 61 | 62 | const handleSubmit = async(e) =>{ 63 | e.preventDefault(); 64 | 65 | // Reset previous errors before trying again 66 | setError({}); 67 | setSuccessMessage(''); 68 | 69 | // Validating the form for price 70 | const formErrors = validateForm(); 71 | if (Object.keys(formErrors).length > 0) { 72 | setError(formErrors); // Set validation errors 73 | return; // Prevent submission if there are validation errors 74 | } 75 | 76 | try{ 77 | if(productId){ 78 | console.log(productId); 79 | await updateProduct(productId,newProduct); 80 | setSuccessMessage('Product updated successfully!'); 81 | console.log('Product updated successfully'); 82 | } 83 | else 84 | { 85 | await createProduct(newProduct); 86 | setSuccessMessage('Product added successfully!'); 87 | console.log('Product added successfully'); 88 | } 89 | 90 | }catch(error){ 91 | console.error('Error creating product', error) 92 | setError({ general: 'There was an issue adding the product. Please try again later.' }) 93 | } 94 | } 95 | 96 | return( 97 |
98 |

99 | {productId ? 'Edit Product' : 'Add New Product'}

100 |
101 | 102 | View Products 103 | 104 |
105 |
106 |
107 | 108 | 109 |
110 |
111 | 112 |