Skip to content
15 changes: 14 additions & 1 deletion backend/src/grant/grant.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Get, Param, Put, Body } from '@nestjs/common';
import { Controller, Get, Param, Put, Body, Patch } from '@nestjs/common';
import { GrantService } from './grant.service';

@Controller('grant')
Expand All @@ -10,6 +10,8 @@ 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));
Expand All @@ -29,4 +31,15 @@ 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' };
}




}
4 changes: 3 additions & 1 deletion backend/src/grant/grant.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export interface Grant {
attached_resources: string[];
comments: string[];
isArchived : boolean;
}
}

// TODO: [JAN-13} Switch deadline to Proper "Date Time"
35 changes: 35 additions & 0 deletions backend/src/grant/grant.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class GrantService {

// function to retrieve all grants in our database
async getAllGrants(): Promise<Grant[]> {
// loads in the environment variable for the table now
const params = {
TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE',
};
Expand Down Expand Up @@ -79,4 +80,38 @@ export class GrantService {
};
return successfulUpdates;
}

/**
* 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
*/
async updateGrant(grantId: number, attributeName: string, newValue: string): Promise<void> {
const params = {
TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE',
Key: {
grantId: grantId
},
UpdateExpression: `set #s = :newValue`,
ExpressionAttributeNames: {
'#s': attributeName,
},
ExpressionAttributeValues: {
":newValue": newValue,
},
ReturnValues: "UPDATED_NEW",
}
console.log(params);

try {
const result = await this.dynamoDb.update(params).promise();
console.log(result);
} catch(err) {
console.log(err);
throw new Error(`Failed to update Grant ${grantId} attribute
${attributeName} with ${newValue}`);
}
}
}
5 changes: 4 additions & 1 deletion backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ async function bootstrap() {
region: process.env.AWS_REGION,
accessKeyId: process.env.OPEN_HATCH,
secretAccessKey: process.env.CLOSED_HATCH
});
});

const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3001);
}
dotenv.config();
bootstrap();


9 changes: 5 additions & 4 deletions backend/src/notifications/notification.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
import { NotificationController } from './notification.controller';
import { NotificationService } from './notifcation.service';


@Module({
providers: [NotificationService],
controllers: [NotificationController],
exports: [NotificationService],
providers: [NotificationService], // providers perform business logic
controllers: [NotificationController], // controllers directly take in http requests
// and are the starting point for anything happening
exports: [NotificationService], // by putting it under exports, this service is available
// to other modules outside of this
})
export class NotificationsModule {}
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8"
}
}
}
38 changes: 1 addition & 37 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
@@ -1,45 +1,9 @@
#root {
max-width: 1280px;
max-width: 100%;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}

.card {
padding: 2em;
}

.read-the-docs {
color: #888;
}

.app-container {
display: flex;
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/grant-info/components/GrantAttributes.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import React from 'react';
import React, { useContext} from 'react';
import './styles/GrantAttributes.css';
import {StatusContext} from './StatusContext.tsx';
interface GrantAttributesProps {
isEditing: boolean;
}

export const GrantAttributes: React.FC<GrantAttributesProps> = ({isEditing}) => {

// placeholder for now before reworking, will remove redundant useState()
const { curStatus, setCurStatus } = useContext(StatusContext);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCurStatus(event.target.value);
};

return (
<div className="grant-attributes">
<div className="attribute-row">
<div className="attribute-label">Status</div>
{isEditing ? (
<input
type="text"
type="text" /*This is also a place holder for updating the status*/
className="attribute-value"
value={curStatus}
onChange={handleChange}
/>
) : (
<span className="attribute-value"></span>
Expand Down
63 changes: 56 additions & 7 deletions frontend/src/grant-info/components/GrantItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, {useState} from 'react';
import React, {useState, useEffect } from 'react';
import './styles/GrantItem.css';
import { GrantAttributes } from './GrantAttributes';
import GrantDetails from './GrantDetails';
import {StatusContext} from './StatusContext';

// TODO: [JAN-14] Make uneditable field editable (ex: Description, Application Reqs, Additional Notes)
interface GrantItemProps {
grantName: string;
applicationDate: string;
Expand All @@ -11,24 +13,69 @@ interface GrantItemProps {
restrictionStatus: string;
}
const GrantItem: React.FC<GrantItemProps> = (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);

// 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);
};
const toggleEdit = () => setIsEditing((prev) => !prev);
// when toggleEdit gets saved, then updates the backend to update itself with whatever
// is shown in the front-end

return (
const toggleEdit = async () => {
if(isEditing) { // if you are saving
try {
const response = await fetch('http://localhost:3001/grant/save/status', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({status: curStatus}),
});
const result = await response.json();
console.log(result);
} catch(err) {
console.error('Error saving data:', err);
}
// holding result for now
}
setIsEditing(!isEditing);
};

return (
// class name with either be grant-item or grant-item-expanded
<div className='grant-item-wrapper'>
<ul className={`grant-summary ${isExpanded ? 'expanded' : ''}`} onClick={toggleExpand}>
<li className="grant-name">{grantName}</li>
<li className="application-date">{applicationDate}</li>
<li className="status">{generalStatus}</li>
<li className="status">{curStatus}</li> {/*This is replacing generalStatus for now*/}
<li className="amount">${amount}</li>
<li className="restriction-status">{restrictionStatus}</li>
</ul>
Expand All @@ -37,8 +84,10 @@ const GrantItem: React.FC<GrantItemProps> = (props) => {
<div className="grant-description">
<h2>Community Development Initiative Grant</h2>
<div className = 'grant-content'>
<GrantAttributes isEditing={isEditing} />
<GrantDetails/>
<StatusContext.Provider value={{curStatus, setCurStatus}}>
<GrantAttributes isEditing={isEditing} />
<GrantDetails/>
</StatusContext.Provider>
</div>
<div className="bottom-buttons">
<button className="done-button" onClick={toggleEdit}>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/grant-info/components/GrantList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const GrantList: React.FC = () => {
- totalPages is calculated
*/}
<PaginationRoot defaultPage={1} count={totalPages}>
{/*
{/*
Paging Controls:
- Prev / Next triggers
- Individual page items
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/grant-info/components/StatusContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// will later be renamed to "GrantContext" when front/backend schemas are aligned
import { createContext } from "react";

// Define the shape of the context
interface StatusContextType {
curStatus: string;
setCurStatus: React.Dispatch<React.SetStateAction<string>>;
}


export const StatusContext = createContext<StatusContextType>({
curStatus: "",
setCurStatus: () => {}, // No-op function as default
});
1 change: 1 addition & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ button {
cursor: pointer;
transition: border-color 0.25s;
}

button:hover {
border-color: #646cff;
}
Expand Down
Loading