Skip to content

Commit

Permalink
search-feature (#445)
Browse files Browse the repository at this point in the history
  • Loading branch information
IkkiOcean authored Nov 10, 2024
1 parent cfcf828 commit 156032d
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 1 deletion.
40 changes: 39 additions & 1 deletion backend/Controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -480,7 +517,8 @@ const UserController = {
getAllFeedback,
getFeedbackByUserId,
deleteFeedbackById,
deleteUnverifiedUsers
deleteUnverifiedUsers,
getUsersWithSimilarUsername
};


Expand Down
1 change: 1 addition & 0 deletions backend/routes/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -73,6 +74,7 @@ function App() {
path="/recipes"
element={<Recipes key={"recipes"} type="" />}
/>
<Route path="/search" element={<UserSearch />} />
<Route
path="/mainmeals"
element={<Recipes key={"Main-meal"} type="Main-meal" />}
Expand Down
22 changes: 22 additions & 0 deletions frontend/src/Components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,17 @@ const Navbar = () => {
>
Recipe bot
</NavLink>
<NavLink
to="/search"
className={({ isActive }) =>
`mr-5 hover:text-red-700 font-semibold ${
isActive ? "text-red-700" : "text-black"
}`
}
onClick={handleLinkClick}
>
Search
</NavLink>
</nav>
</div>

Expand Down Expand Up @@ -229,6 +240,17 @@ const Navbar = () => {
>
Recipe bot
</NavLink>
<NavLink
to="/search"
className={({ isActive }) =>
`mr-5 hover:text-red-700 font-semibold ${
isActive ? "text-red-700" : "text-black"
}`
}
onClick={handleLinkClick}
>
Search
</NavLink>
</nav>

{/* User profile actions */}
Expand Down
146 changes: 146 additions & 0 deletions frontend/src/Pages/SearchPage.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="min-h-screen bg-gradient-to-b from-red-50 to-white">
<header className="bg-red-700 text-white py-4 px-6 shadow-lg">
<h1 className="text-3xl font-bold">FoodieConnect</h1>
</header>

<main className="container mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow-xl p-6 mb-8">
<h2 className="text-2xl font-bold text-red-700 mb-4">Discover Culinary Creators</h2>
<div className="relative">
<input
type="text"
placeholder="Search for chefs, bakers, and food enthusiasts..."
value={searchTerm}
onChange={(e) => 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"
/>
</div>
</div>

{isLoading && <div className="flex justify-center items-center py-8"><div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-red-700"></div></div>}
{error && <div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-8" role="alert"><p>{error}</p></div>}
{!isLoading && !error && searchResults.length === 0 && searchTerm && <div className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-8" role="alert"><p>No users found. Try a different search term!</p></div>}

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{searchResults.map((user) => (
<div key={user._id} className="block">
<div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-xl transition duration-300 ease-in-out transform hover:-translate-y-1">
<Link to={`/profile/${user._id}`} className="p-6">
<div className="flex items-center mb-1 ml-2">
<img src={user.profile} alt={user.username} className="w-16 h-16 rounded-full border-2 border-red-500" />
<div className="ml-4">
<h3 className="text-xl font-semibold text-red-700">{user.username}</h3>
<p className="text-gray-600">{user.recipeCount} Recipes</p>
</div>
</div>
</Link>
<div className="bg-red-50 px-6 py-4 flex justify-between items-center">
<span className="text-sm text-gray-500">{user.followers.length} Followers</span>
<button
className={`px-4 py-2 rounded-full transition duration-300 ease-in-out ${followingStatus[user._id] ? 'bg-gray-500' : 'bg-red-600 text-white hover:bg-red-700'}`}
onClick={() => handleFollow(user._id)}
>
{followingStatus[user._id] ? 'Following' : 'Follow'}
</button>
</div>
</div>
</div>
))}
</div>
</main>
</div>
);
}

0 comments on commit 156032d

Please sign in to comment.