Skip to content
Merged
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: 1 addition & 1 deletion frontend/src/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const Login = observer(() => {
const success = await login(username, password);

if (success) {
navigate("/grant-info");
navigate("/main/all-grants");
} else {
setFailure(true);
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/main-page/MainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function MainPage() {
<div>
<NavBar />
</div>
<div className="px-6 lg:px-16 py-8 pt-16 w-full h-screen overflow-y-auto">
<div className="px-6 lg:px-14 py-8 pt-12 w-full h-screen overflow-y-auto">
<div className="min-h-screen mb-16">
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
Expand Down
131 changes: 104 additions & 27 deletions frontend/src/main-page/grants/GrantPage.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,102 @@
import "./styles/GrantPage.css";
import GrantList from "./grant-list/GrantList.tsx";
// import GrantList from "./grant-list/GrantList.tsx";

import AddGrantButton from "./new-grant/AddGrant.tsx";
import GrantSearch from "./filter-bar/GrantSearch.tsx";
import NewGrantModal from "./new-grant/NewGrantModal.tsx";
import { useEffect, useState } from "react";
import { Grant } from "../../../../middle-layer/types/Grant.ts";
import FilterBar from "./filter-bar/FilterBar.tsx";
// import FilterBar from "./filter-bar/FilterBar.tsx";
import GrantItem from "./grant-view/GrantView.tsx";
import { useAuthContext } from "../../context/auth/authContext";
import {
updateEndDateFilter,
updateFilter,
updateSearchQuery,
updateStartDateFilter,
updateYearFilter,
fetchAllGrants,
} from "../../external/bcanSatchel/actions.ts";
import { toJS } from "mobx";

import { fetchGrants } from "./filter-bar/processGrantData.ts";
import { observer } from "mobx-react-lite";
import { ProcessGrantData } from "./filter-bar/processGrantData.ts";
import { UserStatus } from "../../../../middle-layer/types/UserStatus.ts";
import { Navigate } from "react-router-dom";
import BellButton from "../navbar/Bell.tsx";
import GrantCard from "./grant-list/GrantCard.tsx";
import { api } from "../../api.ts";

// still needed potentially?
interface GrantPageProps {
showOnlyMyGrants?: boolean; //if true, filters grants by user email
}

function GrantPage({ showOnlyMyGrants = false }: GrantPageProps) {
function GrantPage({}: GrantPageProps) {
const [showNewGrantModal, setShowNewGrantModal] = useState(false);
const [wasGrantSubmitted, setWasGrantSubmitted] = useState(false);
const [selectedGrant, setSelectedGrant] = useState<Grant | null>(null);

// Use ProcessGrantData reactively to get filtered grants
const { grants } = ProcessGrantData();
const [curGrant, setCurGrant] = useState<Grant | null>(null);

// Set the first grant when grants are loaded (only on initial mount)
useEffect(() => {
if (grants.length > 0 && curGrant === null) {
setCurGrant(grants[0]);
}
}, [grants]);

// If the NewGrantModal has been closed and a new grant submitted (or existing grant edited),
// refetch the grants list and update the current grant to reflect any changes
// SHOULD BE CHANGED TO ALSO ACCOMODATE DELETIONS (CURRENTLY ONLY UPDATES IF GRANT WAS CREATED/EDITED, NOT DELETED)
useEffect(() => {
if (!wasGrantSubmitted || !curGrant) return;

const updateGrant = async () => {
try {
const response = await api(`/grant/${curGrant.grantId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});

if (response.ok) {
const updatedGrant = await response.json();
setCurGrant(updatedGrant);
console.log("✅ Grant refreshed:", updatedGrant);
} else {
console.error("❌ Failed to fetch updated grant");
}
} catch (err) {
console.error("Error fetching updated grant:", err);
}
};

const updateGrants = async () => {
try {
const response = await api("/grant");
if (!response.ok) {
throw new Error(`HTTP Error, Status: ${response.status}`);
}
const updatedGrants: Grant[] = await response.json();
fetchAllGrants(updatedGrants);
console.log("✅ Grants list refreshed");
} catch (error) {
console.error("Error fetching grants:", error);
}
};

updateGrants();
updateGrant();
setWasGrantSubmitted(false);
}, [wasGrantSubmitted]);

const [openModal, setOpenModal] = useState(false);

const { user } = useAuthContext(); //gets current logged in user
const userObj = toJS(user);

const currentUserEmail = userObj?.email || ""; //safe fallback
// const currentUserEmail = userObj?.email || ""; //safe fallback

console.log("Current logged-in user:", userObj);
// reset filters on initial render
Expand All @@ -47,42 +108,58 @@ function GrantPage({ showOnlyMyGrants = false }: GrantPageProps) {
updateSearchQuery("");
}, []);

useEffect(() => {
if (!showNewGrantModal && wasGrantSubmitted) {
fetchGrants();
setWasGrantSubmitted(false);
console.log("Use effect called in GrantPage");
}
}, [showNewGrantModal, wasGrantSubmitted]);

return user ? (
user?.position !== UserStatus.Inactive ? (
<div className="grant-page px-8">
<div className="top-half"></div>
<div className="flex justify-end align-middle p-4 gap-4">
<GrantSearch />
<div className="grant-page w-full items-end">
<div className="bell-container flex justify-end w-full">
<BellButton setOpenModal={setOpenModal} openModal={openModal} />
</div>
<GrantSearch />
<div className="flex w-full justify-between p-4 gap-4">
<span className="text-lg font-semibold">
FILTERS GO HERE
</span>
<AddGrantButton onClick={() => setShowNewGrantModal(true)} />
<div className="bell-container">
<BellButton setOpenModal={setOpenModal} openModal={openModal} />
</div>

<div className="flex flex-row w-full gap-4 justify-between">
<div className="flex flex-col w-[33%] h-[150vh] overflow-y-scroll pr-2">
{grants.map((grant) => (
<GrantCard
key={grant.grantId}
grant={grant}
isSelected={curGrant?.grantId === grant.grantId}
onClick={() => setCurGrant(grant)}
/>
))}
</div>
<div className="w-[65%]">
{curGrant ? (
<GrantItem grant={curGrant} />
) : (
<div className="flex items-center justify-center h-full text-gray-500">
No grants found.
</div>
)}
</div>
</div>
<div className="grid grid-cols-5 gap-8 px-4">

{/* <div className="grid grid-cols-5 gap-8 px-4">
<div className="col-span-1">
<FilterBar />
</div>
<div className="bot-half col-span-4">
<div className="grant-list-container">
<GrantList
selectedGrantId={
selectedGrant ? selectedGrant.grantId : undefined
curGrant ? curGrant.grantId : undefined
}
onClearSelectedGrant={() => setSelectedGrant(null)}
currentUserEmail={currentUserEmail}
showOnlyMyGrants={showOnlyMyGrants}
/>
</div>
</div>
</div>
</div> */}
<div className="hidden-features">
{showNewGrantModal && (
<NewGrantModal
Expand All @@ -104,4 +181,4 @@ function GrantPage({ showOnlyMyGrants = false }: GrantPageProps) {
);
}

export default GrantPage;
export default observer(GrantPage);
2 changes: 1 addition & 1 deletion frontend/src/main-page/grants/filter-bar/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const FilterBar: React.FC = observer(() => {
to={item.linkTo ? item.linkTo : "#"}
>
<div
className={`grant-button border hover:bg-primary-800 ${
className={`border hover:bg-primary-800 ${
selected === item.name ? "bg-primary-900" : "bg-white"
}`}
>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/main-page/grants/filter-bar/GrantSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function GrantSearch() {
}}
/>
<Input
placeholder="Search"
placeholder="Search for a grant..."
variant="subtle"
className="search-input"
onChange={handleInputChange}
Expand Down
70 changes: 70 additions & 0 deletions frontend/src/main-page/grants/grant-list/GrantCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Meant to replace GrantItem.tsx when we implement new design

import React from "react";
import { Grant } from "../../../../../middle-layer/types/Grant";
import StatusIndicator from "./StatusIndicator";

interface GrantCardProps {
grant: Grant;
isSelected: boolean;
onClick?: () => void;
}

const GrantCard: React.FC<GrantCardProps> = ({ grant, isSelected, onClick }) => {

const formattedDate = new Date(grant.application_deadline).toLocaleDateString('en-US', {
month: 'numeric',
day: 'numeric',
year: '2-digit',
});

return (
<div
onClick={onClick}
className={`
relative w-[100%] h-fit flex-shrink-0 rounded-2xl p-2 lg:p-4 mb-3 cursor-pointer bg-white
flex flex-col lg:flex-row justify-between items-center
${isSelected ? 'border-2 border-secondary-500' : 'border border-grey-200'}
hover:shadow-md
`}
>
{/* Colored left border indicator + Grant Name + Amount*/}
<div className="h-full w-[85%] lg:w-[55%] flex flex-row gap-3 py-4">
<div
className="relative group w-[0.35rem] min-w-[0.35rem] h-full rounded-full flex-shrink-0"
style={{ backgroundColor: grant.does_bcan_qualify ? "var(--color-green)" : "var(--color-red-dark)" }}
>
{/* Eligibility Badge - Shows on hover over the line */}
<div className={`
absolute -top-7 -left-4 px-3 py-1 rounded-full text-sm font-semibold whitespace-nowrap
opacity-0 group-hover:opacity-100 transition-opacity z-10
${grant.does_bcan_qualify
? 'bg-green-light text-green-dark'
: 'bg-red-light text-red-dark'
}
`}>
{grant.does_bcan_qualify ? 'Eligible' : 'Not Eligible'}
</div>
</div>
<div className="overflow-hidden">
<div className="text-lg font-semibold text-gray-900 lg:truncate text-left">
{grant.organization}
</div>
<div className="text-md font-medium text-gray-900 text-left">
${grant.amount}
</div>
</div>
</div>

{/* Card content */}
<div className="h-full w-[85%] lg:w-[40%] flex flex-col justify-between items-end pb-4 lg:pb-0">
<span className="text-md text-gray-600 text-right">
Due: <span className="font-semibold">{formattedDate}</span>
</span>
<StatusIndicator curStatus={grant.status} />
</div>
</div>
);
};

export default GrantCard;
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const StatusIndicator: React.FC<StatusIndicatorProps> = ({ curStatus }) => {

return (
<div
className="inline-flex w-fit flex-none items-center rounded-sm px-2 py-1"
className="inline-flex w-fit flex-none items-center rounded-full px-2 py-1"
style={{ color: darkColor, backgroundColor: lightColor }}
>
<span className="text-md">{labelText}</span>
Expand Down
Loading