diff --git a/cbfc03a4970c5843bf12eb79f438ba1208d03450 b/cbfc03a4970c5843bf12eb79f438ba1208d03450 new file mode 160000 index 00000000..cbfc03a4 --- /dev/null +++ b/cbfc03a4970c5843bf12eb79f438ba1208d03450 @@ -0,0 +1 @@ +Subproject commit cbfc03a4970c5843bf12eb79f438ba1208d03450 diff --git a/client/package.json b/client/package.json index 6c41a1e4..abcfeca1 100644 --- a/client/package.json +++ b/client/package.json @@ -47,5 +47,9 @@ "last 1 firefox version", "last 1 safari version" ] - } + }, + "proxy": + "http://localhost:5000" + + } diff --git a/client/src/components/Card/ItemCard/.cph/.ItemCard.js_7b4e6e623e1f6a5e14c8f79257359ca6.prob b/client/src/components/Card/ItemCard/.cph/.ItemCard.js_7b4e6e623e1f6a5e14c8f79257359ca6.prob new file mode 100644 index 00000000..2f5ebebc --- /dev/null +++ b/client/src/components/Card/ItemCard/.cph/.ItemCard.js_7b4e6e623e1f6a5e14c8f79257359ca6.prob @@ -0,0 +1 @@ +{"name":"Local: ItemCard","url":"d:\\trendhora\\client\\src\\components\\Card\\ItemCard\\ItemCard.js","tests":[{"id":1754153493385,"input":"","output":""}],"interactive":false,"memoryLimit":1024,"timeLimit":3000,"srcPath":"d:\\trendhora\\client\\src\\components\\Card\\ItemCard\\ItemCard.js","group":"local","local":true} \ No newline at end of file diff --git a/client/src/components/Card/ItemCard/ItemCard.css b/client/src/components/Card/ItemCard/ItemCard.css index 064c13c3..59824e37 100644 --- a/client/src/components/Card/ItemCard/ItemCard.css +++ b/client/src/components/Card/ItemCard/ItemCard.css @@ -1,91 +1,77 @@ - - -.product__card__card { - height: 450px; - width: 270px; - margin: 14px; - border-width: 1px; - box-shadow: 0px 2px 6px -3px; - border-radius: 5px; +.product-card { + width: 100%; + max-width: 300px; + border: 1px solid #e0e0e0; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; + background-color: #fff; + margin: 10px auto; } -.product__card { - height: 100%; - width: 100%; +.product-card:hover { + transform: translateY(-5px); } -.product__image { - height: 80%; - width: auto; - display: flex; - justify-content: first baseline; - transition: 1s; +.product-card-image { + position: relative; + width: 100%; + height: 250px; + overflow: hidden; display: flex; - justify-content: center; align-items: center; + justify-content: center; } -.product__image > img{ +.product-img { max-width: 100%; - height: 100%; - transition: 1s; - margin: 0px auto; - border-top-left-radius: 5px; - border-top-right-radius: 5px; + max-height: 100%; + object-fit: cover; } -.product__card__detail { - height: 20%; - position: relative; +.product-card-actions { + position: absolute; + top: 8px; + right: 8px; display: flex; - justify-content: center; - align-items: center; flex-direction: column; - background-color: #ffffff ; + gap: 5px; + z-index: 10; } -.product__name { - height: 30%; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - font-size: 1rem; - font-weight: bold; +.product-action-btn { + background-color: white; + border-radius: 50%; + padding: 6px; + box-shadow: 0 2px 5px rgba(0,0,0,0.2); + transition: background-color 0.2s ease; } -.product__name a{ - color: black !important; - text-decoration: none; +.product-action-btn:hover { + background-color: #f0f0f0; } -.product__description { - height: 20%; - width: 100%; - display: flex; - justify-content: center; - align-items: center; +.product-card-details { + padding: 12px; + text-align: left; +} + +.product-card-name { font-size: 1rem; - text-align: center; + font-weight: 600; + color: #333; + text-decoration: none; } -.product__price { - height: 50%; - width: 100%; - display: flex; - justify-content: center; - align-items: center; - font-size: 1.1rem; - font-weight: bold; +.product-card-description { + font-size: 0.85rem; + color: #777; + margin: 5px 0; } -.product__card__action { - width: 100%; - height: 45px; - position: absolute; - bottom: 0%; - display: flex; - justify-content: space-between; - flex-direction: row; - padding: 0px 5px; -} \ No newline at end of file +.product-card-price { + font-size: 1rem; + font-weight: bold; + color: #111; +} diff --git a/client/src/components/Card/ItemCard/ItemCard.js b/client/src/components/Card/ItemCard/ItemCard.js index 00b55f97..3271ef13 100644 --- a/client/src/components/Card/ItemCard/ItemCard.js +++ b/client/src/components/Card/ItemCard/ItemCard.js @@ -1,221 +1,61 @@ -// import './ItemCard.css'; -// import { useContext, useState } from "react"; -// import { Link } from "react-router-dom"; -// import { CartItemsContext } from "../../../Context/CartItemsContext"; -// import { IconButton } from '@mui/material'; -// import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart'; -// import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; -// import { WishItemsContext } from '../../../Context/WishItemsContext'; -// import Toaster from '../../Toaster/toaster'; // Import the Toaster component - -// const ItemCard = (props) => { -// const [isHovered, setIsHovered] = useState(false); -// const [showToaster, setShowToaster] = useState(false); -// const [toasterMessage, setToasterMessage] = useState(''); - -// const cartItemsContext = useContext(CartItemsContext); -// const wishItemsContext = useContext(WishItemsContext); - -// const handleAddToWishList = () => { -// wishItemsContext.addItem(props.item); -// setToasterMessage("Your item has been added to wishlist."); -// setShowToaster(true); -// }; - -// // const handleAddToCart = () => { -// // cartItemsContext.addItem(props.item, 1); -// // setToasterMessage("Your item has been added to cart."); -// // setShowToaster(true); -// // }; -// const handleAddToCart = () => { -// const existingItem = cartItemsContext.items.find(item => item._id === props.item._id); - -// const currentQty = existingItem?.quantity ?? 0; -// const newQuantity = currentQty + 1; - -// cartItemsContext.addItem(props.item, 1); - -// setToasterMessage(`"${props.item.name}" added to cart\nQuantity ${newQuantity} added to cart`); -// setShowToaster(true); -// }; - - -// const handleCloseToaster = () => { -// setShowToaster(false); -// }; - -// if (!props.item || !props.item.category || !props.item.image || props.item.image.length === 0) { -// return null; // Avoid rendering if item is not defined or missing required properties -// } - -// const getImageUrl = (image) => { -// const url = `https://trendhora-api.onrender.com/public/${props.item.category}/${image.filename}`; -// return url; -// }; - -// return ( -// <> -//
-//
-//
setIsHovered(true)} -// onMouseLeave={() => setIsHovered(false)} -// > -// {isHovered && props.item.image[1] ? -// item : -// item -// } -//
-//
-//
-// -// {props.item.name} -// -//
-//
-// {props.item.description} -//
-//
-// ${props.item.price} -//
-//
-// -// -// -// -// -// -//
-//
-//
-//
- -// {/* Toaster Component */} -// {/* */} -// - -// -// ); -// }; - -// export default ItemCard; - import './ItemCard.css'; import { useContext, useState } from "react"; import { Link } from "react-router-dom"; import { CartItemsContext } from "../../../Context/CartItemsContext"; -import { IconButton } from '@mui/material'; +import { IconButton, Tooltip } from '@mui/material'; import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import { WishItemsContext } from '../../../Context/WishItemsContext'; -import Toaster from '../../Toaster/toaster'; - -const ItemCard = (props) => { +const ItemCard = ({ item }) => { const [isHovered, setIsHovered] = useState(false); - const [showToaster, setShowToaster] = useState(false); - const [toasterTitle, setToasterTitle] = useState(''); - const [toasterMessage, setToasterMessage] = useState(''); - const cartItemsContext = useContext(CartItemsContext); const wishItemsContext = useContext(WishItemsContext); - const handleAddToWishList = () => { - wishItemsContext.addItem(props.item); - setToasterTitle(`Added "${props.item.name}" to wishlist`); - setToasterMessage("Your item has been added to wishlist."); - setShowToaster(true); - }; - - const handleAddToCart = () => { - cartItemsContext.addItem({ ...props.item, quantity: 1 }); - - // 🔁 Get current quantity from cart after adding - const cartItem = cartItemsContext.items.find(i => i._id === props.item._id); - const updatedQty = cartItem ? cartItem.quantity : 1; - - setToasterTitle(`"${props.item.name}" added to cart`); - // setToasterMessage(`Quantity: ${updatedQty}`); - setToasterMessage(`Quantity: ${updatedQty || 1}`); - setShowToaster(true); - }; - - const handleCloseToaster = () => { - setShowToaster(false); - }; - - if (!props.item || !props.item.category || !props.item.image || props.item.image.length === 0) { - return null; - } + if (!item || !item.category || !item.image || item.image.length === 0) return null; const getImageUrl = (image) => { - return `https://trendhora-api.onrender.com/public/${props.item.category}/${image.filename}`; + return window.location.hostname === "localhost" + ? `/public/${item.category}/${image.filename}` + : `https://trendhora-api.onrender.com/public/${item.category}/${image.filename}`; }; return ( - <> -
-
-
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {isHovered && props.item.image[1] ? ( - item - ) : ( - item - )} -
-
-
- - {props.item.name} - -
-
- {props.item.description} -
-
- ${props.item.price} -
-
- - - - - - -
-
+
+
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + {item.name} + +
+ + wishItemsContext.addItem(item)} className="product-action-btn"> + + + + + cartItemsContext.addItem(item, 1)} className="product-action-btn"> + + +
- - - +
+ + {item.name} + +
{item.description}
+
${item.price}
+
+
); }; -export default ItemCard; \ No newline at end of file +export default ItemCard; diff --git a/client/src/components/Search/index.css b/client/src/components/Search/index.css index a59ba6a0..e19257e9 100644 --- a/client/src/components/Search/index.css +++ b/client/src/components/Search/index.css @@ -1,15 +1,80 @@ - .search__container { width: 100%; - height: 70vh; + padding: 40px 20px; display: flex; justify-content: center; - align-items: center; + align-items: flex-start; + min-height: 80vh; + box-sizing: border-box; } -.search__container__header { - height: 100px; - display: flex; - justify-content: center; - align-items: center; -} \ No newline at end of file +.search__container__content { + width: 100%; + max-width: 1280px; +} + +.search__loading, +.search__error, +.search__no-results, +.search__empty { + text-align: center; + padding: 40px 20px; +} + +.search__loading h2, +.search__error h2, +.search__no-results h2, +.search__empty h2 { + font-size: 1.8rem; + margin-bottom: 10px; + color: #333; +} + +.search__error p, +.search__no-results p, +.search__empty p { + font-size: 1.1rem; + color: #666; + margin: 0; +} + +.search__results__header { + text-align: center; + margin-bottom: 30px; + padding: 20px 0; +} + +.search__results__header h2 { + font-size: 2rem; + margin-bottom: 10px; + color: #333; +} + +.search__results__header p { + font-size: 1.1rem; + color: #666; +} + +.search__results__grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 32px 20px; + width: 100%; + align-items: stretch; + margin-bottom: 40px; +} + +/* Responsive */ +@media (max-width: 768px) { + .search__results__grid { + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 16px; + } +} + +@media (max-width: 480px) { + .search__results__grid { + grid-template-columns: 1fr; + gap: 16px; + } +} diff --git a/client/src/components/Search/index.js b/client/src/components/Search/index.js index 32963c4a..cde9c87b 100644 --- a/client/src/components/Search/index.js +++ b/client/src/components/Search/index.js @@ -1,28 +1,88 @@ -import { useEffect } from 'react'; -import { useContext } from 'react'; +import { useEffect, useState, useContext } from 'react'; import { useSearchParams } from 'react-router-dom'; import { SearchContext } from '../../Context/SearchContext'; -import './index.css' +import ItemCard from '../Card/ItemCard/ItemCard'; +import './index.css'; const Search = () => { - const search = useContext(SearchContext) - const [ searchParam, setSearchParam ] = useSearchParams() - - const searchQuery = { - query: search.searchQuery - } + const search = useContext(SearchContext); + const [searchParam, setSearchParam] = useSearchParams(); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); useEffect(() => { - setSearchParam(searchQuery, { replace: true }) - }, [searchQuery.query]) + if (search.searchQuery) { + setSearchParam({ q: search.searchQuery }, { replace: true }); + setLoading(true); + setError(null); + + fetch(`/api/items/search?q=${encodeURIComponent(search.searchQuery)}`) + .then(res => { + if (!res.ok) throw new Error('No items found'); + return res.json(); + }) + .then(data => { + setResults(data); + setLoading(false); + }) + .catch(err => { + setError(err.message); + setResults([]); + setLoading(false); + }); + } else { + setSearchParam({}, { replace: true }); + setResults([]); + } + }, [search.searchQuery]); - return ( + return (
-
-

No results found for "{search.searchQuery}"

+
+ {loading && ( +
+

Searching for "{search.searchQuery}"...

+
+ )} + + {!loading && error && ( +
+

No results found for "{search.searchQuery}"

+

Try searching for something else

+
+ )} + + {!loading && !error && results.length === 0 && search.searchQuery && ( +
+

No results found for "{search.searchQuery}"

+

Try searching for something else

+
+ )} + + {!loading && !error && results.length > 0 && ( +
+
+

Search Results for "{search.searchQuery}"

+

{results.length} item{results.length !== 1 ? 's' : ''} found

+
+
+ {results.map(item => ( + + ))} +
+
+ )} + + {!loading && !error && !search.searchQuery && ( +
+

Search for products

+

Enter a search term to find products

+
+ )}
- ); -} - -export default Search; \ No newline at end of file + ); +}; + +export default Search; diff --git a/server/config/db.js b/server/config/db.js index 45e0f5dd..5e1f6943 100644 --- a/server/config/db.js +++ b/server/config/db.js @@ -3,7 +3,7 @@ const mongoose = require("mongoose"); const connectDB = async () => { try { mongoose.set('strictQuery', false); // Add this line - const conn = await mongoose.connect(process.env.MONGO_URI); + const conn = await mongoose.connect(process.env.MONGO_URI || "mongodb://localhost:27017/test"); console.log("Mongo DB Connected: ", conn.connection.host); } catch (err) { console.log(err); diff --git a/server/controllers/itemsController.js b/server/controllers/itemsController.js index eca40bc8..c20a7bbe 100644 --- a/server/controllers/itemsController.js +++ b/server/controllers/itemsController.js @@ -71,10 +71,39 @@ const deleteItem = (req, res) => { res.json({message: "delete Item"}) } +const searchItems = async (req, res) => { + try { + const { q } = req.query; + if (!q) { + return res.status(400).json({ message: 'Missing search query' }); + } + // Enhanced case-insensitive search in multiple fields + const items = await Item.find({ + $or: [ + { name: { $regex: q, $options: 'i' } }, + { category: { $regex: q, $options: 'i' } }, + { type: { $regex: q, $options: 'i' } }, + { description: { $regex: q, $options: 'i' } }, + { color: { $regex: q, $options: 'i' } }, + { highlights: { $regex: q, $options: 'i' } }, + { detail: { $regex: q, $options: 'i' } } + ] + }); + if (items.length > 0) { + res.status(200).json(items); + } else { + res.status(404).json({ message: 'No items found' }); + } + } catch (err) { + res.status(500).json({ message: 'Server error' }); + } +}; + module.exports = { getItem, addItem, updateItem, deleteItem, - getItemById + getItemById, + searchItems } \ No newline at end of file diff --git a/server/itemsCollection.js b/server/itemsCollection.js index bc49989e..5024b2d2 100644 --- a/server/itemsCollection.js +++ b/server/itemsCollection.js @@ -52,7 +52,96 @@ const itemsCollection = [ ], "createdAt": "2022-08-28T21:41:09.891Z", "updatedAt": "2022-08-28T21:41:09.891Z", - "__v": 0 + "__v": 0, + "detail": "Product details not specified." + }, + { + "_id": "630be0f50dd6605053c3f085", + "name": "Ermenegildo Zegna", + "category": "men", + "color": "navy", + "type": "blazer", + "description": "Classic Navy Blazer", + "price": 1200, + "size": ["S", " M", " L", " XL"], + "highlights": ["100% wool", " Classic fit", " Single breasted", " Two-button closure", " Flap pockets"], + "image": [{ + "fieldname": "images", + "originalname": "zegna-blazer-1.jpg", + "encoding": "7bit", + "mimetype": "image/jpeg", + "destination": "./public/men", + "filename": "images-1661722869734.jpg", + "path": "public/men/images-1661722869734.jpg", + "size": 54700 + }, + { + "fieldname": "images", + "originalname": "zegna-blazer-2.jpg", + "encoding": "7bit", + "mimetype": "image/jpeg", + "destination": "./public/men", + "filename": "images-1661722869796.jpg", + "path": "public/men/images-1661722869796.jpg", + "size": 57072 + } + ], + "createdAt": "2022-08-28T21:41:09.891Z", + "updatedAt": "2022-08-28T21:41:09.891Z", + "__v": 0, + "detail": "Premium Ermenegildo Zegna navy blazer crafted from the finest Italian wool. Perfect for business and formal occasions." + }, + { + "_id": "630be0f50dd6605053c3f086", + "name": "Zegna Sport", + "category": "men", + "color": "grey", + "type": "blazer", + "description": "Casual Grey Blazer", + "price": 850, + "size": ["S", " M", " L"], + "highlights": ["Lightweight wool", " Unstructured fit", " Patch pockets", " Single button", " Casual style"], + "image": [{ + "fieldname": "images", + "originalname": "zegna-sport-1.jpg", + "encoding": "7bit", + "mimetype": "image/jpeg", + "destination": "./public/men", + "filename": "images-1661722869735.jpg", + "path": "public/men/images-1661722869735.jpg", + "size": 54700 + } + ], + "createdAt": "2022-08-28T21:41:09.891Z", + "updatedAt": "2022-08-28T21:41:09.891Z", + "__v": 0, + "detail": "Contemporary grey blazer from Zegna Sport collection, perfect for smart casual occasions." + }, + { + "_id": "630be0f50dd6605053c3f087", + "name": "Balenciaga", + "category": "women", + "color": "black", + "type": "blazer", + "description": "Oversized Blazer", + "price": 2100, + "size": ["S", " M", " L"], + "highlights": ["Oversized fit", " Structured shoulders", " Single button", " Flap pockets", " Modern design"], + "image": [{ + "fieldname": "images", + "originalname": "balenciaga-blazer-1.jpg", + "encoding": "7bit", + "mimetype": "image/jpeg", + "destination": "./public/women", + "filename": "images-1661722869736.jpg", + "path": "public/women/images-1661722869736.jpg", + "size": 54700 + } + ], + "createdAt": "2022-08-28T21:41:09.891Z", + "updatedAt": "2022-08-28T21:41:09.891Z", + "__v": 0, + "detail": "Statement oversized blazer from Balenciaga with bold shoulders and contemporary styling." }, { "_id": "630be1b70dd6605053c3f088", @@ -97,7 +186,8 @@ const itemsCollection = [ ], "createdAt": "2022-08-28T21:44:23.361Z", "updatedAt": "2022-08-28T21:44:23.361Z", - "__v": 0 + "__v": 0, + "detail": "Product details not specified." }, { "_id": "630be4fb0dd6605053c3f095", @@ -142,7 +232,8 @@ const itemsCollection = [ ], "createdAt": "2022-08-28T21:58:19.498Z", "updatedAt": "2022-08-28T21:58:19.498Z", - "__v": 0 + "__v": 0, + "detail": "Product details not specified." }, { "_id": "630c0ba6cacae4f0ab3bd5b0", @@ -980,4 +1071,6 @@ const itemsCollection = [ "updatedAt": "2022-08-29T14:28:06.542Z", "__v": 0 } -] \ No newline at end of file +] + +module.exports = itemsCollection; \ No newline at end of file diff --git a/server/models/Item.js b/server/models/Item.js index db19d96b..58366bfa 100644 --- a/server/models/Item.js +++ b/server/models/Item.js @@ -50,4 +50,4 @@ const itemSchema = mongoose.Schema( } ) -module.exports = mongoose.model("Item", itemSchema) \ No newline at end of file +module.exports = mongoose.model("Item", itemSchema); \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 815912c4..8e72b06d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,11 +1,11 @@ { - "name": "Trendhora Server", + "name": "TrendHora", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "Trendhora Server", + "name": "TrendHora", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/server/routes/items.js b/server/routes/items.js index fd327c06..0c6fe04f 100644 --- a/server/routes/items.js +++ b/server/routes/items.js @@ -2,7 +2,7 @@ const express = require("express") const router = express.Router() const cors = require("cors") const uploadPhoto = require("../middlewares/upload") -const { getItem, addItem, updateItem, deleteItem, getItemById } = require("../controllers/itemsController") +const { getItem, addItem, updateItem, deleteItem, searchItems } = require("../controllers/itemsController") const Item = require("../models/Item"); router.get('/', cors(), async (req, res) => { @@ -18,19 +18,13 @@ router.get('/', cors(), async (req, res) => { res.status(500).json({ message: "Server error" }); } }); - -/* The get request to get single item by ID*/ -router.get('/:id',getItemById); - - - - /* The post request must have a body elemnt with name images */ -router.post('/', uploadPhoto.array('images'), addItem); +router.post('/', uploadPhoto.array('images'), addItem) +router.put('/:id', updateItem) -router.put('/:id', updateItem); +router.delete('/:id', deleteItem) -router.delete('/:id', deleteItem); +router.get('/search',cors(), searchItems) -module.exports = router; \ No newline at end of file +module.exports = router \ No newline at end of file diff --git a/server/seedItem.js b/server/seedItem.js new file mode 100644 index 00000000..2b857859 --- /dev/null +++ b/server/seedItem.js @@ -0,0 +1,18 @@ +const mongoose = require('mongoose'); +const Item = require('./models/Item'); +const itemsCollection = require('./itemsCollection'); +require('dotenv').config(); + +const MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/test'; + +mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }) + .then(async () => { + await Item.deleteMany({}); + await Item.insertMany(itemsCollection); + console.log('Database seeded!'); + mongoose.disconnect(); + }) + .catch(err => { + console.error('Error seeding database:', err); + mongoose.disconnect(); + }); \ No newline at end of file diff --git a/server/server.js b/server/server.js index d001e63f..0b77512a 100644 --- a/server/server.js +++ b/server/server.js @@ -22,7 +22,6 @@ app.use('/api/payment', require("./routes/payment")); app.use('/api/auth', require('./routes/auth')); -// Add this line after other routes // Root route app.get('/', (req, res) => {