From 57ba5093a4cefd837409575cd63277bfb1dca83b Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Mon, 11 May 2026 16:54:21 -0400 Subject: [PATCH 1/8] chore: bump need4deed-sdk to 0.0.82 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0c4307dc..21fef525 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "date-fns": "^4.1.0", "email-validator": "^2.0.4", "i18next": "^25.3.2", - "need4deed-sdk": "^0.0.81", + "need4deed-sdk": "^0.0.82", "next": "15.3.8", "react": "^19.0.0", "react-day-picker": "^9.13.0", diff --git a/yarn.lock b/yarn.lock index 0b48f4cb..177cc3b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2407,10 +2407,10 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -need4deed-sdk@^0.0.81: - version "0.0.81" - resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.81.tgz#30ae25f87898567ed3afdf7da93db09dc5eb8aec" - integrity sha512-rrjFSEavw4oSVCiIKKNwGwyagoDU6E4FG61RShz2trgnoDas7rZ9q7IzpkdSkSHTMSHPFz3W1mn4J15WBKOGnQ== +need4deed-sdk@^0.0.82: + version "0.0.82" + resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.82.tgz#a1e85c26bc496fffc1d9fbdf3bef0ab211919ff5" + integrity sha512-Wqt/p62QY3AAzkGWAvE7NG/0XQ/lC8rrXKTI2OHRT7NIsrENtT01EQ+lKXnnQQOUQT82COTR62KAe16PQ8FqpA== next@15.3.8: version "15.3.8" From 52755161d48b88c8b8ad1fafcdcbe3d9b25ae24e Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Mon, 11 May 2026 19:32:26 -0400 Subject: [PATCH 2/8] add generic EntityTableList component and ViewMode type --- .../EntityTableList/EntityTableList.tsx | 40 +++++++++++++++++++ .../Dashboard/common/EntityTableList/index.ts | 2 + .../common/EntityTableList/styles.ts | 8 ++++ .../Dashboard/common/EntityTableList/types.ts | 18 +++++++++ src/components/Dashboard/common/types.ts | 1 + 5 files changed, 69 insertions(+) create mode 100644 src/components/Dashboard/common/EntityTableList/EntityTableList.tsx create mode 100644 src/components/Dashboard/common/EntityTableList/index.ts create mode 100644 src/components/Dashboard/common/EntityTableList/styles.ts create mode 100644 src/components/Dashboard/common/EntityTableList/types.ts create mode 100644 src/components/Dashboard/common/types.ts diff --git a/src/components/Dashboard/common/EntityTableList/EntityTableList.tsx b/src/components/Dashboard/common/EntityTableList/EntityTableList.tsx new file mode 100644 index 00000000..95a2f280 --- /dev/null +++ b/src/components/Dashboard/common/EntityTableList/EntityTableList.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { Table, TableBody, TableContainer, TableHeader, TableHeaderCell } from "@/components/core/common/Table"; +import { Wrapper } from "./styles"; +import { EntityTableListProps } from "./types"; +import PaginationNumbers from "@/components/core/paginatedGrid/PaginationNumbers"; + +export function EntityTableList({ + columns, + data, + renderRow, + count, + itemsPerPage, + currentPage, + setCurrentPage, + testIdPrefix, +}: EntityTableListProps) { + const totalPages = Math.ceil(count / itemsPerPage); + const goToPage = (page: number) => { + if (page > 0 && page <= totalPages) setCurrentPage(page); + }; + + return ( + + + + + {columns.map((col) => ( + + {col.label} + + ))} + + {data.map((item, index) => renderRow(item, index === data.length - 1))} +
+
+ +
+ ); +} diff --git a/src/components/Dashboard/common/EntityTableList/index.ts b/src/components/Dashboard/common/EntityTableList/index.ts new file mode 100644 index 00000000..ce8338a7 --- /dev/null +++ b/src/components/Dashboard/common/EntityTableList/index.ts @@ -0,0 +1,2 @@ +export * from "./EntityTableList"; +export type { Column, EntityTableListProps } from "./types"; diff --git a/src/components/Dashboard/common/EntityTableList/styles.ts b/src/components/Dashboard/common/EntityTableList/styles.ts new file mode 100644 index 00000000..07784d11 --- /dev/null +++ b/src/components/Dashboard/common/EntityTableList/styles.ts @@ -0,0 +1,8 @@ +import styled from "styled-components"; + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: var(--opportunities-container-gap); + width: 100%; +`; diff --git a/src/components/Dashboard/common/EntityTableList/types.ts b/src/components/Dashboard/common/EntityTableList/types.ts new file mode 100644 index 00000000..ff45ec60 --- /dev/null +++ b/src/components/Dashboard/common/EntityTableList/types.ts @@ -0,0 +1,18 @@ +import { ReactNode } from "react"; + +export interface Column { + key: string; + label: string; + width?: string; +} + +export interface EntityTableListProps { + columns: Column[]; + data: T[]; + renderRow: (item: T, isLast: boolean) => ReactNode; + count: number; + itemsPerPage: number; + currentPage: number; + setCurrentPage: (page: number) => void; + testIdPrefix: string; +} diff --git a/src/components/Dashboard/common/types.ts b/src/components/Dashboard/common/types.ts new file mode 100644 index 00000000..ce039a07 --- /dev/null +++ b/src/components/Dashboard/common/types.ts @@ -0,0 +1 @@ +export type ViewMode = "cards" | "list" | "map"; From 32dbee5a5128d7c7e85bb22a5763335b41a03ba6 Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Mon, 11 May 2026 20:29:08 -0400 Subject: [PATCH 3/8] migrate VolunteerTableList to use EntityTableList --- .../Volunteers/VolunteerTableList.tsx | 68 +++++++------------ .../Volunteers/VolunteerTableRow.tsx | 2 +- .../Volunteers/volunteerTableColumns.ts | 12 ++++ 3 files changed, 36 insertions(+), 46 deletions(-) create mode 100644 src/components/Dashboard/Volunteers/volunteerTableColumns.ts diff --git a/src/components/Dashboard/Volunteers/VolunteerTableList.tsx b/src/components/Dashboard/Volunteers/VolunteerTableList.tsx index fd8d90b7..a7874e19 100644 --- a/src/components/Dashboard/Volunteers/VolunteerTableList.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerTableList.tsx @@ -1,16 +1,15 @@ "use client"; -import { ApiVolunteerGetList } from "need4deed-sdk"; +import type { ApiVolunteerGetList } from "need4deed-sdk"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import styled from "styled-components"; import { createEngagementStatusLabelMap, createStatusLabelMap, } from "@/components/Dashboard/Profile/sections/VolunteerAgents/types"; -import { Table, TableBody, TableContainer, TableHeader, TableHeaderCell } from "@/components/core/common/Table"; -import PaginationNumbers from "@/components/core/paginatedGrid/PaginationNumbers"; +import { createVolunteerTableColumns } from "./volunteerTableColumns"; import { VolunteerTableRow } from "./VolunteerTableRow"; +import { EntityTableList } from "../common/EntityTableList"; interface TableListProps { volunteers: ApiVolunteerGetList[]; @@ -30,50 +29,29 @@ export function VolunteerTableList({ opportunityId, }: TableListProps) { const { t } = useTranslation(); - const totalPages = Math.ceil(count / itemsPerPage); - const engagementLabels = useMemo(() => createEngagementStatusLabelMap(t), [t]); const typeLabels = useMemo(() => createStatusLabelMap(t), [t]); - - const goToPage = (page: number) => { - if (page > 0 && page <= totalPages) setCurrentPage(page); - }; + const columns = useMemo(() => createVolunteerTableColumns(t), [t]); return ( - - - - - {t("dashboard.volunteers.table.name")} - {t("dashboard.volunteers.table.type")} - {t("dashboard.volunteers.table.engagementStatus")} - {t("dashboard.volunteers.table.matchingStatus")} - {t("dashboard.volunteers.table.language")} - {t("dashboard.volunteers.table.district")} - {t("dashboard.volunteers.table.email")} - - - {volunteers.map((volunteer, index) => ( - - ))} - -
-
- -
+ ( + + )} + count={count} + itemsPerPage={itemsPerPage} + currentPage={currentPage} + setCurrentPage={setCurrentPage} + testIdPrefix="volunteers" + /> ); } - -const Wrapper = styled.div` - display: flex; - flex-direction: column; - gap: var(--opportunities-container-gap); - width: 100%; -`; diff --git a/src/components/Dashboard/Volunteers/VolunteerTableRow.tsx b/src/components/Dashboard/Volunteers/VolunteerTableRow.tsx index 268dcaf1..56cf7bfa 100644 --- a/src/components/Dashboard/Volunteers/VolunteerTableRow.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerTableRow.tsx @@ -1,6 +1,6 @@ "use client"; -import { ApiVolunteerGetList } from "need4deed-sdk"; +import type { ApiVolunteerGetList } from "need4deed-sdk"; import { useRouter } from "next/navigation"; import { useTranslation } from "react-i18next"; import styled from "styled-components"; diff --git a/src/components/Dashboard/Volunteers/volunteerTableColumns.ts b/src/components/Dashboard/Volunteers/volunteerTableColumns.ts new file mode 100644 index 00000000..b60a07f2 --- /dev/null +++ b/src/components/Dashboard/Volunteers/volunteerTableColumns.ts @@ -0,0 +1,12 @@ +import { TFunction } from "i18next"; +import { Column } from "../common/EntityTableList"; + +export const createVolunteerTableColumns = (t: TFunction): Column[] => [ + { key: "name", label: t("dashboard.volunteers.table.name") }, + { key: "type", label: t("dashboard.volunteers.table.type"), width: "180px" }, + { key: "engagement", label: t("dashboard.volunteers.table.engagementStatus"), width: "200px" }, + { key: "matching", label: t("dashboard.volunteers.table.matchingStatus"), width: "140px" }, + { key: "language", label: t("dashboard.volunteers.table.language"), width: "180px" }, + { key: "district", label: t("dashboard.volunteers.table.district"), width: "200px" }, + { key: "email", label: t("dashboard.volunteers.table.email") }, +]; From a6bb9b98d6a6473d24d8d059ad75477b7eef12ab Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Mon, 11 May 2026 20:47:10 -0400 Subject: [PATCH 4/8] replace selectedTabIndex with viewMode prop on VolunteerListController --- .../Dashboard/Volunteers/VolunteerListController.tsx | 11 +++++------ src/components/Dashboard/Volunteers/Volunteers.tsx | 5 +++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/Dashboard/Volunteers/VolunteerListController.tsx b/src/components/Dashboard/Volunteers/VolunteerListController.tsx index 6451485f..6796e5d8 100644 --- a/src/components/Dashboard/Volunteers/VolunteerListController.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerListController.tsx @@ -7,11 +7,11 @@ import { CardsFilter } from "./Filters/types"; import { serializeFilters } from "./helpers"; import { VolunteerCardList } from "./VolunteerCardList"; // We will modify this component import { VolunteerTableList } from "./VolunteerTableList"; +import { ViewMode } from "../common/types"; const CARD_COLUMNS = 3; const CARD_ROWS = 3; const CARD_LIMIT = CARD_COLUMNS * CARD_ROWS; -const LIST_TAB_INDEX = 0; interface VolunteerListControllerProps { setNumOfVols: (numOfVols: number) => void; @@ -20,7 +20,7 @@ interface VolunteerListControllerProps { filter: CardsFilter; apiFilterOptions?: ApiOptionLists; opportunityId?: string; - selectedTabIndex: number; + viewMode: ViewMode; } export function VolunteerListController({ @@ -30,10 +30,9 @@ export function VolunteerListController({ filter, apiFilterOptions, opportunityId, - selectedTabIndex, + viewMode, }: VolunteerListControllerProps) { - const isListView = selectedTabIndex === LIST_TAB_INDEX; - const limit = isListView ? TABLE_LIMIT : CARD_LIMIT; + const limit = viewMode === "list" ? TABLE_LIMIT : CARD_LIMIT; const { currentPage, setCurrentPage } = usePageParam(); const serializedFilter = serializeFilters(filter, undefined, false, { serializeToIDs: true, @@ -61,7 +60,7 @@ export function VolunteerListController({ setNumOfVols(count); }, [count, setNumOfVols]); - if (isListView) { + if (viewMode === "list") { return ( Date: Tue, 12 May 2026 07:38:27 -0400 Subject: [PATCH 5/8] centralize volunteer column widths --- .../Volunteers/VolunteerTableRow.tsx | 11 +++++----- .../Volunteers/volunteerTableColumns.ts | 22 ++++++++++++++----- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/components/Dashboard/Volunteers/VolunteerTableRow.tsx b/src/components/Dashboard/Volunteers/VolunteerTableRow.tsx index 56cf7bfa..959d886e 100644 --- a/src/components/Dashboard/Volunteers/VolunteerTableRow.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerTableRow.tsx @@ -12,6 +12,7 @@ import { TableCell, TableRow } from "@/components/core/common/Table"; import { CirclePic } from "@/components/styled/img"; import { defaultAvatarURL } from "@/config/constants"; import { getImageUrl } from "@/utils"; +import { VOLUNTEER_COL_WIDTHS } from "./volunteerTableColumns"; interface TableRowProps { volunteer: ApiVolunteerGetList; @@ -50,19 +51,19 @@ export function VolunteerTableRow({ volunteer, isLast, engagementLabels, typeLab {name} - + {statusType ? typeLabels[statusType] : "—"} - + {statusEngagement ? engagementLabels[statusEngagement] : "—"} - + - + {languageText} - + {districtText} diff --git a/src/components/Dashboard/Volunteers/volunteerTableColumns.ts b/src/components/Dashboard/Volunteers/volunteerTableColumns.ts index b60a07f2..595b3a92 100644 --- a/src/components/Dashboard/Volunteers/volunteerTableColumns.ts +++ b/src/components/Dashboard/Volunteers/volunteerTableColumns.ts @@ -1,12 +1,24 @@ import { TFunction } from "i18next"; import { Column } from "../common/EntityTableList"; +export const VOLUNTEER_COL_WIDTHS = { + type: "180px", + engagement: "200px", + matching: "140px", + language: "180px", + district: "200px", +}; + export const createVolunteerTableColumns = (t: TFunction): Column[] => [ { key: "name", label: t("dashboard.volunteers.table.name") }, - { key: "type", label: t("dashboard.volunteers.table.type"), width: "180px" }, - { key: "engagement", label: t("dashboard.volunteers.table.engagementStatus"), width: "200px" }, - { key: "matching", label: t("dashboard.volunteers.table.matchingStatus"), width: "140px" }, - { key: "language", label: t("dashboard.volunteers.table.language"), width: "180px" }, - { key: "district", label: t("dashboard.volunteers.table.district"), width: "200px" }, + { key: "type", label: t("dashboard.volunteers.table.type"), width: VOLUNTEER_COL_WIDTHS.type }, + { + key: "engagement", + label: t("dashboard.volunteers.table.engagementStatus"), + width: VOLUNTEER_COL_WIDTHS.engagement, + }, + { key: "matching", label: t("dashboard.volunteers.table.matchingStatus"), width: VOLUNTEER_COL_WIDTHS.matching }, + { key: "language", label: t("dashboard.volunteers.table.language"), width: VOLUNTEER_COL_WIDTHS.language }, + { key: "district", label: t("dashboard.volunteers.table.district"), width: VOLUNTEER_COL_WIDTHS.district }, { key: "email", label: t("dashboard.volunteers.table.email") }, ]; From 7b822ba9f9a6192b8235c48d1bd041f10bf67ccc Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Mon, 18 May 2026 19:48:21 -0400 Subject: [PATCH 6/8] replace page-specific CSS variable with generic --entity-table-gap in EntityTableList --- src/app/[lang]/globals.css | 3 +++ src/components/Dashboard/common/EntityTableList/styles.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/[lang]/globals.css b/src/app/[lang]/globals.css index 569e6a70..8260457c 100644 --- a/src/app/[lang]/globals.css +++ b/src/app/[lang]/globals.css @@ -503,6 +503,9 @@ html { --custom-px-17: 17px; --zero: 0px; + /* entity table */ + --entity-table-gap: var(--spacing-16); + /* volunteer header */ --volunteer-header-label-width: 214px; } diff --git a/src/components/Dashboard/common/EntityTableList/styles.ts b/src/components/Dashboard/common/EntityTableList/styles.ts index 07784d11..b93dc099 100644 --- a/src/components/Dashboard/common/EntityTableList/styles.ts +++ b/src/components/Dashboard/common/EntityTableList/styles.ts @@ -3,6 +3,6 @@ import styled from "styled-components"; export const Wrapper = styled.div` display: flex; flex-direction: column; - gap: var(--opportunities-container-gap); + gap: var(--entity-table-gap); width: 100%; `; From 368422af801b14e834aa02e450536011d3d4278c Mon Sep 17 00:00:00 2001 From: ivannissimrch Date: Mon, 18 May 2026 19:58:10 -0400 Subject: [PATCH 7/8] add isListView const to replace repeated viewMode checks in VolunteerListController --- .../Dashboard/Volunteers/VolunteerListController.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Dashboard/Volunteers/VolunteerListController.tsx b/src/components/Dashboard/Volunteers/VolunteerListController.tsx index 6796e5d8..3c3a310a 100644 --- a/src/components/Dashboard/Volunteers/VolunteerListController.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerListController.tsx @@ -32,7 +32,8 @@ export function VolunteerListController({ opportunityId, viewMode, }: VolunteerListControllerProps) { - const limit = viewMode === "list" ? TABLE_LIMIT : CARD_LIMIT; + const isListView = viewMode === "list"; + const limit = isListView ? TABLE_LIMIT : CARD_LIMIT; const { currentPage, setCurrentPage } = usePageParam(); const serializedFilter = serializeFilters(filter, undefined, false, { serializeToIDs: true, @@ -60,7 +61,7 @@ export function VolunteerListController({ setNumOfVols(count); }, [count, setNumOfVols]); - if (viewMode === "list") { + if (isListView) { return ( Date: Wed, 20 May 2026 17:19:56 -0400 Subject: [PATCH 8/8] Address Arturas review: ViewMode enum and Object.values mapping --- .../Dashboard/Volunteers/VolunteerListController.tsx | 2 +- src/components/Dashboard/Volunteers/Volunteers.tsx | 2 +- src/components/Dashboard/common/types.ts | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Dashboard/Volunteers/VolunteerListController.tsx b/src/components/Dashboard/Volunteers/VolunteerListController.tsx index f31eaee2..bd06f2da 100644 --- a/src/components/Dashboard/Volunteers/VolunteerListController.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerListController.tsx @@ -33,7 +33,7 @@ export function VolunteerListController({ opportunityId, viewMode, }: VolunteerListControllerProps) { - const isListView = viewMode === "list"; + const isListView = viewMode === ViewMode.LIST; const limit = isListView ? TABLE_LIMIT : CARD_LIMIT; const { currentPage, setCurrentPage } = usePageParam(); const serializedFilter = serializeFilters(filter, undefined, false, { diff --git a/src/components/Dashboard/Volunteers/Volunteers.tsx b/src/components/Dashboard/Volunteers/Volunteers.tsx index d86dbde7..c70968e9 100644 --- a/src/components/Dashboard/Volunteers/Volunteers.tsx +++ b/src/components/Dashboard/Volunteers/Volunteers.tsx @@ -35,7 +35,7 @@ export function Volunteers() { const pathname = usePathname(); const router = useRouter(); const tabs = [t("dashboard.volunteers.tabs.tab1"), t("dashboard.volunteers.tabs.tab2")]; - const viewMode: ViewMode = selectedTabIndex === 0 ? "list" : "cards"; + const viewMode = Object.values(ViewMode)[selectedTabIndex]; const opportunityId = searchParams.get("opportunity") ?? undefined; const opportunityFilter = useGetOpportunity(opportunityId); diff --git a/src/components/Dashboard/common/types.ts b/src/components/Dashboard/common/types.ts index ce039a07..9f6b985c 100644 --- a/src/components/Dashboard/common/types.ts +++ b/src/components/Dashboard/common/types.ts @@ -1 +1,5 @@ -export type ViewMode = "cards" | "list" | "map"; +export enum ViewMode { + LIST = "list", + CARDS = "cards", + MAP = "map", +}