From 43b9aa1ece1889494415321e128f7979637474a8 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Sun, 15 Feb 2026 22:50:30 -0500 Subject: [PATCH 01/10] Adding icon support to button component --- frontend/src/main-page/settings/Settings.tsx | 3 +++ frontend/src/main-page/settings/components/Button.tsx | 10 ++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/main-page/settings/Settings.tsx b/frontend/src/main-page/settings/Settings.tsx index 61fe0a4..5cd0a2e 100644 --- a/frontend/src/main-page/settings/Settings.tsx +++ b/frontend/src/main-page/settings/Settings.tsx @@ -1,6 +1,7 @@ import Button from "./components/Button"; import InfoCard from "./components/InfoCard"; import logo from "../../images/logo.svg"; +import { faPenToSquare } from "@fortawesome/free-solid-svg-icons"; export default function Settings() { return ( @@ -49,6 +50,8 @@ export default function Settings() { text="Edit" onClick={() => alert("edit personal info")} className="bg-white text-black border-2 border-grey-500" + logo={faPenToSquare} + logoPosition="right" /> } fields={[ diff --git a/frontend/src/main-page/settings/components/Button.tsx b/frontend/src/main-page/settings/components/Button.tsx index 2da0f16..72efd36 100644 --- a/frontend/src/main-page/settings/components/Button.tsx +++ b/frontend/src/main-page/settings/components/Button.tsx @@ -1,8 +1,11 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { IconProp } from "@fortawesome/fontawesome-svg-core"; + type ButtonProps = { text: string; onClick: () => void; className?: string; - logo?: string; + logo?: IconProp; logoPosition?: 'left' | 'right'; } @@ -21,13 +24,12 @@ export default function Button({ text, onClick, className, logo, logoPosition }: > {logo && logoPosition === 'left' && - + } {text} {logo && logoPosition === 'right' && - + } ); From 400549fcf97b0168a3fd6f79899f65ef55ceb986 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Sun, 15 Feb 2026 23:09:36 -0500 Subject: [PATCH 02/10] Filter status indicator --- .../grants/grant-list/StatusIndicator.tsx | 19 +-- frontend/tailwind.config.ts | 3 +- middle-layer/types/Status.ts | 115 ++++++++++++------ 3 files changed, 90 insertions(+), 47 deletions(-) diff --git a/frontend/src/main-page/grants/grant-list/StatusIndicator.tsx b/frontend/src/main-page/grants/grant-list/StatusIndicator.tsx index 5344a31..aca00ad 100644 --- a/frontend/src/main-page/grants/grant-list/StatusIndicator.tsx +++ b/frontend/src/main-page/grants/grant-list/StatusIndicator.tsx @@ -1,23 +1,26 @@ // StatusIndicator.tsx import React from "react"; -import { FaCircle } from "react-icons/fa"; -import { Status, getColorStatus } from "../../../../../middle-layer/types/Status.ts"; +import { + Status, + getColorStatus, +} from "../../../../../middle-layer/types/Status.ts"; interface StatusIndicatorProps { curStatus: Status; } const StatusIndicator: React.FC = ({ curStatus }) => { - const circleColor = getColorStatus(curStatus.toString()) + const lightColor = getColorStatus(curStatus.toString(), "light"); + const darkColor = getColorStatus(curStatus.toString(), "dark"); const labelText = curStatus; // curStatus from the json is stored as a string, so can directly use return ( -
- - - {labelText} - +
+ {labelText}
); }; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 0d38fbc..8b1f16e 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -89,9 +89,10 @@ export default { "9xl": "128rem", }, borderRadius: { - "4xl": "2rem", + sm: "0.5rem", md: "0.75rem", DEFAULT: "0.75rem", + "4xl": "2rem", }, }, }, diff --git a/middle-layer/types/Status.ts b/middle-layer/types/Status.ts index 3483345..c5d8ac9 100644 --- a/middle-layer/types/Status.ts +++ b/middle-layer/types/Status.ts @@ -5,57 +5,96 @@ * (3) Inactive: Grant earnings are used up */ export enum Status { - Potential = "Potential", - Active = "Active", - Inactive = "Inactive", - Rejected = "Rejected", - Pending = "Pending" + Potential = "Potential", + Active = "Active", + Inactive = "Inactive", + Rejected = "Rejected", + Pending = "Pending", } // TODO: 1) override stringify behavior of status enum 2) stringify, and then go back and modify enum (create helper function to generalize) // 3) turn enums to string // string rep of status -export function stringToStatus(status: string): Status | null{ - switch (status) { - case 'All': return null; // no filter - case 'Active': return Status.Active; - case 'Inactive': return Status.Inactive; - case 'Potential': return Status.Potential; - case 'Rejected': return Status.Rejected; - case 'Pending': return Status.Pending; - default: throw new Error(`Unknown status: ${status}`); - } +export function stringToStatus(status: string): Status | null { + switch (status) { + case "All": + return null; // no filter + case "Active": + return Status.Active; + case "Inactive": + return Status.Inactive; + case "Potential": + return Status.Potential; + case "Rejected": + return Status.Rejected; + case "Pending": + return Status.Pending; + default: + throw new Error(`Unknown status: ${status}`); + } } -export function statusToString(status : Status): string { - switch (status) { - case Status.Active : return 'Active'; - case Status.Inactive : return "Inactive"; - case Status.Potential : return "Potential"; - case Status.Rejected : return "Rejected"; - case Status.Pending : return "Pending"; - } +export function statusToString(status: Status): string { + switch (status) { + case Status.Active: + return "Active"; + case Status.Inactive: + return "Inactive"; + case Status.Potential: + return "Potential"; + case Status.Rejected: + return "Rejected"; + case Status.Pending: + return "Pending"; + } } // color associated with status on UI, represented as a string -export function getColorStatus(status: string) { +export function getColorStatus( + status: string, + variant: "light" | "dark" = "dark", +): string { + if (variant === "light") { switch (status) { - case "Active": return "var(--color-green)"; // green - case "Inactive": return "var(--color-grey-500)" // gray - case "Potential": return "var(--color-blue-dark)" // blue - // TODO add colors for rejected and pending - case "Rejected": return "var(--color-red)" // red - case "Pending": return "var(--color-yellow-dark)" // yellow - default: return 'gray'; + case "Active": + return "var(--color-green-light)"; // green + case "Inactive": + return "var(--color-grey-400)"; // gray + case "Potential": + return "var(--color-blue-light)"; // blue + // TODO add colors for rejected and pending + case "Rejected": + return "var(--color-red-light)"; // red + case "Pending": + return "var(--color-yellow-light)"; // yellow + default: + return "gray"; } + } else { + switch (status) { + case "Active": + return "var(--color-green-dark)"; // green + case "Inactive": + return "var(--color-grey-700)"; // gray + case "Potential": + return "var(--color-blue-dark)"; // blue + // TODO add colors for rejected and pending + case "Rejected": + return "var(--color-red-dark)"; // red + case "Pending": + return "var(--color-yellow-dark)"; // yellow + default: + return "gray"; + } + } } // Get list of status types for received and unreceived grants -export function getListApplied(received: boolean){ - if(received){ - return ["Active", "Inactive"] - } else{ - return ["Pending", "Rejected"] - } -} \ No newline at end of file +export function getListApplied(received: boolean) { + if (received) { + return ["Active", "Inactive"]; + } else { + return ["Pending", "Rejected"]; + } +} From 12801b10d04df9cb0ea131c11f0b3ced29f3f1d1 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 16 Feb 2026 00:13:41 -0500 Subject: [PATCH 03/10] finished description and top header --- .../grants/grant-details/GrantAttributes.tsx | 114 ----------- .../main-page/grants/grant-list/GrantList.tsx | 162 ++++++++------- .../grants/grant-list/StatusIndicator.tsx | 4 +- .../main-page/grants/grant-view/GrantView.tsx | 186 ++++++++++++++++++ frontend/src/main-page/settings/Settings.tsx | 1 - 5 files changed, 280 insertions(+), 187 deletions(-) delete mode 100644 frontend/src/main-page/grants/grant-details/GrantAttributes.tsx create mode 100644 frontend/src/main-page/grants/grant-view/GrantView.tsx diff --git a/frontend/src/main-page/grants/grant-details/GrantAttributes.tsx b/frontend/src/main-page/grants/grant-details/GrantAttributes.tsx deleted file mode 100644 index b1a3c51..0000000 --- a/frontend/src/main-page/grants/grant-details/GrantAttributes.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import '../styles/GrantAttributes.css'; -import { Grant } from '../../../../../middle-layer/types/Grant'; - -interface GrantAttributesProps { - isEditing: boolean; - curGrant: Grant; - setCurGrant: React.Dispatch>; -} - -export const GrantAttributes: React.FC = ({curGrant, setCurGrant, isEditing}) => { - - // placeholder for now before reworking, will remove redundant useState() - - const handleChange = (event: React.ChangeEvent) => { - const {name, value} = event.target; - - // only modifies the changed field - setCurGrant(curGrant => ({ - ...curGrant, - [name]: value - })); - }; - - return ( -
-
-
Status
- -
-
-
Does BCAN qualify?
- -
-
-
Deadline
- -
-
-
Notification Date
- -
-
-
Report Due:
- -
-
-
Estimated Completion Time:
- -
-
-
Scope Document:
- -
-
-
Grantmaker POC:
- -
-
-
Timeline:
- -
-
- ); -}; diff --git a/frontend/src/main-page/grants/grant-list/GrantList.tsx b/frontend/src/main-page/grants/grant-list/GrantList.tsx index be350d1..b080821 100644 --- a/frontend/src/main-page/grants/grant-list/GrantList.tsx +++ b/frontend/src/main-page/grants/grant-list/GrantList.tsx @@ -8,7 +8,8 @@ import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; import { ProcessGrantData } from "../filter-bar/processGrantData"; import NewGrantModal from "../new-grant/NewGrantModal"; import { Grant } from "../../../../../middle-layer/types/Grant"; -import { getAppStore } from '../../../external/bcanSatchel/store'; +import { getAppStore } from "../../../external/bcanSatchel/store"; +import GrantView from "../grant-view/GrantView"; const ITEMS_PER_PAGE = 6; @@ -27,24 +28,24 @@ const GrantList: React.FC = observer( currentUserEmail, }) => { const { grants } = ProcessGrantData(); - const {filterStatus} = getAppStore(); + const { filterStatus } = getAppStore(); const [currentPage, setPage] = useState(1); const [showNewGrantModal, setShowNewGrantModal] = useState(false); - // @ts-ignore + // @ts-ignore const [wasGrantSubmitted, setWasGrantSubmitted] = useState(false); const displayedGrants = showOnlyMyGrants ? grants.filter( (grant: Grant) => grant.bcan_poc?.POC_email?.toLowerCase() === - currentUserEmail?.toLowerCase() + currentUserEmail?.toLowerCase(), ) : grants; useEffect(() => { if (selectedGrantId !== undefined && grants.length > 0) { const index = grants.findIndex( - (grant) => grant.grantId === Number(selectedGrantId) + (grant) => grant.grantId === Number(selectedGrantId), ); if (index !== -1) { const targetPage = Math.floor(index / ITEMS_PER_PAGE) + 1; @@ -56,8 +57,8 @@ const GrantList: React.FC = observer( }, [selectedGrantId, grants, currentPage]); useEffect(() => { - setPage(1); - },[filterStatus, showOnlyMyGrants]); + setPage(1); + }, [filterStatus, showOnlyMyGrants]); const count = displayedGrants.length; const startRange = (currentPage - 1) * ITEMS_PER_PAGE; @@ -65,72 +66,93 @@ const GrantList: React.FC = observer( const visibleItems = displayedGrants.slice(startRange, endRange); return ( -
-
- -
- {visibleItems.map((grant) => ( - - ))} - {visibleItems.length === 0 && ( -

- {showOnlyMyGrants - ? "You currently have no grants assigned as BCAN POC." - : "No grants found :("} -

- )} -
-
- { - if (onClearSelectedGrant) { onClearSelectedGrant();}}} - onPageChange={(e) => { - setPage(e.page);}} - > - - - - - - - - {({ pages }) => - pages.map((page, index) => - page.type === "page" ? ( - setPage(page.value)} - aria-label={`Go to page ${page.value}`} - > - {page.value} - - ) : ( - "..." - ) - ) - } - - - - - - - - - {showNewGrantModal && ( - {setShowNewGrantModal(false); setWasGrantSubmitted(true); }} isOpen={showNewGrantModal} /> +
+
+ +
+ {visibleItems.map((grant) => ( +
+ + +
+ ))} + {visibleItems.length === 0 && ( +

+ {showOnlyMyGrants + ? "You currently have no grants assigned as BCAN POC." + : "No grants found :("} +

)} +
- + { + if (onClearSelectedGrant) { + onClearSelectedGrant(); + } + }} + onPageChange={(e) => { + setPage(e.page); + }} + > + + + + + + + + {({ pages }) => + pages.map((page, index) => + page.type === "page" ? ( + setPage(page.value)} + aria-label={`Go to page ${page.value}`} + > + {page.value} + + ) : ( + "..." + ), + ) + } + + + + + + + + + {showNewGrantModal && ( + { + setShowNewGrantModal(false); + setWasGrantSubmitted(true); + }} + isOpen={showNewGrantModal} + /> + )} +
); - } + }, ); export default GrantList; diff --git a/frontend/src/main-page/grants/grant-list/StatusIndicator.tsx b/frontend/src/main-page/grants/grant-list/StatusIndicator.tsx index aca00ad..884176a 100644 --- a/frontend/src/main-page/grants/grant-list/StatusIndicator.tsx +++ b/frontend/src/main-page/grants/grant-list/StatusIndicator.tsx @@ -17,10 +17,10 @@ const StatusIndicator: React.FC = ({ curStatus }) => { return (
- {labelText} + {labelText}
); }; diff --git a/frontend/src/main-page/grants/grant-view/GrantView.tsx b/frontend/src/main-page/grants/grant-view/GrantView.tsx new file mode 100644 index 0000000..df343d0 --- /dev/null +++ b/frontend/src/main-page/grants/grant-view/GrantView.tsx @@ -0,0 +1,186 @@ +import React, { useEffect, useState, useLayoutEffect } from "react"; +import "../styles/GrantItem.css"; +import { Grant } from "../../../../../middle-layer/types/Grant"; +import { api } from "../../../api"; +import { observer } from "mobx-react-lite"; +import { fetchGrants } from "../filter-bar/processGrantData"; +import StatusIndicator from "../../grants/grant-list/StatusIndicator"; +import { faPenToSquare } from "@fortawesome/free-solid-svg-icons"; +import Button from "../../settings/components/Button"; + +interface GrantItemProps { + grant: Grant; +} + +const GrantItem: React.FC = observer(({ grant }) => { + const [curGrant, setCurGrant] = useState(grant); + const [showNewGrantModal, setShowNewGrantModal] = useState(false); + const [wasGrantSubmitted, setWasGrantSubmitted] = useState(false); + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const useTruncatedElement = ({ + ref, + }: { + ref: React.RefObject; + }) => { + const [isTruncated, setIsTruncated] = useState(false); + const [isShowingMore, setIsShowingMore] = useState(false); + + useLayoutEffect(() => { + const { offsetHeight, scrollHeight } = ref.current || {}; + + if (offsetHeight && scrollHeight && offsetHeight < scrollHeight) { + setIsTruncated(true); + } else { + setIsTruncated(false); + } + }, [ref]); + + const toggleIsShowingMore = () => setIsShowingMore((prev) => !prev); + + return { + isTruncated, + isShowingMore, + toggleIsShowingMore, + }; + }; + const ref = React.useRef(null); + const { isTruncated, isShowingMore, toggleIsShowingMore } = + useTruncatedElement({ + ref, + }); + + // If the NewGrantModal has been closed and a new grant submitted (or existing grant edited), + // fetch the grant at this index so that all new changes are immediately reflected + useEffect(() => { + const updateGrant = async () => { + if (!showNewGrantModal && wasGrantSubmitted) { + try { + const response = await api(`/grant/${grant.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); + } + setWasGrantSubmitted(false); + } + }; + + updateGrant(); + }, [showNewGrantModal, wasGrantSubmitted]); + + const deleteGrant = async () => { + setShowDeleteModal(false); + + console.log("=== DELETE GRANT DEBUG ==="); + console.log("Current grant:", curGrant); + console.log("Grant ID:", curGrant.grantId); + console.log("Organization:", curGrant.organization); + console.log("Full URL:", `/grant/${curGrant.grantId}`); + + try { + const response = await api(`/grant/${curGrant.grantId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + + console.log("Response status:", response.status); + console.log("Response ok:", response.ok); + + if (response.ok) { + console.log("✅ Grant deleted successfully"); + // Refetch grants to update UI + await fetchGrants(); + } else { + // Get error details + const errorText = await response.text(); + console.error("❌ Error response:", errorText); + + let errorData; + try { + errorData = JSON.parse(errorText); + console.error("Parsed error:", errorData); + } catch { + console.error("Could not parse error response"); + } + } + } catch (err) { + console.error("=== EXCEPTION CAUGHT ==="); + console.error("Error type:", err instanceof Error ? "Error" : typeof err); + console.error("Error message:", err instanceof Error ? err.message : err); + console.error("Full error:", err); + } + }; + + function formatDate(isoString: string): string { + const date = new Date(isoString); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const year = date.getFullYear(); + return `${month}/${day}/${year}`; + } + + function formatCurrency(amount: number): string { + const formattedCurrency = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format(amount); + return formattedCurrency; + } + + return ( +
+ {/* Top header part */} +
+ {/* Left side */} +
+

{curGrant.organization}

+ +
+ {/* Right side */} +
+
+
+
+ {/* Middle info part */} +
+ {/* Description */} +
+

Description

+

+ {curGrant?.description || "N/A"} +

+ {isTruncated && ( + + )} +
+ {/* Other details */} +
+
+ ); +}); + +export default GrantItem; diff --git a/frontend/src/main-page/settings/Settings.tsx b/frontend/src/main-page/settings/Settings.tsx index 5cd0a2e..1bf0b6d 100644 --- a/frontend/src/main-page/settings/Settings.tsx +++ b/frontend/src/main-page/settings/Settings.tsx @@ -46,7 +46,6 @@ export default function Settings() { title="Personal Information" action={

{/* Middle info part */} -
+
{/* Description */}
-

Description

+

Description

{curGrant?.description || "N/A"}

{isTruncated && ( )}
{/* Other details */} +
+
+
+

Amount ($)

+

+ {formatCurrency(curGrant.amount) || "N/A"} +

+
+
+

BCAN Eligible

+

+ {curGrant.does_bcan_qualify ? ( + + Yes + + ) : ( + + No + + )} +

+
+
+
+
+

Due Date

+

+ {formatDate(curGrant.application_deadline) || "N/A"} +

+
+
+

Application Date

+

+ {formatDate(curGrant.application_deadline) || "N/A"} +

+
+
+
+
+

Grant Start Date

+

+ {formatDate(curGrant.grant_start_date) || "N/A"} +

+
+
+

Report Deadlines

+

+ {curGrant.report_deadlines && + curGrant.report_deadlines.length > 0 ? ( + curGrant.report_deadlines.map( + (deadline: string, index: number) => ( +

+ {formatDate(deadline)} +
+ ), + ) + ) : ( +
N/A
+ )} +

+
+
+
+
+

Timeline (years)

+

+ {curGrant.timeline ? curGrant.timeline : "N/A"} +

+
+
+

+ Estimated Completion Time (hours) +

+

+ {curGrant.estimated_completion_time + ? curGrant.estimated_completion_time + : "N/A"} +

+
+
+
+
+
+ {/* Bottom info */} +
+ {/* Contacts */} +
+

Contacts

+
+
+ Profile +
+

+ {grant.bcan_poc?.POC_name || "N/A"} +

+

+ + {grant.bcan_poc?.POC_email} + +

+
+
+
+ BCAN +
+
+
+
+ Profile +
+

+ {grant.bcan_poc?.POC_name || "N/A"} +

+

+ + {grant.grantmaker_poc?.POC_email || "N/A"} + +

+
+
+
+ Granter +
+
+
+
+
+ {/* Documents */} +
+

Documents

+

+ {curGrant?.description || "N/A"} +

+
); diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css index ab168ff..4501a0d 100644 --- a/frontend/src/styles/index.css +++ b/frontend/src/styles/index.css @@ -86,7 +86,6 @@ } a { - font-weight: 500; color: var(--color-secondary-500); text-decoration: inherit; } From fc9550b207dc7329768032a2472cf705c6de393c Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 16 Feb 2026 22:18:55 -0500 Subject: [PATCH 05/10] Grant view components --- .../grants/grant-list/GrantStatus.tsx | 5 - .../grants/grant-view/ContactCard.tsx | 41 +++ .../grants/grant-view/GrantFieldCol.tsx | 36 ++ .../main-page/grants/grant-view/GrantView.tsx | 337 +++++++----------- 4 files changed, 214 insertions(+), 205 deletions(-) delete mode 100644 frontend/src/main-page/grants/grant-list/GrantStatus.tsx create mode 100644 frontend/src/main-page/grants/grant-view/ContactCard.tsx create mode 100644 frontend/src/main-page/grants/grant-view/GrantFieldCol.tsx diff --git a/frontend/src/main-page/grants/grant-list/GrantStatus.tsx b/frontend/src/main-page/grants/grant-list/GrantStatus.tsx deleted file mode 100644 index dd63044..0000000 --- a/frontend/src/main-page/grants/grant-list/GrantStatus.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export function isActiveStatus(status: string): boolean { - return ["Pending", "In Review", "Awaiting Submission"].includes(status); -} - - diff --git a/frontend/src/main-page/grants/grant-view/ContactCard.tsx b/frontend/src/main-page/grants/grant-view/ContactCard.tsx new file mode 100644 index 0000000..1f7982f --- /dev/null +++ b/frontend/src/main-page/grants/grant-view/ContactCard.tsx @@ -0,0 +1,41 @@ +import POC from "../../../../../middle-layer/types/POC"; +import logo from "../../../images/logo.svg"; + +type ContactCardProps = { + contact?: POC; + type?: "BCAN" | "Granter"; +}; + +export default function ContactCard({ contact, type }: ContactCardProps) { + return ( +
+ Profile +
+

+ {contact?.POC_name || "N/A"} +

+

+ + {contact?.POC_email} + +

+
+
+
+ {type} +
+
+
+ ); +} diff --git a/frontend/src/main-page/grants/grant-view/GrantFieldCol.tsx b/frontend/src/main-page/grants/grant-view/GrantFieldCol.tsx new file mode 100644 index 0000000..af5f450 --- /dev/null +++ b/frontend/src/main-page/grants/grant-view/GrantFieldCol.tsx @@ -0,0 +1,36 @@ +import React from "react"; + +type InfoField = { + label: string; + value?: string | number | boolean; + item?: React.ReactNode; + important?: boolean; +}; + +type GrantFieldColProps = { + fields: InfoField[]; + colspan?: 1 | 2; +}; + +export default function GrantFieldCol({ + fields, + colspan = 1, +}: GrantFieldColProps) { + return ( +
+ {fields.map((field) => ( +
+

{field.label}

+

+ {field.item ? field.item : field.value || "N/A"} +

+
+ ))} +
+ ); +} diff --git a/frontend/src/main-page/grants/grant-view/GrantView.tsx b/frontend/src/main-page/grants/grant-view/GrantView.tsx index f24251b..e21bf7b 100644 --- a/frontend/src/main-page/grants/grant-view/GrantView.tsx +++ b/frontend/src/main-page/grants/grant-view/GrantView.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState, useLayoutEffect } from "react"; import { Grant } from "../../../../../middle-layer/types/Grant"; import { api } from "../../../api"; import { observer } from "mobx-react-lite"; -import { fetchGrants } from "../filter-bar/processGrantData"; import StatusIndicator from "../../grants/grant-list/StatusIndicator"; import { faPenToSquare, @@ -10,7 +9,8 @@ import { } from "@fortawesome/free-solid-svg-icons"; import Button from "../../settings/components/Button"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import logo from "../../../images/logo.svg"; +import ContactCard from "./ContactCard"; +import GrantFieldCol from "./GrantFieldCol"; interface GrantItemProps { grant: Grant; @@ -18,9 +18,7 @@ interface GrantItemProps { const GrantItem: React.FC = observer(({ grant }) => { const [curGrant, setCurGrant] = useState(grant); - const [showNewGrantModal, setShowNewGrantModal] = useState(false); const [wasGrantSubmitted, setWasGrantSubmitted] = useState(false); - const [showDeleteModal, setShowDeleteModal] = useState(false); const useTruncatedElement = ({ ref, @@ -58,7 +56,7 @@ const GrantItem: React.FC = observer(({ grant }) => { // fetch the grant at this index so that all new changes are immediately reflected useEffect(() => { const updateGrant = async () => { - if (!showNewGrantModal && wasGrantSubmitted) { + if (wasGrantSubmitted) { try { const response = await api(`/grant/${grant.grantId}`, { method: "GET", @@ -82,52 +80,7 @@ const GrantItem: React.FC = observer(({ grant }) => { }; updateGrant(); - }, [showNewGrantModal, wasGrantSubmitted]); - - const deleteGrant = async () => { - setShowDeleteModal(false); - - console.log("=== DELETE GRANT DEBUG ==="); - console.log("Current grant:", curGrant); - console.log("Grant ID:", curGrant.grantId); - console.log("Organization:", curGrant.organization); - console.log("Full URL:", `/grant/${curGrant.grantId}`); - - try { - const response = await api(`/grant/${curGrant.grantId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - }); - - console.log("Response status:", response.status); - console.log("Response ok:", response.ok); - - if (response.ok) { - console.log("✅ Grant deleted successfully"); - // Refetch grants to update UI - await fetchGrants(); - } else { - // Get error details - const errorText = await response.text(); - console.error("❌ Error response:", errorText); - - let errorData; - try { - errorData = JSON.parse(errorText); - console.error("Parsed error:", errorData); - } catch { - console.error("Could not parse error response"); - } - } - } catch (err) { - console.error("=== EXCEPTION CAUGHT ==="); - console.error("Error type:", err instanceof Error ? "Error" : typeof err); - console.error("Error message:", err instanceof Error ? err.message : err); - console.error("Full error:", err); - } - }; + }, [wasGrantSubmitted]); function formatDate(isoString: string): string { if (!isoString) return "N/A"; @@ -149,6 +102,7 @@ const GrantItem: React.FC = observer(({ grant }) => { return (
+ {/* Top header part */}
{/* Left side */} @@ -168,33 +122,48 @@ const GrantItem: React.FC = observer(({ grant }) => {

+ {/* Middle info part */} -
+
{/* Description */} -
-

Description

-

- {curGrant?.description || "N/A"} -

- {isTruncated && ( - - )} -
+ +

+ {curGrant?.description || "N/A"} +

+ {isTruncated && ( + + )} +
+ ), + }, + ]} + /> {/* Other details */}
-
-
-

Amount ($)

-

- {formatCurrency(curGrant.amount) || "N/A"} -

-
-
-

BCAN Eligible

-

- {curGrant.does_bcan_qualify ? ( + Yes @@ -202,139 +171,107 @@ const GrantItem: React.FC = observer(({ grant }) => { No - )} -

-
-
-
-
-

Due Date

-

- {formatDate(curGrant.application_deadline) || "N/A"} -

-
-
-

Application Date

-

- {formatDate(curGrant.application_deadline) || "N/A"} -

-
-
-
-
-

Grant Start Date

-

- {formatDate(curGrant.grant_start_date) || "N/A"} -

-
-
-

Report Deadlines

-

- {curGrant.report_deadlines && - curGrant.report_deadlines.length > 0 ? ( - curGrant.report_deadlines.map( - (deadline: string, index: number) => ( -

- {formatDate(deadline)} -
- ), - ) - ) : ( -
N/A
- )} -

-
-
-
-
-

Timeline (years)

-

- {curGrant.timeline ? curGrant.timeline : "N/A"} -

-
-
-

- Estimated Completion Time (hours) -

-

- {curGrant.estimated_completion_time - ? curGrant.estimated_completion_time - : "N/A"} -

-
-
+ ), + }, + ]} + /> + + 0 ? ( + curGrant.report_deadlines.map( + (deadline: string, index: number) => ( +
+ {formatDate(deadline)} +
+ ), + ) + ) : ( +
N/A
+ ), + }, + ]} + /> +

+ {/* Bottom info */} -
+
{/* Contacts */} -
-

Contacts

-
-
- Profile -
-

- {grant.bcan_poc?.POC_name || "N/A"} -

-

- - {grant.bcan_poc?.POC_email} - -

-
-
-
- BCAN + + +
-
-
-
- Profile -
-

- {grant.bcan_poc?.POC_name || "N/A"} -

-

- - {grant.grantmaker_poc?.POC_email || "N/A"} - -

-
-
-
- Granter -
-
-
-
-
+ ), + }, + ]} + /> {/* Documents */} -
-

Documents

-

- {curGrant?.description || "N/A"} -

-
+ 0 ? ( +
+ {curGrant.attachments.map((attachment, index) => ( +

+ + {attachment.attachment_name || attachment.url} + +

+ ))} +
+ ) : ( +
N/A
+ ), + }, + ]} + />
); From 8c13f744e5539a164c38c95a718becff848a9066 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 16 Feb 2026 22:55:28 -0500 Subject: [PATCH 06/10] Updating currency format --- .../src/main-page/grants/grant-view/GrantView.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/frontend/src/main-page/grants/grant-view/GrantView.tsx b/frontend/src/main-page/grants/grant-view/GrantView.tsx index e21bf7b..dbb08f3 100644 --- a/frontend/src/main-page/grants/grant-view/GrantView.tsx +++ b/frontend/src/main-page/grants/grant-view/GrantView.tsx @@ -92,17 +92,13 @@ const GrantItem: React.FC = observer(({ grant }) => { } function formatCurrency(amount: number): string { - const formattedCurrency = new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - maximumFractionDigits: 0, - }).format(amount); + const formattedCurrency = new Intl.NumberFormat().format(amount); return formattedCurrency; } return ( -
- +
+ {/* Top header part */}
{/* Left side */} @@ -135,6 +131,7 @@ const GrantItem: React.FC = observer(({ grant }) => {

{curGrant?.description || "N/A"}

@@ -254,12 +251,12 @@ const GrantItem: React.FC = observer(({ grant }) => { curGrant.attachments && curGrant.attachments.length > 0 ? (
{curGrant.attachments.map((attachment, index) => ( -

+

{attachment.attachment_name || attachment.url} From 40b06d55b5fe6ef034e7972884b9dc9cb109c308 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 16 Feb 2026 22:57:39 -0500 Subject: [PATCH 07/10] Bottom half resizing --- frontend/src/main-page/grants/grant-view/GrantView.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/main-page/grants/grant-view/GrantView.tsx b/frontend/src/main-page/grants/grant-view/GrantView.tsx index dbb08f3..6cee48e 100644 --- a/frontend/src/main-page/grants/grant-view/GrantView.tsx +++ b/frontend/src/main-page/grants/grant-view/GrantView.tsx @@ -98,7 +98,6 @@ const GrantItem: React.FC = observer(({ grant }) => { return (

- {/* Top header part */}
{/* Left side */} @@ -234,7 +233,7 @@ const GrantItem: React.FC = observer(({ grant }) => { { label: "Contacts", item: ( -
+
@@ -249,7 +248,7 @@ const GrantItem: React.FC = observer(({ grant }) => { label: "Documents", item: curGrant.attachments && curGrant.attachments.length > 0 ? ( -
+
{curGrant.attachments.map((attachment, index) => (

Date: Mon, 16 Feb 2026 22:58:53 -0500 Subject: [PATCH 08/10] Not eligible icon --- frontend/src/main-page/grants/grant-view/GrantView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/main-page/grants/grant-view/GrantView.tsx b/frontend/src/main-page/grants/grant-view/GrantView.tsx index 6cee48e..812f688 100644 --- a/frontend/src/main-page/grants/grant-view/GrantView.tsx +++ b/frontend/src/main-page/grants/grant-view/GrantView.tsx @@ -6,6 +6,7 @@ import StatusIndicator from "../../grants/grant-list/StatusIndicator"; import { faPenToSquare, faCheckSquare, + faXmarkSquare, } from "@fortawesome/free-solid-svg-icons"; import Button from "../../settings/components/Button"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -165,7 +166,7 @@ const GrantItem: React.FC = observer(({ grant }) => { ) : ( - No + No ), }, From 7fcc8c86d0091a8359f426b0b3a6b014f96be6cb Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Tue, 17 Feb 2026 16:26:16 -0500 Subject: [PATCH 09/10] Adding cost benefit analysis --- .../grant-details/CostBenefitAnalysis.tsx | 115 ------------- .../main-page/grants/grant-list/GrantItem.tsx | 2 +- .../grants/grant-view/ContactCard.tsx | 2 +- .../grants/grant-view/CostBenefitAnalysis.tsx | 153 ++++++++++++++++++ .../main-page/grants/grant-view/GrantView.tsx | 20 ++- frontend/src/sign-up/InputField.tsx | 38 +++++ frontend/tailwind.config.ts | 4 + 7 files changed, 215 insertions(+), 119 deletions(-) delete mode 100644 frontend/src/main-page/grants/grant-details/CostBenefitAnalysis.tsx create mode 100644 frontend/src/main-page/grants/grant-view/CostBenefitAnalysis.tsx create mode 100644 frontend/src/sign-up/InputField.tsx diff --git a/frontend/src/main-page/grants/grant-details/CostBenefitAnalysis.tsx b/frontend/src/main-page/grants/grant-details/CostBenefitAnalysis.tsx deleted file mode 100644 index 9e6ab6c..0000000 --- a/frontend/src/main-page/grants/grant-details/CostBenefitAnalysis.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, { useState } from 'react'; -import { Grant } from '../../../../../middle-layer/types/Grant'; -import '../styles/CostBenefitAnalysis.css'; - -interface CostBenefitAnalysisProps { - grant: Grant; -} - -export const CostBenefitAnalysis: React.FC = ({ grant }) => { - const [hourlyRate, setHourlyRate] = useState(''); - const [timePerReport, setTimePerReport] = useState(''); - const [netBenefit, setNetBenefit] = useState(null); - - const calculateNetBenefit = () => { - console.log('Called calculate') - console.log('hourlyRate state:', hourlyRate) - console.log('timePerReport state:', timePerReport) - const rate = parseFloat(hourlyRate); - const timeReport = parseFloat(timePerReport); - - console.log('Parsed rate:', rate) - console.log('Parsed timeReport:', timeReport) - - // Validation - if (isNaN(rate) || isNaN(timeReport) || rate <= 0 || timeReport <= 0) { - alert('Please enter valid positive numbers for hourly rate and time per report.'); - return; - } - - const reportCount = grant.report_deadlines?.length ?? 0; - const grantAmount = grant.amount; - const estimatedTime = grant.estimated_completion_time | 5; - - console.log('Grant values - Amount:', grantAmount, 'EstTime:', estimatedTime, 'ReportCount:', reportCount); - - // Formula: NetBenefit = GrantAmount - ((EstimatedCompletionTime + ReportCount * TimePerReport) * StaffHourlyRate) - const result = grantAmount - ((estimatedTime + reportCount * timeReport) * rate); - - console.log('Final result:', result); - - setNetBenefit(result); - }; - - const formatCurrency = (amount: number): string => { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', - maximumFractionDigits: 2 - }).format(amount); - }; - - return ( -

- ); -}; \ No newline at end of file diff --git a/frontend/src/main-page/grants/grant-list/GrantItem.tsx b/frontend/src/main-page/grants/grant-list/GrantItem.tsx index 427bec6..3960fd1 100644 --- a/frontend/src/main-page/grants/grant-list/GrantItem.tsx +++ b/frontend/src/main-page/grants/grant-list/GrantItem.tsx @@ -9,7 +9,7 @@ import { api } from "../../../api"; import { MdOutlinePerson2 } from "react-icons/md"; import Attachment from "../../../../../middle-layer/types/Attachment"; import NewGrantModal from "../new-grant/NewGrantModal"; -import { CostBenefitAnalysis } from "../grant-details/CostBenefitAnalysis"; +import { CostBenefitAnalysis } from "../grant-view/CostBenefitAnalysis"; import ActionConfirmation from "../../../custom/ActionConfirmation"; import { observer } from "mobx-react-lite"; import { fetchGrants } from "../filter-bar/processGrantData"; diff --git a/frontend/src/main-page/grants/grant-view/ContactCard.tsx b/frontend/src/main-page/grants/grant-view/ContactCard.tsx index 1f7982f..843b254 100644 --- a/frontend/src/main-page/grants/grant-view/ContactCard.tsx +++ b/frontend/src/main-page/grants/grant-view/ContactCard.tsx @@ -8,7 +8,7 @@ type ContactCardProps = { export default function ContactCard({ contact, type }: ContactCardProps) { return ( -
+
Profile = ({ + grant, +}) => { + const [hourlyRate, setHourlyRate] = useState(""); + const [timePerReport, setTimePerReport] = useState(""); + const [costBenefitResult, setCostBenefitResult] = + useState(null); + + const calculateCostBenefit = () => { + console.log("Called calculate"); + console.log("hourlyRate state:", hourlyRate); + console.log("timePerReport state:", timePerReport); + const rate = parseFloat(hourlyRate); + const timeReport = parseFloat(timePerReport); + + console.log("Parsed rate:", rate); + console.log("Parsed timeReport:", timeReport); + + // Validation + if (isNaN(rate) || isNaN(timeReport) || rate <= 0 || timeReport <= 0) { + alert( + "Please enter valid positive numbers for hourly rate and time per report.", + ); + return; + } + + const reportCount = grant.report_deadlines?.length ?? 0; + const grantAmount = grant.amount; + const estimatedTime = grant.estimated_completion_time | 5; + + console.log( + "Grant values - Amount:", + grantAmount, + "EstTime:", + estimatedTime, + "ReportCount:", + reportCount, + ); + + // Formula: NetBenefit = GrantAmount - ((EstimatedCompletionTime + ReportCount * TimePerReport) * StaffHourlyRate) + const totalReportingCost = reportCount * timeReport * rate; + + const resultObj = { + totalReportingCost: totalReportingCost, + totalReportingTime: reportCount * timeReport, + overhead: (totalReportingCost / grantAmount) * 100 || 0, + costPerReport: totalReportingCost / reportCount || 0, + grantAmount: grantAmount, + }; + + setCostBenefitResult(resultObj); + }; + + const formatCurrency = (amount: number): string => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format(amount); + }; + + return ( +
+ + {/* Input form */} +
+ {/* Hourly Rate Input */} +
+ setHourlyRate(e.target.value)} + /> +
+ + {/* Time Per Report Input */} +
+ setTimePerReport(e.target.value)} + /> +
+ + {/* Calculate Button */} +
+ + {/* Shows the net benefit result */} +
+ {costBenefitResult && ( +
+
+

+ {formatCurrency(costBenefitResult.costPerReport)} +

+

Cost per report

+
+
+
+ Total reporting cost: + + {formatCurrency(costBenefitResult.totalReportingCost)} + +
+
+ Total reporting time: + + {new Intl.NumberFormat("en-US").format( + costBenefitResult.totalReportingTime, + )}{" "} + hours + +
+
+ Overhead: + + {costBenefitResult.overhead.toFixed(1)}% of{" "} + {formatCurrency(costBenefitResult.grantAmount)} grant + +
+
+
+ )} +
+
+ ); +}; diff --git a/frontend/src/main-page/grants/grant-view/GrantView.tsx b/frontend/src/main-page/grants/grant-view/GrantView.tsx index 812f688..534f01b 100644 --- a/frontend/src/main-page/grants/grant-view/GrantView.tsx +++ b/frontend/src/main-page/grants/grant-view/GrantView.tsx @@ -12,6 +12,7 @@ import Button from "../../settings/components/Button"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import ContactCard from "./ContactCard"; import GrantFieldCol from "./GrantFieldCol"; +import { CostBenefitAnalysis } from "./CostBenefitAnalysis"; interface GrantItemProps { grant: Grant; @@ -181,7 +182,7 @@ const GrantItem: React.FC = observer(({ grant }) => { }, { label: "Application Date", - value: formatDate(curGrant.application_deadline), + value: "TBD (add to DB)", }, ]} /> @@ -234,7 +235,7 @@ const GrantItem: React.FC = observer(({ grant }) => { { label: "Contacts", item: ( -
+
@@ -270,6 +271,21 @@ const GrantItem: React.FC = observer(({ grant }) => { ]} />
+
+ + {/* Cost Benefit */} +
+ + ), + }, + ]} + /> +
); }); diff --git a/frontend/src/sign-up/InputField.tsx b/frontend/src/sign-up/InputField.tsx new file mode 100644 index 0000000..d722d8d --- /dev/null +++ b/frontend/src/sign-up/InputField.tsx @@ -0,0 +1,38 @@ +import type { InputHTMLAttributes } from "react"; + +type InputFieldProps = { + id: string; + label: string; + required?: boolean; + error?: boolean; +} & Omit, "id" | "className">; + +/** + * Reusable text input with label and optional required asterisk. + * Uses Tailwind and project color tokens (grey-400, red, etc.). + */ +export default function InputField({ + id, + label, + required, + error, + ...inputProps +}: InputFieldProps) { + return ( +
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 8b1f16e..bec42b2 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -88,6 +88,10 @@ export default { "8xl": "96rem", "9xl": "128rem", }, + borderWidth: { + DEFAULT: "1.5px", + 0: "0", + }, borderRadius: { sm: "0.5rem", md: "0.75rem", From 0b0881b1c8a973d92165bd7c204fb4cdc127bb45 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Tue, 17 Feb 2026 16:54:06 -0500 Subject: [PATCH 10/10] Fixing dom error --- frontend/src/main-page/grants/grant-view/GrantFieldCol.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/main-page/grants/grant-view/GrantFieldCol.tsx b/frontend/src/main-page/grants/grant-view/GrantFieldCol.tsx index af5f450..efdb2c9 100644 --- a/frontend/src/main-page/grants/grant-view/GrantFieldCol.tsx +++ b/frontend/src/main-page/grants/grant-view/GrantFieldCol.tsx @@ -26,9 +26,9 @@ export default function GrantFieldCol({ {fields.map((field) => (

{field.label}

-

+

{field.item ? field.item : field.value || "N/A"} -

+
))}