From 156032dfd7fd86a04f28edd078a97206804005c6 Mon Sep 17 00:00:00 2001 From: Vivek Prakash Date: Sun, 10 Nov 2024 16:18:15 +0530 Subject: [PATCH] search-feature (#445) --- backend/Controllers/UserController.js | 40 ++++++- backend/routes/web.js | 1 + frontend/src/App.jsx | 2 + frontend/src/Components/Navbar.jsx | 22 ++++ frontend/src/Pages/SearchPage.jsx | 146 ++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 frontend/src/Pages/SearchPage.jsx diff --git a/backend/Controllers/UserController.js b/backend/Controllers/UserController.js index a1defd19..91590d5e 100644 --- a/backend/Controllers/UserController.js +++ b/backend/Controllers/UserController.js @@ -94,6 +94,43 @@ const getAllUserName = async (req, res) => { } }; +/** + * @route {GET} /api/users + * @description Returns an array of usernames that match the given substring + * @access public + */ + +const getUsersWithSimilarUsername = async (req, res) => { + try { + const { search } = req.query; // Get the substring from the query parameters + if (!search) { + return res.status(400).json({ success: false, message: "Username is required" }); + } + + // Find users matching the search query (case-insensitive) + const users = await User.find({ + username: { $regex: search, $options: 'i' }, // 'i' makes the search case-insensitive + }); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: "No users found with the given username substring" }); + } + + // Add the number of recipes for each user + const usersWithRecipeCount = await Promise.all(users.map(async (user) => { + const recipeCount = await Recipe.countDocuments({ user: user._id }); // Count recipes for each user + return { ...user.toObject(), recipeCount }; // Add the recipe count to the user object + })); + + res.status(200).json({ users: usersWithRecipeCount, success: true }); + } catch (error) { + console.log(error); + res.status(500).json({ success: false, message: "Internal server error" }); + } +}; + + + /** * @route {POST} /api/usernames * @description Authenticates an User @@ -480,7 +517,8 @@ const UserController = { getAllFeedback, getFeedbackByUserId, deleteFeedbackById, - deleteUnverifiedUsers + deleteUnverifiedUsers, + getUsersWithSimilarUsername }; diff --git a/backend/routes/web.js b/backend/routes/web.js index 9c94815d..1f672b4f 100644 --- a/backend/routes/web.js +++ b/backend/routes/web.js @@ -8,6 +8,7 @@ const router = Router(); // Get Requests router.get("/usernames", UserController.getAllUserName); +router.get("/users", UserController.getUsersWithSimilarUsername); router.get("/token", authenticateToken, UserController.verifyUserByToken); router.get("/recipes", RecipeController.allRecipe); // added route to get previous comments diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index cb566ccd..3e0d8fe9 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -26,6 +26,7 @@ import EmailVerification from "./Pages/EmailVerification.jsx" import ResendVerificationPage from "./Pages/ResendVerification.jsx"; import UserProfile from "./Pages/Profile.jsx"; +import UserSearch from "./Pages/SearchPage.jsx"; function App() { const [showScroll, setShowScroll] = useState(false); @@ -73,6 +74,7 @@ function App() { path="/recipes" element={} /> + } /> } diff --git a/frontend/src/Components/Navbar.jsx b/frontend/src/Components/Navbar.jsx index 3ae21844..dc733d39 100644 --- a/frontend/src/Components/Navbar.jsx +++ b/frontend/src/Components/Navbar.jsx @@ -152,6 +152,17 @@ const Navbar = () => { > Recipe bot + + `mr-5 hover:text-red-700 font-semibold ${ + isActive ? "text-red-700" : "text-black" + }` + } + onClick={handleLinkClick} + > + Search + @@ -229,6 +240,17 @@ const Navbar = () => { > Recipe bot + + `mr-5 hover:text-red-700 font-semibold ${ + isActive ? "text-red-700" : "text-black" + }` + } + onClick={handleLinkClick} + > + Search + {/* User profile actions */} diff --git a/frontend/src/Pages/SearchPage.jsx b/frontend/src/Pages/SearchPage.jsx new file mode 100644 index 00000000..2516a3db --- /dev/null +++ b/frontend/src/Pages/SearchPage.jsx @@ -0,0 +1,146 @@ +import { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; +import axios from 'axios'; + +export default function UserSearch() { + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [followingStatus, setFollowingStatus] = useState({}); + const backendURL = import.meta.env.VITE_BACKEND_URL; + const [user, setUser] = useState(null); + const path = useLocation().pathname; + + useEffect(() => { + // Fetch logged-in user details + let token = localStorage.getItem('tastytoken'); + if (token) { + token = JSON.parse(token); + axios.get(`${backendURL}/api/token`, { headers: { Authorization: `Bearer ${token}` } }) + .then((res) => { + if (res.data.success) { + setUser(res.data.user); + } + }) + .catch((err) => { + console.log(err); + setUser(null); + }); + } else { + setUser(null); + } + }, [path]); + + useEffect(() => { + // Check follow status for users in search results + if (searchResults.length > 0 && user) { + const userFollowStatus = {}; + searchResults.forEach((u) => { + userFollowStatus[u._id] = u.followers.includes(user._id); + }); + setFollowingStatus(userFollowStatus); + } + }, [searchResults, user]); + + useEffect(() => { + // Debounce search term input + const delayDebounceFn = setTimeout(() => { + if (searchTerm) { + searchUsers(searchTerm); + } else { + setSearchResults([]); + } + }, 300); + + return () => clearTimeout(delayDebounceFn); + }, [searchTerm]); + + const searchUsers = async (usernameSubstring) => { + setIsLoading(true); + setError(null); + try { + const response = await axios.get(`${backendURL}/api/users?search=${usernameSubstring}`); + if (response.data.success) { + setSearchResults(response.data.users); + } else { + setError('No users found.'); + } + } catch (err) { + setError('Failed to fetch users. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + const handleFollow = async (id) => { + const token = localStorage.getItem('tastytoken'); + try { + const username = JSON.parse(localStorage.getItem('username')); + await axios.post(`${backendURL}/api/follow`, { username, userId: id }, { headers: { Authorization: `Bearer ${token}` } }); + + // Update following status in the UI + setFollowingStatus((prevState) => ({ + ...prevState, + [id]: !prevState[id], + })); + } catch (err) { + console.error('Error updating follow status', err); + } + }; + + return ( +
+
+

FoodieConnect

+
+ +
+
+

Discover Culinary Creators

+
+ setSearchTerm(e.target.value)} + className="w-full p-4 pr-12 border-2 border-red-300 rounded-full focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-transparent" + /> +
+
+ + {isLoading &&
} + {error &&

{error}

} + {!isLoading && !error && searchResults.length === 0 && searchTerm &&

No users found. Try a different search term!

} + +
+ {searchResults.map((user) => ( +
+
+ +
+ {user.username} +
+

{user.username}

+

{user.recipeCount} Recipes

+
+
+ +
+ {user.followers.length} Followers + +
+
+
+ ))} +
+
+
+ ); +}