From 24ed2e063143bb6f778c272b73c950ac575e8548 Mon Sep 17 00:00:00 2001 From: ryaken-nakamoto Date: Mon, 3 Feb 2025 21:00:34 -0500 Subject: [PATCH 1/6] created satchel store for all grants fetched from the backend to display in grantlist --- backend/src/grant/grant.controller.ts | 3 -- frontend/src/external/bcanSatchel/actions.ts | 9 ++++ frontend/src/external/bcanSatchel/mutators.ts | 11 +++- frontend/src/external/bcanSatchel/store.ts | 21 ++++++++ .../src/grant-info/components/GrantItem.tsx | 54 ++++++------------- .../src/grant-info/components/GrantList.tsx | 51 ++++++++++++++---- package.json | 2 +- 7 files changed, 98 insertions(+), 53 deletions(-) diff --git a/backend/src/grant/grant.controller.ts b/backend/src/grant/grant.controller.ts index 913f7965..fe809738 100644 --- a/backend/src/grant/grant.controller.ts +++ b/backend/src/grant/grant.controller.ts @@ -10,8 +10,6 @@ export class GrantController { return await this.grantService.getAllGrants(); } - // the @Param looks for arguments, in the decorator above the function definition, and - // then uses it as a parameter @Get(':id') async getGrantById(@Param('id') GrantId: string) { return await this.grantService.getGrantById(parseInt(GrantId, 10)); @@ -41,5 +39,4 @@ export class GrantController { - } \ No newline at end of file diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts index b8cd2ae9..760016e9 100644 --- a/frontend/src/external/bcanSatchel/actions.ts +++ b/frontend/src/external/bcanSatchel/actions.ts @@ -1,4 +1,5 @@ import { action } from 'satcheljs'; +import { Grant } from './store.js' /** * Set whether the user is authenticated, update the user object, @@ -24,3 +25,11 @@ export const updateUserProfile = action('updateUserProfile', (user: any) => ({ * Completely log out the user (clear tokens, user data, etc.). */ export const logoutUser = action('logoutUser'); + +/** + * Moves along the all grants that are fetched from back end to mutator. + */ +export const fetchAllGrants = action( + 'fetchAllGrants', + (grants: Grant[]) => ({grants}) +); diff --git a/frontend/src/external/bcanSatchel/mutators.ts b/frontend/src/external/bcanSatchel/mutators.ts index e423df73..f8ea0332 100644 --- a/frontend/src/external/bcanSatchel/mutators.ts +++ b/frontend/src/external/bcanSatchel/mutators.ts @@ -1,5 +1,5 @@ import { mutator } from 'satcheljs'; -import { setAuthState, updateUserProfile, logoutUser } from './actions'; +import { setAuthState, updateUserProfile, logoutUser, fetchAllGrants } from './actions'; import { getAppStore } from './store'; /** @@ -34,3 +34,12 @@ mutator(logoutUser, () => { store.user = null; store.accessToken = null; }); + + +/** + * Reassigns all grants to new grants from the backend. + */ +mutator(fetchAllGrants, (actionMessage) => { + const store = getAppStore(); + store.allGrants = actionMessage.grants; +}); diff --git a/frontend/src/external/bcanSatchel/store.ts b/frontend/src/external/bcanSatchel/store.ts index c05bb42c..027d6b85 100644 --- a/frontend/src/external/bcanSatchel/store.ts +++ b/frontend/src/external/bcanSatchel/store.ts @@ -10,6 +10,26 @@ export interface AppState { isAuthenticated: boolean; user: User | null; accessToken: string | null; + allGrants: Grant[] | [] +} + +// model for Grant objects, matches exactly the same as backend grant.model +// TODO: should synchronize from same file? +export interface Grant { + grantId: number; + organization_name: string; + description: string; + is_bcan_qualifying: boolean; + status: string; + amount: number; + deadline: string; + notifications_on_for_user: boolean; + reporting_requirements: string; + restrictions: string; + point_of_contacts: string[]; + attached_resources: string[]; + comments: string[]; + isArchived : boolean; } // Define initial state @@ -17,6 +37,7 @@ const initialState: AppState = { isAuthenticated: false, user: null, accessToken: null, + allGrants: [] }; const store = createStore('appStore', initialState); diff --git a/frontend/src/grant-info/components/GrantItem.tsx b/frontend/src/grant-info/components/GrantItem.tsx index e440ab11..8467d36d 100644 --- a/frontend/src/grant-info/components/GrantItem.tsx +++ b/frontend/src/grant-info/components/GrantItem.tsx @@ -1,47 +1,23 @@ -import React, {useState, useEffect } from 'react'; +import React, {useState } from 'react'; import './styles/GrantItem.css'; import { GrantAttributes } from './GrantAttributes'; import GrantDetails from './GrantDetails'; import {StatusContext} from './StatusContext'; +import {Grant} from "@/external/bcanSatchel/store.ts"; -// TODO: [JAN-14] Make uneditable field editable (ex: Description, Application Reqs, Additional Notes) interface GrantItemProps { - grantName: string; - applicationDate: string; - generalStatus: string; - amount: number; - restrictionStatus: string; + grant: Grant; } -const GrantItem: React.FC = (props) => { - // will change back to const later once below is achieved. - const { grantName, applicationDate, generalStatus, amount, restrictionStatus } = props; - // NOTE: For now, generalStatus will be changed to demonstrate fetching from the database - // Once Front-End display matches database schema, the query will actually be made in - // GrantList.tsx. Furthermore, there is no established way for the front-end to know - // which grant will be edited. Will default to GrantId: 1 for now as well. - const [isExpanded, setIsExpanded] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [curStatus, setCurStatus] = useState(generalStatus); +// TODO: [JAN-14] Make uneditable field editable (ex: Description, Application Reqs, Additional Notes) +const GrantItem: React.FC = ({grant}) => { + + + const [isExpanded, setIsExpanded] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [curStatus, setCurStatus] = useState(grant.status); // NOTE: ^^this is also a placeholder for generalStatus - // fetching initial status - useEffect(() => { - const fetchStatus = async () => { - try { - const rawResponse = await fetch('http://localhost:3001/grant/1'); - if (rawResponse.ok) { - const data = await rawResponse.json(); - setCurStatus(data.status); - } else { - console.error('Failed to fetch grant status:', rawResponse.statusText); - } - } catch (err) { - console.error('Error fetching status:', err); - } - }; - fetchStatus(); - }, []); const toggleExpand = () => { setIsExpanded(!isExpanded); @@ -73,11 +49,11 @@ const GrantItem: React.FC = (props) => { // class name with either be grant-item or grant-item-expanded
    -
  • {grantName}
  • -
  • {applicationDate}
  • -
  • {curStatus}
  • {/*This is replacing generalStatus for now*/} -
  • ${amount}
  • -
  • {restrictionStatus}
  • +
  • {grant.organization_name}
  • +
  • {"no attribute for app-date"}
  • +
  • {grant.status}
  • +
  • ${grant.amount}
  • +
  • {grant.restrictions}
{isExpanded && ( diff --git a/frontend/src/grant-info/components/GrantList.tsx b/frontend/src/grant-info/components/GrantList.tsx index 4b9007fa..37dda6fb 100644 --- a/frontend/src/grant-info/components/GrantList.tsx +++ b/frontend/src/grant-info/components/GrantList.tsx @@ -1,5 +1,9 @@ import GrantItem from "./GrantItem" import "./styles/GrantList.css" +import {useEffect, useState} from "react" +import { fetchAllGrants} from "../../external/bcanSatchel/actions.ts"; +import { Grant } from "../../external/bcanSatchel/actions.ts"; +import { getAppStore } from "../../external/bcanSatchel/store.ts"; import { PaginationRoot, @@ -11,21 +15,40 @@ import { import { usePaginationContext } from "@chakra-ui/react" + + // simulate a big list: -const ALL_GRANTS = Array.from({ length: 11 }).map((_, i) => ({ - grantName: `Community Development Grant #${i + 1}`, - applicationDate: `2024-09-${(i % 30) + 1}`, - generalStatus: i % 2 === 0 ? "Approved" : "Pending", - amount: (i + 1) * 1000, - restrictionStatus: i % 3 === 0 ? "Restricted" : "Unrestricted", -})) +// const ALL_GRANTS = Array.from({ length: 11 }).map((_, i) => ({ +// grantName: `Community Development Grant #${i + 1}`, +// applicationDate: `2024-09-${(i % 30) + 1}`, +// generalStatus: i % 2 === 0 ? "Approved" : "Pending", +// amount: (i + 1) * 1000, +// restrictionStatus: i % 3 === 0 ? "Restricted" : "Unrestricted", +// })) // How many items to show per page -const ITEMS_PER_PAGE = 3 +const fetchGrants = async () => { + try { + const response = await fetch('http://localhost:3001/grant'); + if (!response.ok) { + throw new Error(`HTTP Error, Status: ${response.status}`) + } + const updatedGrants: Grant[] = await response.json(); + // satchel store updated + fetchAllGrants(updatedGrants); + console.log(updatedGrants); + console.log("Successfully fetched grants"); + } catch (error) { + console.error("Error fetching grants:", error); + } + // local grant data updated +} +const ITEMS_PER_PAGE = 3 // Read the current page from our custom pagination context // and figure out which items to display. function GrantListView() { + const ALL_GRANTS = getAppStore().allGrants; const { page } = usePaginationContext() // figure out which grants to slice for the current page @@ -36,13 +59,23 @@ function GrantListView() { return (
{currentGrants.map((grant, index) => ( - + ))}
) } const GrantList: React.FC = () => { + + // since useEffect only changes what is in the satchel, it doesn't change any props, meaning + // the component doesn't rerender, so forceRefresh acts as a dummy prop. + const [,forceRefresh] = useState({}) + useEffect(() => { + fetchGrants().then(() => forceRefresh({})); + }, []); + + const ALL_GRANTS = getAppStore().allGrants; + // total number of pages const totalPages = Math.ceil(ALL_GRANTS.length / ITEMS_PER_PAGE) diff --git a/package.json b/package.json index a74207db..3e3a9b7d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "keywords": [], + "keywords": [ ], "author": "", "license": "ISC" } From e825de5b98426fddab72ffc245d9a572c572723b Mon Sep 17 00:00:00 2001 From: ryaken-nakamoto Date: Mon, 3 Feb 2025 21:05:11 -0500 Subject: [PATCH 2/6] fixed import issue --- frontend/src/grant-info/components/GrantList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/grant-info/components/GrantList.tsx b/frontend/src/grant-info/components/GrantList.tsx index 37dda6fb..bb4266e5 100644 --- a/frontend/src/grant-info/components/GrantList.tsx +++ b/frontend/src/grant-info/components/GrantList.tsx @@ -2,7 +2,7 @@ import GrantItem from "./GrantItem" import "./styles/GrantList.css" import {useEffect, useState} from "react" import { fetchAllGrants} from "../../external/bcanSatchel/actions.ts"; -import { Grant } from "../../external/bcanSatchel/actions.ts"; +import { Grant } from "../../external/bcanSatchel/store.ts"; import { getAppStore } from "../../external/bcanSatchel/store.ts"; import { From 048dfed77601e7e011bc66e7e4899a750e66b767 Mon Sep 17 00:00:00 2001 From: ryaken-nakamoto Date: Wed, 5 Feb 2025 17:06:28 -0500 Subject: [PATCH 3/6] code cleaned up --- .../src/grant-info/components/GrantItem.tsx | 2 +- .../src/grant-info/components/GrantList.tsx | 37 +++++++------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/frontend/src/grant-info/components/GrantItem.tsx b/frontend/src/grant-info/components/GrantItem.tsx index 8467d36d..db22ede5 100644 --- a/frontend/src/grant-info/components/GrantItem.tsx +++ b/frontend/src/grant-info/components/GrantItem.tsx @@ -16,7 +16,6 @@ const GrantItem: React.FC = ({grant}) => { const [isExpanded, setIsExpanded] = useState(false); const [isEditing, setIsEditing] = useState(false); const [curStatus, setCurStatus] = useState(grant.status); - // NOTE: ^^this is also a placeholder for generalStatus const toggleExpand = () => { @@ -28,6 +27,7 @@ const GrantItem: React.FC = ({grant}) => { const toggleEdit = async () => { if(isEditing) { // if you are saving try { + const response = await fetch('http://localhost:3001/grant/save/status', { method: 'PUT', headers: { diff --git a/frontend/src/grant-info/components/GrantList.tsx b/frontend/src/grant-info/components/GrantList.tsx index bb4266e5..f85e5569 100644 --- a/frontend/src/grant-info/components/GrantList.tsx +++ b/frontend/src/grant-info/components/GrantList.tsx @@ -1,9 +1,10 @@ import GrantItem from "./GrantItem" import "./styles/GrantList.css" -import {useEffect, useState} from "react" +import {useEffect} from "react" import { fetchAllGrants} from "../../external/bcanSatchel/actions.ts"; import { Grant } from "../../external/bcanSatchel/store.ts"; import { getAppStore } from "../../external/bcanSatchel/store.ts"; +import { observer } from 'mobx-react-lite'; import { PaginationRoot, @@ -16,16 +17,6 @@ import { import { usePaginationContext } from "@chakra-ui/react" - -// simulate a big list: -// const ALL_GRANTS = Array.from({ length: 11 }).map((_, i) => ({ -// grantName: `Community Development Grant #${i + 1}`, -// applicationDate: `2024-09-${(i % 30) + 1}`, -// generalStatus: i % 2 === 0 ? "Approved" : "Pending", -// amount: (i + 1) * 1000, -// restrictionStatus: i % 3 === 0 ? "Restricted" : "Unrestricted", -// })) - // How many items to show per page const fetchGrants = async () => { @@ -37,18 +28,18 @@ const fetchGrants = async () => { const updatedGrants: Grant[] = await response.json(); // satchel store updated fetchAllGrants(updatedGrants); - console.log(updatedGrants); - console.log("Successfully fetched grants"); } catch (error) { console.error("Error fetching grants:", error); } - // local grant data updated } const ITEMS_PER_PAGE = 3 + +interface GrantListViewProps { + ALL_GRANTS: Grant[]; +} // Read the current page from our custom pagination context // and figure out which items to display. -function GrantListView() { - const ALL_GRANTS = getAppStore().allGrants; +const GrantListView: React.FC = ({ALL_GRANTS}) => { const { page } = usePaginationContext() // figure out which grants to slice for the current page @@ -65,13 +56,11 @@ function GrantListView() { ) } -const GrantList: React.FC = () => { +const GrantList: React.FC = observer(() => { - // since useEffect only changes what is in the satchel, it doesn't change any props, meaning - // the component doesn't rerender, so forceRefresh acts as a dummy prop. - const [,forceRefresh] = useState({}) + // fetch grant immedietely upon loading the page useEffect(() => { - fetchGrants().then(() => forceRefresh({})); + fetchGrants(); }, []); const ALL_GRANTS = getAppStore().allGrants; @@ -81,7 +70,7 @@ const GrantList: React.FC = () => { return (
- {/* + {/* Wrap everything in PaginationRoot: - defaultPage can be 1 - totalPages is calculated @@ -101,10 +90,10 @@ const GrantList: React.FC = () => {
{/* Actual grants for the current page */} - +
) -} +}); export default GrantList From 6381dce1a8441d918aa81e25f639a26dff42d12d Mon Sep 17 00:00:00 2001 From: ryaken-nakamoto Date: Wed, 5 Feb 2025 17:08:46 -0500 Subject: [PATCH 4/6] minor spacing fix --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e3a9b7d..a74207db 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "keywords": [ ], + "keywords": [], "author": "", "license": "ISC" } From a3ba3fbc772500144044588c777d855031a56c6e Mon Sep 17 00:00:00 2001 From: ryaken-nakamoto Date: Sat, 15 Feb 2025 19:18:18 -0500 Subject: [PATCH 5/6] added full grant save functionality --- backend/src/grant/grant.controller.ts | 10 +- backend/src/grant/grant.service.ts | 45 ++-- .../grant-info/components/GrantAttributes.tsx | 213 +++++++++--------- .../grant-info/components/GrantDetails.tsx | 84 ++++--- .../src/grant-info/components/GrantItem.tsx | 30 ++- .../components/styles/GrantAttributes.css | 2 + .../components/styles/GrantDetails.css | 15 ++ .../components/styles/GrantItem.css | 15 +- 8 files changed, 221 insertions(+), 193 deletions(-) diff --git a/backend/src/grant/grant.controller.ts b/backend/src/grant/grant.controller.ts index fe809738..99672369 100644 --- a/backend/src/grant/grant.controller.ts +++ b/backend/src/grant/grant.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Param, Put, Body, Patch } from '@nestjs/common'; import { GrantService } from './grant.service'; +import {Grant} from "./grant.model"; @Controller('grant') export class GrantController { @@ -29,12 +30,9 @@ export class GrantController { return await this.grantService.unarchiveGrants(grantIds) } - @Put('save/status') - async saveStatus( - @Body('status') status: string - ) { - await this.grantService.updateGrant(1, 'status', status) - return { message: 'Status has been updated' }; + @Put('save') + async saveGrant(@Body() grantData: Grant) { + return await this.grantService.updateGrant(grantData) } diff --git a/backend/src/grant/grant.service.ts b/backend/src/grant/grant.service.ts index f51fbc03..e8b6eb00 100644 --- a/backend/src/grant/grant.service.ts +++ b/backend/src/grant/grant.service.ts @@ -82,36 +82,37 @@ export class GrantService { } /** - * Given primary key, attribute name, and the content to update, queries database to update - * that info. Returns true if operation was successful. Assumes inputs are valid. - * @param grantId - * @param attributeName - * @param newValue + * Will push or overwrite new grant data to database + * @param grantData */ - async updateGrant(grantId: number, attributeName: string, newValue: string): Promise { + async updateGrant(grantData: Grant): Promise { + // dynamically creates the update expression/attribute names based on names of grant interface + // assumption: grant interface field names are exactly the same as db storage naming + + const updateKeys = Object.keys(grantData).filter( + key => key != 'grantId' + ); + const UpdateExpression = "SET " + updateKeys.map((key) => `#${key} = :${key}`).join(", "); + const ExpressionAttributeNames = updateKeys.reduce((acc, key) => + ({ ...acc, [`#${key}`]: key }), {}); + const ExpressionAttributeValues = updateKeys.reduce((acc, key) => + ({ ...acc, [`:${key}`]: grantData[key as keyof typeof grantData] }), {}); + const params = { - TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', - Key: { - grantId: grantId - }, - UpdateExpression: `set #s = :newValue`, - ExpressionAttributeNames: { - '#s': attributeName, - }, - ExpressionAttributeValues: { - ":newValue": newValue, - }, + TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || "TABLE_FAILURE", + Key: { grantId: grantData.grantId }, + UpdateExpression, + ExpressionAttributeNames, + ExpressionAttributeValues, ReturnValues: "UPDATED_NEW", - } - console.log(params); + }; try { const result = await this.dynamoDb.update(params).promise(); - console.log(result); + return JSON.stringify(result); // returns the changed attributes stored in db } catch(err) { console.log(err); - throw new Error(`Failed to update Grant ${grantId} attribute - ${attributeName} with ${newValue}`); + throw new Error(`Failed to update Grant ${grantData.grantId}`) } } } \ No newline at end of file diff --git a/frontend/src/grant-info/components/GrantAttributes.tsx b/frontend/src/grant-info/components/GrantAttributes.tsx index b915cd0b..e6b18d2e 100644 --- a/frontend/src/grant-info/components/GrantAttributes.tsx +++ b/frontend/src/grant-info/components/GrantAttributes.tsx @@ -1,124 +1,113 @@ -import React, { useContext} from 'react'; +import React from 'react'; import './styles/GrantAttributes.css'; -import {StatusContext} from './StatusContext.tsx'; +import {Grant} from "@/external/bcanSatchel/store.ts"; interface GrantAttributesProps { isEditing: boolean; + curGrant: Grant; + setCurGrant: React.Dispatch>; } -export const GrantAttributes: React.FC = ({isEditing}) => { +export const GrantAttributes: React.FC = ({curGrant, setCurGrant, isEditing}) => { // placeholder for now before reworking, will remove redundant useState() - const { curStatus, setCurStatus } = useContext(StatusContext); const handleChange = (event: React.ChangeEvent) => { - setCurStatus(event.target.value); + const {name, value} = event.target; + + // only modifies the changed field + setCurGrant(curGrant => ({ + ...curGrant, + [name]: value + })); }; - return ( -
-
-
Status
- {isEditing ? ( - - ) : ( - - )} -
-
-
Does BCAN qualify?
- {isEditing ? ( - - ) : ( - - )} -
-
-
Deadline
- {isEditing ? ( - - ) : ( - - )} -
-
-
Notification Date
- {isEditing ? ( - - ) : ( - - )} -
-
-
Report Due:
- {isEditing ? ( - - ) : ( - - )} -
-
-
Estimated Completion Time:
- {isEditing ? ( - - ) : ( - - )} -
-
-
Scope Document:
- {isEditing ? ( - - ) : ( - - )} -
-
-
Grantmaker POC:
- {isEditing ? ( - - ) : ( - - )} -
-
-
Timeline:
- {isEditing ? ( - - ) : ( - - )} -
-
- ); + return ( +
+
+
Status
+ +
+
+
Does BCAN qualify?
+ +
+
+
Deadline
+ +
+
+
Notification Date
+ +
+
+
Report Due:
+ +
+
+
Estimated Completion Time:
+ +
+
+
Scope Document:
+ +
+
+
Grantmaker POC:
+ +
+
+
Timeline:
+ +
+
+ ); }; diff --git a/frontend/src/grant-info/components/GrantDetails.tsx b/frontend/src/grant-info/components/GrantDetails.tsx index 32f2fcc5..3f92c756 100644 --- a/frontend/src/grant-info/components/GrantDetails.tsx +++ b/frontend/src/grant-info/components/GrantDetails.tsx @@ -1,41 +1,55 @@ import React from 'react'; import './styles/GrantDetails.css'; +import {Grant} from "@/external/bcanSatchel/store.ts"; -const GrantDetails: React.FC = () => { - return ( -
-

Description

-

- The Community Development Initiative Grant is designed to empower - local organizations in implementing impactful projects that address - critical social and economic issues. This grant focuses on fostering - community-led programs that aim to improve educational opportunities, - enhance public health services, and support economic development within - underserved areas. Applicants are encouraged to outline how their proposed - projects will contribute to sustainable growth, promote equity, and engage - local stakeholders. -

- -

Application Requirements

-

- Eligible programs include those that offer job training and workforce development, - youth mentorship, health and wellness programs, and initiatives aimed at reducing - environmental impacts. Each application should include a detailed plan that highlights - the project’s goals, implementation strategies, and measurable outcomes. Projects that - demonstrate strong community involvement and partnerships with other local organizations - will be prioritized for funding. -

- -

Additional Notes

-

- Funding for this grant may cover program expenses such as staffing, equipment, training - materials, and outreach activities. The review committee seeks innovative and sustainable - approaches that align with the mission of strengthening communities and fostering long-term - positive change. Grant recipients will also be expected to submit periodic reports outlining - the progress and achievements of their projects over the funding period. -

-
- ); +interface GrantDetailsProps { + isEditing: boolean; + curGrant: Grant; + setCurGrant: React.Dispatch>; +} + +const GrantDetails: React.FC = ({isEditing, curGrant, setCurGrant}) => { + + + const handleChange = (event: React.ChangeEvent) => { + const {name, value} = event.target; + + // only modifies the changed field + setCurGrant(curGrant =>({ + ...curGrant, + [name]: value + })); + }; + + + return ( +
+

Description

+