Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { UserProvider } from "./contexts/UserProvider";
import UploadFromUrl from "./pages/ImageUploadURLPage/UploadFromUrl";
import UserPins from "./pages/UserPins/UserPins";
import CategoryPage from "./pages/CategoryPage/CategoryPage";
import SearchResultsPage from "./pages/SearchResultsPage/SearchResultsPage";

function AppContent() {
const location = useLocation();
Expand All @@ -43,6 +44,7 @@ function AppContent() {
<Route path="/image_upload_from_url" element={<UploadFromUrl />} />
<Route path="/userpins" element={<UserPins />} />
<Route path="/category/:id" element={<CategoryPage />} />
<Route path="/search" element={<SearchResultsPage />} />
</Routes>
{<Footer />}
</>
Expand Down
109 changes: 105 additions & 4 deletions frontend/src/components/Header/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import SearchIcon from "@mui/icons-material/Search";
import { IconButton } from "@mui/material";
import React, { useState, useContext, useEffect } from "react";
import { Link } from "react-router-dom";
import React, { useState, useContext, useEffect, useRef } from "react";
import { Link, useNavigate } from "react-router-dom";
import Login from "../Forms/Login/Login";
import SignUp from "../Forms/SignUp/SignUp";
import Modal from "../Modal/Modal";
Expand All @@ -21,12 +21,20 @@
PressButton,
LoginButton,
SignupButton,
IconWrapper
IconWrapper,
SuggestionsContainer,
SuggestionItem
} from "./HeaderStyles";

const Header = () => {
const { user, loading } = useContext(UserContext);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [suggestions, setSuggestions] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const searchTimeoutRef = useRef(null);

const navigate = useNavigate();

// State for modal management
const [isOpen, setIsOpen] = useState(false);
Expand All @@ -51,6 +59,83 @@
setModalType(null);
};

const fetchSuggestions = async query => {
if (query.length < 2) {
setSuggestions([]);
return;
}

return new Promise(resolve => {
setTimeout(() => {
const dummySuggestions = [
`${query} ideas`,
`${query} recipes`,
`${query} fashion trends`,
`how to ${query}`,
`best ${query} 2025`
].filter(s => s.toLowerCase().includes(query.toLowerCase()));
resolve(dummySuggestions);
}, 300);

Check warning on line 78 in frontend/src/components/Header/Header.jsx

View workflow job for this annotation

GitHub Actions / eslint

No magic number: 300
});
};

const handleSearchInputChange = async event => {
const value = event.target.value;
setSearchTerm(value);
setShowSuggestions(true);
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}

// Set a new timeout to debounce the suggestion fetch
searchTimeoutRef.current = setTimeout(async () => {
if (value.trim().length > 1) {
const fetched = await fetchSuggestions(value.trim());
setSuggestions(fetched);
} else {
setSuggestions([]);
}
}, 400);

Check warning on line 98 in frontend/src/components/Header/Header.jsx

View workflow job for this annotation

GitHub Actions / eslint

No magic number: 400
};

const handleSearch = (query = searchTerm) => {
if (query.trim()) {
console.log("Performing search for:", query.trim());

Check warning on line 103 in frontend/src/components/Header/Header.jsx

View workflow job for this annotation

GitHub Actions / eslint

Unexpected console statement. Only these console methods are allowed: warn, error
navigate(`/search?q=${encodeURIComponent(query.trim())}`);
setSearchTerm(query.trim());
setSuggestions([]);
setShowSuggestions(false);
}
};

const handleFormSubmit = event => {
event.preventDefault();
handleSearch();
};

const handleSuggestionClick = suggestion => {
setSearchTerm(suggestion);
handleSearch(suggestion);
};

useEffect(() => {
const handleClickOutside = event => {
// Check if the click is outside the SearchWrapper (or a more specific parent)
if (
event.target.closest(`.${SearchWrapper.styledComponentId}`) === null &&
event.target.closest(`.${SuggestionsContainer.styledComponentId}`) ===
null
) {
setShowSuggestions(false);
}
};

document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);

return (
<Wrapper>
<LogoWrapper>
Expand Down Expand Up @@ -80,14 +165,30 @@
<IconButton>
<SearchIcon />
</IconButton>
<form action="">
<form onSubmit={handleFormSubmit}>
<input
type="text"
placeholder="Search for easy dinners, fashion, etc."
value={searchTerm}
onChange={handleSearchInputChange}
onFocus={() => setShowSuggestions(true)} // Show suggestions when input is focused
/>
<button type="submit"></button>
</form>
</SearchBarWrapper>

{showSuggestions && suggestions.length > 0 && (
<SuggestionsContainer>
{suggestions.map((s, index) => (
<SuggestionItem
key={index}
onClick={() => handleSuggestionClick(s)}
>
{s}
</SuggestionItem>
))}
</SuggestionsContainer>
)}
</SearchWrapper>

<AboutButton>
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/components/Header/HeaderStyles.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,26 @@ export const SearchBarWrapper = styled.div`
`;

export const IconWrapper = styled.div``;

export const SuggestionsContainer = styled.div`
position: absolute; /* Position relative to SearchWrapper */
top: 100%; /* Place it right below the search bar */
left: 0;
right: 0;
background-color: white;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 8px 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 1000; /* Ensure it's above other content */
max-height: 200px; /* Limit height and add scroll if many suggestions */
overflow-y: auto;
`;

export const SuggestionItem = styled.div`
padding: 10px 15px;
cursor: pointer;
&:hover {
background-color: #f0f0f0;
}
`;
94 changes: 94 additions & 0 deletions frontend/src/pages/SearchResultsPage/SearchResultsPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SearchResultsPage.jsx
import React, { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import styles from "./SearchResultsPage.module.css"; // Import the CSS Module

function SearchResultsPage() {
const [searchParams] = useSearchParams();
const query = searchParams.get("q");

const [results, setResults] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchSearchResults = async () => {
setLoading(true);
setResults([]);

if (query) {
// --- Dummy Data Simulation (replace with your API call) ---
await new Promise(resolve => setTimeout(resolve, 500));

const dummySearchResults = [
{
id: 1,
title: `Delicious ${query} Recipes`,
description: `Find easy and quick recipes for ${query} that your family will love.`,
imageUrl: `https://via.placeholder.com/60?text=${query.substring(0, 3)}1`
},
{
id: 2,
title: `Top ${query} Fashion Trends`,
description: `Explore the latest fashion trends related to ${query} for this season.`,
imageUrl: `https://via.placeholder.com/60?text=${query.substring(0, 3)}2`
},
{
id: 3,
title: `Guide to ${query} Decor`,
description: `Ideas and tips for incorporating ${query} into your home decor.`,
imageUrl: `https://via.placeholder.com/60?text=${query.substring(0, 3)}3`
},
{
id: 4,
title: `Healthy ${query} Meal Prep`,
description: `Plan your meals with these healthy ${query} meal prep ideas.`,
imageUrl: `https://placehold.co/60x60?text=${query.substring(0, 3)}`
}
].filter(
item =>
item.title.toLowerCase().includes(query.toLowerCase()) ||
item.description.toLowerCase().includes(query.toLowerCase())
);

setResults(dummySearchResults);
}
setLoading(false);
};

fetchSearchResults();
}, [query]);

return (
<div className={styles.resultsContainer}>
<h2 className={styles.resultsHeader}>
{query
? `Search Results for: "${query}"`
: "Please enter a search query."}
</h2>

{loading ? (
<p className={styles.loadingMessage}>Loading search results...</p>
) : results.length > 0 ? (
<ul className={styles.resultsList}>
{results.map(item => (
<li className={styles.resultItem} key={item.id}>
{item.imageUrl && <img src={item.imageUrl} alt={item.title} />}
<div>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
</li>
))}
</ul>
) : (
query && (
<p className={styles.noResultsMessage}>
No results found for "{query}".
</p>
)
)}
</div>
);
}

export default SearchResultsPage;
63 changes: 63 additions & 0 deletions frontend/src/pages/SearchResultsPage/SearchResultsPage.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* SearchResultsPage.module.css */

.resultsContainer {
padding: 20px;
max-width: 960px;
margin: 20px auto;
background-color: #f9f9f9;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding-top: 150px;
}

.resultsHeader {
color: #333;
margin-bottom: 20px;
text-align: center;
}

.loadingMessage {
text-align: center;
color: #666;
font-style: italic;
}

.noResultsMessage {
text-align: center;
color: #999;
}

.resultsList {
list-style: none;
padding: 0;
}

.resultItem {
background-color: white;
border: 1px solid #eee;
border-radius: 5px;
padding: 15px;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 15px;
}

.resultItem h3 {
margin: 0;
color: #007bff;
font-size: 1.2em;
}

.resultItem p {
margin: 5px 0 0;
color: #555;
font-size: 0.9em;
}

.resultItem img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
}
2 changes: 0 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading