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
47 changes: 47 additions & 0 deletions frontend/src/GrantSearch.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.grant-page {
width: 100%;
padding: 20px;
}

.search-container {
position: relative;
display: flex;
align-items: center;
gap: 8px;
}

.search-input-container {
position: relative;
width: 100%;
}

.search-input {
color: black;
}

.dropdown-container {
position: absolute;
top: 100%;
left: 0;
width: 100%;
background: white;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
z-index: 10;
max-height: 200px;
overflow-y: auto;
}

.dropdown-item {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #eee;
transition: background 0.2s ease;
color: black;
}

.dropdown-item:hover {
background: #f1f1f1;
color: black;
}
2 changes: 2 additions & 0 deletions frontend/src/grant-info/components/GrantPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import GrantList from './GrantList.js';
import Footer from './Footer.js';
import BellButton from '../../Bell.js';
import '../../Bell.css'
import GrantSearch from './GrantSearch.js';


function GrantPage() {
Expand All @@ -12,6 +13,7 @@ function GrantPage() {
<div className="grant-page">
<div className="top-half">
<Header />
<GrantSearch/>

</div>
<div className="bell-container">
Expand Down
131 changes: 131 additions & 0 deletions frontend/src/grant-info/components/GrantSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Button, Input } from "@chakra-ui/react"
import { useEffect, useState } from "react";
import Fuse from "fuse.js";
import { Grant } from "@/external/bcanSatchel/store";
import "../../GrantSearch.css"


function GrantSearch() {

const [userInput, setUserInput] = useState("");
const [grants, setGrants] = useState<Grant[]>([]);
const [filter, setFilter] = useState("");
const [showDropdown, setShowDropdown] = useState(false);
const [dropdownGrants, setDropdownGrants] = useState<Grant[]>([]);


// intially fetches grants from backend and creates an event listener to handle for clicks outside of the dropdown
useEffect(() => {
fetchGrants()
document.addEventListener("click", handleClickOutside);
return () => {
document.removeEventListener("click", handleClickOutside);
};

}, []);

// fetches grants from backend
const fetchGrants = async () => {
try {
const response = await fetch(`http://localhost:3001/grant`, { method: 'GET' });
const data: Grant[] = await response.json();

// change 'organization' to 'organization_name'; TODO: fix the grant type in backend
const formattedData: Grant[] = data.map((grant: any) => ({
...grant,
organization_name: grant.organization || "Unknown Organization",
}));

console.log("Formatted Grants:", formattedData);
setGrants(formattedData);
} catch (error) {
console.error("Error fetching grants:", error);
}
};

// handles when the filter changes
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilter(e.target.value);
};

// stores the grant name
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUserInput(e.target.value);
performSearch(e.target.value)
};

// searches using fuzzy search
const performSearch = (query: string) => {
if (!query) {
setDropdownGrants([]);
setShowDropdown(false);
return;
}

const fuse = new Fuse<Grant>(grants, {
keys: filter ? [filter] : ["organization_name"],
threshold: 0.3,
});

const results = fuse.search(query).map(result => result.item);
setDropdownGrants(results.slice(0, 5));
setShowDropdown(results.length > 0);
};

// handle selection from dropdown
const handleSelectGrant = (selectedGrant: Grant) => {
setUserInput(selectedGrant.organization_name);
setShowDropdown(false);
};

// hide dropdown when clicking outside of it
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest(".search-container") && !target.closest(".dropdown-container")) {
setShowDropdown(false);
}
};


return (
<div className="grant-page">
<form className="search-container">
<select onChange={handleFilterChange} value={filter}>
<option value="">Filter by</option>
<option value="organization">Organization Name</option>
</select>

<div className="search-input-container">
<Input
placeholder="Search"
variant="subtle"
className="search-input"
onChange={handleInputChange}
value={userInput}
onFocus={() => setShowDropdown(dropdownGrants.length > 0)}
/>

{showDropdown && (
<div className="dropdown-container">
{dropdownGrants.map((grant, index) => (
<div
key={index}
className="dropdown-item"
onClick={() => handleSelectGrant(grant)}
>
{grant.organization_name}
</div>
))}
</div>
)}
</div>

<Button type="button" colorScheme="blue" onClick={() => performSearch(userInput)}>
Search
</Button>
</form>
</div>
);
}

export default GrantSearch;
118 changes: 10 additions & 108 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"dependencies": {
"fuse.js": "^7.1.0",
"ts-morph": "^23.0.0",
"typescript": "^5.7.3"
},
Expand Down