-
Notifications
You must be signed in to change notification settings - Fork 0
wire frontend to real APIs #303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
cb5f0dc
9fcf41b
45eaa69
6ee81df
3c06214
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| 'use client'; | ||
| "use client"; | ||
|
|
||
| import React from 'react'; | ||
| import React, { useEffect, useState } from 'react'; | ||
| import { useApi } from '@/hooks/useApi'; | ||
| import StaffCard from '../components/StaffCard'; | ||
| import { User } from '@/types'; | ||
|
|
||
|
|
@@ -28,18 +29,44 @@ export const teamMembers = mockUsers.filter(u => !u.is_admin); | |
|
|
||
|
|
||
| export default function AccountsPage() { | ||
| const api = useApi(); | ||
| const [users, setUsers] = useState<User[]>([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| async function fetchUsers() { | ||
| try { | ||
| const json = await api.get<User[] | { data: User[] }>('http://localhost:3001/users'); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this response shape may be wrong since user can be paginated |
||
| const list = Array.isArray(json) ? json : (json && 'data' in json ? json.data : []); | ||
| setUsers(list); | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : 'Failed to load users'); | ||
| setUsers([]); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| fetchUsers(); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, []); | ||
|
|
||
| const shownFacilitation = users.length ? users.filter(u => u.is_admin) : facilitationTeam; | ||
| const shownTeam = users.length ? users.filter(u => !u.is_admin) : teamMembers; | ||
| return ( | ||
| <div className="!p-6"> | ||
| <h1 className="![font-family:var(--font-heading)] !text-[length:var(--font-size-heading-1)] !font-semibold">Accounts</h1> | ||
| <h3 className="![font-family:var(--font-heading)] !text-[length:var(--font-size-heading-3)] !font-semibold">Core BRANCH Facilitation Team</h3> | ||
| <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 !pt-3 !pb-7"> | ||
| {facilitationTeam.map(user => ( | ||
| {loading && <p>Loading users...</p>} | ||
| {error && <p style={{ color: 'var(--color-error-red)' }}>{error}</p>} | ||
| {!loading && !error && shownFacilitation.map(user => ( | ||
| <StaffCard key={user.user_id} name={user.name} email={user.email} /> | ||
| ))} | ||
| </div> | ||
| <h3 className="![font-family:var(--font-heading)] !text-[length:var(--font-size-heading-3)] !font-semibold">BRANCH Team Members</h3> | ||
| <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 !pt-3 !pb-7"> | ||
| {teamMembers.map(user => ( | ||
| {!loading && !error && shownTeam.map(user => ( | ||
| <StaffCard key={user.user_id} name={user.name} email={user.email} /> | ||
| ))} | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| 'use client' | ||
| import React, { useState } from 'react'; | ||
| import React, { useEffect, useState } from 'react'; | ||
| import NavBar from "../components/Navbar"; | ||
| import { HStack, Input, Button, Table, Dialog, Portal, CloseButton, Stack } from "@chakra-ui/react"; | ||
| import TextInputField from '../components/TextInputField'; | ||
|
|
@@ -14,29 +14,79 @@ type Donation = { | |
| project_name: string; | ||
| amount: number; | ||
| }; | ||
| import { useApi } from '@/hooks/useApi'; | ||
|
|
||
| const mockDonors = ['Green Future Foundation', 'Horizon Trust', 'Bright Path Nonprofit', 'Unity Giving Circle', 'Sunrise Community Fund']; | ||
| const mockProjects = ['Clean Water Initiative', 'Youth Mentorship Program', 'Food Security Drive', 'Urban Garden Project', 'STEM Education Fund']; | ||
| const donorsBase = 'http://localhost:3003'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hard coding this will give invalid URL. we set a global base url that is either localhost or the api gateway url, so you should just be able to append to it |
||
| const projectsBase = 'http://localhost:3002'; | ||
|
|
||
| const mockDonations: Donation[] = [ | ||
| { donor_id: 1, date: '03/12/2024', project_name: 'Clean Water Initiative', amount: 5000 }, | ||
| { donor_id: 2, date: '01/05/2024', project_name: 'Youth Mentorship Program', amount: 12000 }, | ||
| { donor_id: 3, date: '02/28/2024', project_name: 'Food Security Drive', amount: 750 }, | ||
| { donor_id: 4, date: '03/30/2024', project_name: 'Urban Garden Project', amount: 3200 }, | ||
| { donor_id: 5, date: '04/01/2024', project_name: 'STEM Education Fund', amount: 8500 }, | ||
| { donor_id: 6, date: '02/14/2024', project_name: 'Shelter Renovation', amount: 1500 }, | ||
| { donor_id: 7, date: '01/20/2024', project_name: 'Mental Health Outreach', amount: 20000 }, | ||
| { donor_id: 8, date: '03/05/2024', project_name: 'Digital Literacy Program', amount: 9750 }, | ||
| { donor_id: 9, date: '04/10/2024', project_name: 'Community Health Fair', amount: 4300 }, | ||
| { donor_id: 10, date: '03/22/2024', project_name: 'After-School Arts', amount: 600 }, | ||
| ]; | ||
|
|
||
| export default function DonationsPage() { | ||
| const [currentPage, setCurrentPage] = useState(1); | ||
| const rowsPerPage = 10; | ||
|
|
||
| const totalPages = Math.ceil(mockDonations.length / rowsPerPage); | ||
| const currentDonations = mockDonations.slice( | ||
| const api = useApi(); | ||
| const [donations, setDonations] = useState<Donation[]>([]); | ||
| const [donorNames, setDonorNames] = useState<string[]>([]); | ||
| const [projectNames, setProjectNames] = useState<string[]>([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| async function loadAll() { | ||
| try { | ||
| const [donationsJson, donorsJson, projectsJson] = await Promise.all([ | ||
| api.get<Donation[] | { data: Donation[] }>(`${donorsBase}/donations`), | ||
| api.get<string[] | { data: unknown[] }>(`${donorsBase}/donors`), | ||
| api.get<string[] | { data: unknown[] }>(`${projectsBase}/projects`), | ||
| ]); | ||
|
|
||
| const dList = Array.isArray(donationsJson) ? donationsJson : (donationsJson && 'data' in donationsJson ? donationsJson.data : []); | ||
| const dn = Array.isArray(donorsJson) ? donorsJson : (donorsJson && 'data' in donorsJson ? donorsJson.data : []); | ||
| const pn = Array.isArray(projectsJson) ? projectsJson : (projectsJson && 'data' in projectsJson ? projectsJson.data : []); | ||
|
|
||
| setDonations(dList); | ||
| // donors API may return objects; if so map to organization names | ||
| const dnArray: unknown[] = dn as unknown[]; | ||
| const donorNamesMapped = dnArray | ||
| .map((d) => { | ||
| if (typeof d === 'string') return d; | ||
| if (d && typeof d === 'object' && 'organization' in d) { | ||
| const maybeOrg = (d as { [key: string]: unknown })['organization']; | ||
| if (typeof maybeOrg === 'string') return maybeOrg; | ||
| } | ||
| return ''; | ||
| }) | ||
| .filter((s): s is string => Boolean(s)); | ||
|
|
||
| const pnArray: unknown[] = pn as unknown[]; | ||
| const projectNamesMapped = pnArray | ||
| .map((p) => { | ||
| if (typeof p === 'string') return p; | ||
| if (p && typeof p === 'object' && 'name' in p) { | ||
| const maybeName = (p as { [key: string]: unknown })['name']; | ||
| if (typeof maybeName === 'string') return maybeName; | ||
| } | ||
| return ''; | ||
| }) | ||
| .filter((s): s is string => Boolean(s)); | ||
|
|
||
| setDonorNames(donorNamesMapped); | ||
| setProjectNames(projectNamesMapped); | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : 'Failed to load donations data'); | ||
| setDonations([]); | ||
| setDonorNames([]); | ||
| setProjectNames([]); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| loadAll(); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, []); | ||
|
|
||
| const totalPages = Math.max(1, Math.ceil(donations.length / rowsPerPage)); | ||
| const currentDonations = donations.slice( | ||
| (currentPage - 1) * rowsPerPage, | ||
| currentPage * rowsPerPage | ||
| ); | ||
|
|
@@ -104,7 +154,7 @@ export default function DonationsPage() { | |
| {showFilter && ( | ||
| <div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 10 }}> | ||
| <DropdownSelector | ||
| options={mockDonors} | ||
| options={donorNames} | ||
| placeholder="Filter by donor..." | ||
| multiSelect={true} | ||
| value={selectedDonor} | ||
|
|
@@ -126,13 +176,13 @@ export default function DonationsPage() { | |
| </Button> | ||
| {showSort && ( | ||
| <div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 10 }}> | ||
| <DropdownSelector | ||
| options={sortOptions} | ||
| placeholder="Sort by..." | ||
| multiSelect={false} | ||
| value={selectedSort} | ||
| onChange={(val: string | string[]) => setSelectedSort(val as string)} | ||
| /> | ||
| <DropdownSelector | ||
| options={sortOptions} | ||
| placeholder="Sort by..." | ||
| multiSelect={false} | ||
| value={selectedSort} | ||
| onChange={(val: string | string[]) => setSelectedSort(val as string)} | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
@@ -174,15 +224,15 @@ export default function DonationsPage() { | |
| {dateError && <span style={{ color: 'red', fontSize: '12px' }}>Enter a valid date</span>} | ||
| </div> | ||
| <DropdownSelector | ||
| options={mockDonors} | ||
| options={donorNames} | ||
| placeholder="Select a donor" | ||
| multiSelect={false} | ||
| value={newDonor} | ||
| onChange={(val: string | string[]) => { setNewDonor(val as string); setDonorError(false); }} | ||
| /> | ||
| {donorError && <span style={{ color: 'red', fontSize: '12px' }}>Select a donor</span>} | ||
| <DropdownSelector | ||
| options={mockProjects} | ||
| options={projectNames} | ||
| placeholder="Select a project" | ||
| multiSelect={false} | ||
| value={newProject} | ||
|
|
@@ -208,6 +258,9 @@ export default function DonationsPage() { | |
| </Portal> | ||
| </Dialog.Root> | ||
|
|
||
| {loading && <p>Loading donations...</p>} | ||
| {error && <p style={{ color: 'var(--color-error-red)' }}>{error}</p>} | ||
| {!loading && !error && ( | ||
| <Table.Root> | ||
| <Table.ColumnGroup> | ||
| <Table.Column width="15%" /> | ||
|
|
@@ -234,6 +287,7 @@ export default function DonationsPage() { | |
| ))} | ||
| </Table.Body> | ||
| </Table.Root> | ||
| )} | ||
|
|
||
| <div style={{ marginTop: 'auto' }}> | ||
| <HStack width="100%" justify="center" paddingTop="3%" paddingBottom="3%" gap="6"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| 'use client' | ||
| import React, { useState } from 'react'; | ||
| import React, { useEffect, useState } from 'react'; | ||
| import NavBar from "../components/Navbar"; | ||
| import { HStack, Input, Button, Table, Dialog, Portal, CloseButton, Stack } from "@chakra-ui/react"; | ||
| import TextInputField from '../components/TextInputField'; | ||
|
|
@@ -16,26 +16,40 @@ type Donor = { | |
| num_projects: number; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think this needs to be updated. i dont think we give num_projects |
||
| last_donation: string | null; | ||
| }; | ||
| import { useApi } from '@/hooks/useApi'; | ||
|
|
||
| // fetched from API | ||
| const apiBase = 'http://localhost:3003'; | ||
|
|
||
| const mockDonors: Donor[] = [ | ||
| { donor_id: 1, organization: 'Green Future Foundation', contact_name: 'Alice Chen', contact_email: 'alice@greenfuture.org', num_projects: 4, last_donation: '03/12/2024' }, | ||
| { donor_id: 2, organization: 'Horizon Trust', contact_name: 'James Patel', contact_email: 'james@horizontrust.org', num_projects: 2, last_donation: '01/05/2024' }, | ||
| { donor_id: 3, organization: 'Bright Path Nonprofit', contact_name: null, contact_email: null, num_projects: 7, last_donation: '02/28/2024' }, | ||
| { donor_id: 4, organization: 'Unity Giving Circle', contact_name: 'Maria Lopez', contact_email: 'maria@unitygiving.org', num_projects: 1, last_donation: '03/30/2024' }, | ||
| { donor_id: 5, organization: 'Sunrise Community Fund', contact_name: 'David Kim', contact_email: 'david@sunrisefund.org', num_projects: 3, last_donation: '04/01/2024' }, | ||
| { donor_id: 6, organization: 'Blue Ridge Giving', contact_name: 'Sarah Thompson', contact_email: 'sarah@blueridge.org', num_projects: 5, last_donation: '02/14/2024' }, | ||
| { donor_id: 7, organization: 'Maple Leaf Charitable Trust', contact_name: null, contact_email: null, num_projects: 2, last_donation: '01/20/2024' }, | ||
| { donor_id: 8, organization: 'Evergreen Partners', contact_name: 'Rachel Singh', contact_email: 'rachel@evergreenpartners.org', num_projects: 6, last_donation: '03/05/2024' }, | ||
| { donor_id: 9, organization: 'New Horizons Society', contact_name: 'Tom Bradley', contact_email: 'tom@newhorizons.org', num_projects: 9, last_donation: '04/10/2024' }, | ||
| { donor_id: 10, organization: 'Coastal Care Foundation', contact_name: 'Nina Rossi', contact_email: 'nina@coastalcare.org', num_projects: 3, last_donation: '03/22/2024' }, | ||
| ]; | ||
|
|
||
| export default function DonorsPage() { | ||
| const [currentPage, setCurrentPage] = useState(1); | ||
| const rowsPerPage = 10; | ||
|
|
||
| const totalPages = Math.ceil(mockDonors.length / rowsPerPage); | ||
| const currentDonors = mockDonors.slice( | ||
| const api = useApi(); | ||
| const [donors, setDonors] = useState<Donor[]>([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| async function fetchDonors() { | ||
| try { | ||
| const json = await api.get<Donor[] | { data: Donor[] }>(`${apiBase}/donors`); | ||
| const list = Array.isArray(json) ? json : (json && 'data' in json ? json.data : []); | ||
| setDonors(list); | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : 'Failed to load donors'); | ||
| setDonors([]); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| fetchDonors(); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, []); | ||
|
|
||
| const totalPages = Math.max(1, Math.ceil(donors.length / rowsPerPage)); | ||
| const currentDonors = donors.slice( | ||
| (currentPage - 1) * rowsPerPage, | ||
| currentPage * rowsPerPage | ||
| ); | ||
|
|
@@ -49,7 +63,7 @@ export default function DonorsPage() { | |
|
|
||
| const [showFilter, setShowFilter] = useState(false); | ||
| const [selectedDonor, setSelectedDonor] = useState<string>(''); | ||
| const donorNames = mockDonors.map(d => d.organization); | ||
| const donorNames = donors.map(d => d.organization); | ||
|
|
||
| const [showSort, setShowSort] = useState(false); | ||
| const [selectedSort, setSelectedSort] = useState<string>(''); | ||
|
|
@@ -186,6 +200,9 @@ export default function DonorsPage() { | |
| </Portal> | ||
| </Dialog.Root> | ||
|
|
||
| {loading && <p>Loading donors...</p>} | ||
| {error && <p style={{ color: 'var(--color-error-red)' }}>{error}</p>} | ||
| {!loading && !error && ( | ||
| <Table.Root> | ||
| <Table.ColumnGroup> | ||
| <Table.Column width="15%" /> | ||
|
|
@@ -212,6 +229,7 @@ export default function DonorsPage() { | |
| ))} | ||
| </Table.Body> | ||
| </Table.Root> | ||
| )} | ||
|
|
||
| <div style={{ marginTop: 'auto' }}> | ||
| <HStack width="100%" justify="center" paddingTop="3%" paddingBottom="3%" gap="6"> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd want to put this fetch in a custom hook