From 8bbd48c09b27f226636c7901e270b014c5a53739 Mon Sep 17 00:00:00 2001 From: DarrellRoberts Date: Thu, 11 Jun 2026 18:26:53 +0200 Subject: [PATCH 1/5] adds non-admin/non-coordinator view to AgentList, VolunteerLists, Volunteerlists --- .../Dashboard/Agents/AgentCardList.tsx | 12 +++- .../Dashboard/Agents/AgentReadOnlyCard.tsx | 38 +++++++++++ .../Agents/AgentReadOnlyTableRow.tsx | 27 ++++++++ .../Dashboard/Agents/AgentTableList.tsx | 39 +++++++---- .../Dashboard/Agents/agentsTableColumns.ts | 6 ++ src/components/Dashboard/Agents/styles.ts | 4 +- .../Opportunities/OpportunityCardList.tsx | 26 +++++--- .../Opportunities/OpportunityReadOnlyCard.tsx | 51 ++++++++++++++ .../OpportunityReadOnlyTableRow.tsx | 31 +++++++++ .../Opportunities/OpportunityTableList.tsx | 36 ++++++---- .../opportunitiesTableColumns.ts | 6 ++ .../Dashboard/Opportunities/styles.ts | 4 +- .../Volunteers/VolunteerCardList.tsx | 13 +++- .../Volunteers/VolunteerReadOnlyCard.tsx | 66 +++++++++++++++++++ .../Volunteers/VolunteerReadOnlyTableRow.tsx | 51 ++++++++++++++ .../Volunteers/VolunteerTableList.tsx | 42 ++++++++---- src/components/Dashboard/Volunteers/styles.ts | 64 ++++++++++++++++++ .../Volunteers/volunteerTableColumns.ts | 6 ++ src/components/core/common/Table/styles.ts | 5 +- src/hooks/useCurrentUser.ts | 6 +- 20 files changed, 475 insertions(+), 58 deletions(-) create mode 100644 src/components/Dashboard/Agents/AgentReadOnlyCard.tsx create mode 100644 src/components/Dashboard/Agents/AgentReadOnlyTableRow.tsx create mode 100644 src/components/Dashboard/Opportunities/OpportunityReadOnlyCard.tsx create mode 100644 src/components/Dashboard/Opportunities/OpportunityReadOnlyTableRow.tsx create mode 100644 src/components/Dashboard/Volunteers/VolunteerReadOnlyCard.tsx create mode 100644 src/components/Dashboard/Volunteers/VolunteerReadOnlyTableRow.tsx create mode 100644 src/components/Dashboard/Volunteers/styles.ts diff --git a/src/components/Dashboard/Agents/AgentCardList.tsx b/src/components/Dashboard/Agents/AgentCardList.tsx index 3cbc439d..0fdccb10 100644 --- a/src/components/Dashboard/Agents/AgentCardList.tsx +++ b/src/components/Dashboard/Agents/AgentCardList.tsx @@ -2,6 +2,8 @@ import type { ApiAgentGetList, OptionItem } from "need4deed-sdk"; import { PaginatedGrid } from "@/components/core/paginatedGrid"; import { AgentCard } from "./AgentCard"; import { AgentCardListContainer } from "./styles"; +import { useCurrentUser } from "@/hooks/useCurrentUser"; +import { AgentReadOnlyCard } from "./AgentReadOnlyCard"; type Props = { agents: ApiAgentGetList[]; @@ -14,7 +16,15 @@ type Props = { }; export function AgentCardList({ agents, count, columns, rows, currentPage, setCurrentPage, districtsList }: Props) { - const items = agents.map((agent) => ); + const { isAuthorized } = useCurrentUser(); + + const items = agents.map((agent) => + isAuthorized ? ( + + ) : ( + + ), + ); return ( diff --git a/src/components/Dashboard/Agents/AgentReadOnlyCard.tsx b/src/components/Dashboard/Agents/AgentReadOnlyCard.tsx new file mode 100644 index 00000000..24c7d270 --- /dev/null +++ b/src/components/Dashboard/Agents/AgentReadOnlyCard.tsx @@ -0,0 +1,38 @@ +import { MapPinIcon } from "@phosphor-icons/react"; +import { IconDiv } from "@/components/styled/container"; +import { Heading4, Paragraph } from "@/components/styled/text"; +import { Card, CardDetailsInfo, CardHeader, CardHeaderInfo, DistrictContainer, DistrictDiv } from "./styles"; +import { useTranslation } from "react-i18next"; +import { ApiAgentGetList, OptionItem } from "need4deed-sdk"; + +interface Props { + agent: ApiAgentGetList; + districtsList?: OptionItem[]; +} + +export const AgentReadOnlyCard = ({ agent, districtsList }: Props) => { + const { t } = useTranslation(); + const { title, district, type } = agent; + const districtTitle = district?.id ? (districtsList?.find((d) => d.id === district.id)?.title ?? null) : null; + return ( + + + + {title} + + + + {t("dashboard.agentProfile.type")} + {type} + + + + + + + {districtTitle} + + + + ); +}; diff --git a/src/components/Dashboard/Agents/AgentReadOnlyTableRow.tsx b/src/components/Dashboard/Agents/AgentReadOnlyTableRow.tsx new file mode 100644 index 00000000..1cc23b4a --- /dev/null +++ b/src/components/Dashboard/Agents/AgentReadOnlyTableRow.tsx @@ -0,0 +1,27 @@ +import { ApiAgentGetList, OptionItem } from "need4deed-sdk"; +import { AGENT_COL_WIDTHS } from "./agentsTableColumns"; +import { ClickableRow, TableCell } from "@/components/core/common/Table"; + +interface Props { + agent: ApiAgentGetList; + isLast: boolean; + typeLabels: Record; + searchLabels: Record; + districtsList?: OptionItem[]; +} + +export function AgentReadOnlyTableRow({ agent, isLast, districtsList }: Props) { + const { id, title, type, district } = agent; + const districtTitle = district?.id ? (districtsList?.find((d) => d.id === district.id)?.title ?? null) : null; + return ( + + {title} + + {type} + + + {districtTitle || "—"} + + + ); +} diff --git a/src/components/Dashboard/Agents/AgentTableList.tsx b/src/components/Dashboard/Agents/AgentTableList.tsx index 2a428c54..0ee8d83e 100644 --- a/src/components/Dashboard/Agents/AgentTableList.tsx +++ b/src/components/Dashboard/Agents/AgentTableList.tsx @@ -3,10 +3,12 @@ import type { ApiAgentGetList, OptionItem } from "need4deed-sdk"; import { EntityTableList } from "../common/EntityTableList"; import { useTranslation } from "react-i18next"; -import { createAgentTableColumns } from "./agentsTableColumns"; +import { createAgentTableColumns, createReadOnlyAgentTableColumns } from "./agentsTableColumns"; import { useMemo } from "react"; import { AgentTableRow } from "./AgentTableRow"; import { createAgentTypeMap, createVolunteerSearchMap } from "./constants"; +import { useCurrentUser } from "@/hooks/useCurrentUser"; +import { AgentReadOnlyTableRow } from "./AgentReadOnlyTableRow"; interface TableListProps { agents: ApiAgentGetList[]; @@ -26,25 +28,38 @@ export function AgentTableList({ districtsList, }: TableListProps) { const { t } = useTranslation(); + const { isAuthorized } = useCurrentUser(); const columns = useMemo(() => createAgentTableColumns(t), [t]); + const readOnlyColumns = useMemo(() => createReadOnlyAgentTableColumns(t), [t]); const typeLabels = useMemo(() => createAgentTypeMap(t), [t]); const searchLabels = useMemo(() => createVolunteerSearchMap(t), [t]); return ( ( - - )} + renderRow={(agent, isLast) => + isAuthorized ? ( + + ) : ( + + ) + } count={count} itemsPerPage={itemsPerPage} currentPage={currentPage} diff --git a/src/components/Dashboard/Agents/agentsTableColumns.ts b/src/components/Dashboard/Agents/agentsTableColumns.ts index 95a5db20..5190201f 100644 --- a/src/components/Dashboard/Agents/agentsTableColumns.ts +++ b/src/components/Dashboard/Agents/agentsTableColumns.ts @@ -32,3 +32,9 @@ export const createAgentTableColumns = (t: TFunction): Column[] => [ }, { key: "email", label: t("dashboard.agents.table.email") }, ]; + +export const createReadOnlyAgentTableColumns = (t: TFunction): Column[] => [ + { key: "title", label: t("dashboard.agents.table.title") }, + { key: "type", label: t("dashboard.agents.table.type"), width: AGENT_COL_WIDTHS.type }, + { key: "district", label: t("dashboard.agents.table.district"), width: AGENT_COL_WIDTHS.district }, +]; diff --git a/src/components/Dashboard/Agents/styles.ts b/src/components/Dashboard/Agents/styles.ts index 08a069a9..73422bb8 100644 --- a/src/components/Dashboard/Agents/styles.ts +++ b/src/components/Dashboard/Agents/styles.ts @@ -23,7 +23,7 @@ export const AgentCardListContainer = styled.div` justify-content: left; `; -export const Card = styled(BaseCard)` +export const Card = styled(BaseCard)<{ $cursor?: string }>` background-color: var(--color-orchid-subtle); background-color: var(--color-orchid-subtle); width: var(--dashboard-agents-card-width); @@ -33,7 +33,7 @@ export const Card = styled(BaseCard)` transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; - cursor: pointer; + cursor: ${(props) => props.$cursor || "pointer"}; &:hover { background-color: var(--color-orchid); diff --git a/src/components/Dashboard/Opportunities/OpportunityCardList.tsx b/src/components/Dashboard/Opportunities/OpportunityCardList.tsx index 6e825bc5..a9c343b3 100644 --- a/src/components/Dashboard/Opportunities/OpportunityCardList.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityCardList.tsx @@ -2,6 +2,8 @@ import { ApiVolunteerOpportunityGetList, OptionItem } from "need4deed-sdk"; import { PaginatedGrid } from "@/components/core/paginatedGrid"; import { OpportunityCard } from "./OpportunityCard"; import { OpportunityCardListContainer } from "./styles"; +import { useCurrentUser } from "@/hooks/useCurrentUser"; +import { OpportunityReadOnlyCard } from "./OpportunityReadOnlyCard"; type Props = { activitiesList?: OptionItem[]; @@ -26,15 +28,21 @@ export function OpportunityCardList({ activitiesList, districtsList, }: Props) { - const items = opportunities.map((opp) => ( - - )); + const { isAuthorized } = useCurrentUser(); + + const items = opportunities.map((opp) => + isAuthorized ? ( + + ) : ( + + ), + ); return ( diff --git a/src/components/Dashboard/Opportunities/OpportunityReadOnlyCard.tsx b/src/components/Dashboard/Opportunities/OpportunityReadOnlyCard.tsx new file mode 100644 index 00000000..2e8a0ef5 --- /dev/null +++ b/src/components/Dashboard/Opportunities/OpportunityReadOnlyCard.tsx @@ -0,0 +1,51 @@ +import { ApiVolunteerOpportunityGetList, OptionItem, ProfileVolunteeringType } from "need4deed-sdk"; +import { useTranslation } from "react-i18next"; +import { Paragraph } from "@/components/styled/text"; +import CardDetail from "../Volunteers/CardDetail"; +import { CardParagraph } from "../Volunteers/VolunteerCard"; +import { IconName } from "../Volunteers/icon"; +import { volunteerTypeIconMap } from "./OpportunityCard.helpers"; +import { Card, StatusTagsDiv, TagDiv, TitleParagraph } from "./styles"; + +type Props = { + opportunity: ApiVolunteerOpportunityGetList; + volunteerId?: string; + activitiesList?: OptionItem[]; + districtsList?: OptionItem[]; +}; + +export function OpportunityReadOnlyCard({ opportunity, districtsList }: Props) { + const { t } = useTranslation(); + + const { title, volunteerType, district } = opportunity as ApiVolunteerOpportunityGetList & { + accompanyingDetails?: { appointmentDate?: string; appointmentTime?: string }; + statusMatch?: string; + district?: { id: number }; + }; + + const districtTitle = district?.id ? (districtsList?.find((d) => d.id === district.id)?.title ?? null) : null; + return ( + + + {volunteerType && ( + + + {t(`dashboard.opportunities.type.${volunteerType}`)} + + {volunteerTypeIconMap[volunteerType as ProfileVolunteeringType]} + + )} + + + {title} + + + {districtTitle && } + + + ); +} diff --git a/src/components/Dashboard/Opportunities/OpportunityReadOnlyTableRow.tsx b/src/components/Dashboard/Opportunities/OpportunityReadOnlyTableRow.tsx new file mode 100644 index 00000000..364197c5 --- /dev/null +++ b/src/components/Dashboard/Opportunities/OpportunityReadOnlyTableRow.tsx @@ -0,0 +1,31 @@ +"use client"; + +import type { ApiVolunteerOpportunityGetList, OptionItem } from "need4deed-sdk"; +import { useTranslation } from "react-i18next"; +import { ClickableRow, TableCell } from "@/components/core/common/Table"; +import { OPPORTUNITY_COL_WIDTHS } from "./opportunitiesTableColumns"; + +interface TableRowProps { + opportunity: ApiVolunteerOpportunityGetList; + isLast: boolean; + activitiesList?: OptionItem[]; + districtsList?: OptionItem[]; +} + +export function OpportunityReadOnlyTableRow({ opportunity, isLast, districtsList }: TableRowProps) { + const { t } = useTranslation(); + + const { id, title, volunteerType, district } = opportunity; + const districtTitle = district?.id ? (districtsList?.find((d) => d.id === district.id)?.title ?? null) : null; + return ( + + {title} + + {t(`dashboard.opportunities.type.${volunteerType}`)} + + + {districtTitle || "—"} + + + ); +} diff --git a/src/components/Dashboard/Opportunities/OpportunityTableList.tsx b/src/components/Dashboard/Opportunities/OpportunityTableList.tsx index ce8b296a..e627534c 100644 --- a/src/components/Dashboard/Opportunities/OpportunityTableList.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityTableList.tsx @@ -4,8 +4,10 @@ import type { ApiVolunteerOpportunityGetList, OptionItem } from "need4deed-sdk"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { EntityTableList } from "../common/EntityTableList"; -import { createOpportunityTableColumns } from "./opportunitiesTableColumns"; +import { createOpportunityTableColumns, createReadOnlyAgentTableColumns } from "./opportunitiesTableColumns"; import { OpportunityTableRow } from "./OpportunityTableRow"; +import { useCurrentUser } from "@/hooks/useCurrentUser"; +import { OpportunityReadOnlyTableRow } from "./OpportunityReadOnlyTableRow"; interface TableListProps { opportunities: ApiVolunteerOpportunityGetList[]; @@ -27,22 +29,32 @@ export function OpportunityTableList({ activitiesList, }: TableListProps) { const { t } = useTranslation(); + const { isAuthorized } = useCurrentUser(); const columns = useMemo(() => createOpportunityTableColumns(t), [t]); - + const readOnlyColumns = useMemo(() => createReadOnlyAgentTableColumns(t), [t]); return ( ( - - )} + renderRow={(opportunity, isLast) => + isAuthorized ? ( + + ) : ( + + ) + } count={count} itemsPerPage={itemsPerPage} currentPage={currentPage} diff --git a/src/components/Dashboard/Opportunities/opportunitiesTableColumns.ts b/src/components/Dashboard/Opportunities/opportunitiesTableColumns.ts index 1eb64cb1..f966b987 100644 --- a/src/components/Dashboard/Opportunities/opportunitiesTableColumns.ts +++ b/src/components/Dashboard/Opportunities/opportunitiesTableColumns.ts @@ -39,3 +39,9 @@ export const createOpportunityTableColumns = (t: TFunction): Column[] => [ }, { key: "agentTitle", label: t("dashboard.opportunities.table.agentName"), width: OPPORTUNITY_COL_WIDTHS.agentTitle }, ]; + +export const createReadOnlyAgentTableColumns = (t: TFunction): Column[] => [ + { key: "title", label: t("dashboard.agents.table.title") }, + { key: "type", label: t("dashboard.agents.table.type"), width: OPPORTUNITY_COL_WIDTHS.volunteerType }, + { key: "district", label: t("dashboard.agents.table.district"), width: OPPORTUNITY_COL_WIDTHS.district }, +]; diff --git a/src/components/Dashboard/Opportunities/styles.ts b/src/components/Dashboard/Opportunities/styles.ts index 8ef5d5d9..7952f7f8 100644 --- a/src/components/Dashboard/Opportunities/styles.ts +++ b/src/components/Dashboard/Opportunities/styles.ts @@ -24,7 +24,7 @@ export const OpportunityCardListContainer = styled.div` justify-content: left; `; -export const Card = styled(BaseCard)` +export const Card = styled(BaseCard)<{ $cursor?: string }>` background-color: var(--color-orchid-subtle); width: var(--dashboard-volunteers-card-width); min-height: var(--dashboard-volunteers-card-height); @@ -33,7 +33,7 @@ export const Card = styled(BaseCard)` transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out; - cursor: pointer; + cursor: ${(props) => props.$cursor || "pointer"}; &:hover { background-color: var(--color-orchid); diff --git a/src/components/Dashboard/Volunteers/VolunteerCardList.tsx b/src/components/Dashboard/Volunteers/VolunteerCardList.tsx index 1b6c6fe7..993d56ed 100644 --- a/src/components/Dashboard/Volunteers/VolunteerCardList.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerCardList.tsx @@ -3,6 +3,8 @@ import { PaginatedGrid } from "@/components/core/paginatedGrid"; import VolunteerCard from "./VolunteerCard"; import { ApiVolunteerGetList } from "need4deed-sdk"; import styled from "styled-components"; +import { useCurrentUser } from "@/hooks/useCurrentUser"; +import { VolunteerReadOnlyCard } from "./VolunteerReadOnlyCard"; interface VolunteerCardListProps { volunteers: ApiVolunteerGetList[]; @@ -30,9 +32,14 @@ export function VolunteerCardList({ setCurrentPage, opportunityId, }: VolunteerCardListProps) { - const items = volunteers.map((volunteer) => ( - - )); + const { isAuthorized } = useCurrentUser(); + const items = volunteers.map((volunteer) => + isAuthorized ? ( + + ) : ( + + ), + ); return ( diff --git a/src/components/Dashboard/Volunteers/VolunteerReadOnlyCard.tsx b/src/components/Dashboard/Volunteers/VolunteerReadOnlyCard.tsx new file mode 100644 index 00000000..bea60f6e --- /dev/null +++ b/src/components/Dashboard/Volunteers/VolunteerReadOnlyCard.tsx @@ -0,0 +1,66 @@ +import { SparkleIcon } from "@phosphor-icons/react"; +import { ApiVolunteerGetList } from "need4deed-sdk"; +import { Paragraph } from "@/components/styled/text"; +import CardDetail from "./CardDetail"; +import { getFirstName, truncateList } from "./helpers"; +import { IconName } from "./icon"; +import { Card, ProfileDiv, StatusTagsDiv, TagDiv } from "./styles"; +import { useTranslation } from "react-i18next"; + +interface Props { + volunteer: ApiVolunteerGetList; +} + +export function VolunteerReadOnlyCard({ volunteer }: Props) { + const { t } = useTranslation(); + + const { name, locations, statusType } = volunteer; + const MAX_CARD_ITEMS = 2; + const locationTitles = + truncateList( + locations.map((loc) => loc.title), + MAX_CARD_ITEMS, + ) || "—"; + + return ( + + + <> + {statusType && ( + <> + + + {statusType.toUpperCase()} + + + + + )} + + + + + + {getFirstName(name)} + + + + + + {locationTitles} + + + + ); +} diff --git a/src/components/Dashboard/Volunteers/VolunteerReadOnlyTableRow.tsx b/src/components/Dashboard/Volunteers/VolunteerReadOnlyTableRow.tsx new file mode 100644 index 00000000..52d639b8 --- /dev/null +++ b/src/components/Dashboard/Volunteers/VolunteerReadOnlyTableRow.tsx @@ -0,0 +1,51 @@ +"use client"; + +import type { + createEngagementStatusLabelMap, + createStatusLabelMap, +} from "@/components/Dashboard/Profile/sections/VolunteerAgents/types"; +import { ClickableRow, TableCell, TruncatedText } from "@/components/core/common/Table"; +import { ApiVolunteerGetList } from "need4deed-sdk"; +import { VOLUNTEER_COL_WIDTHS } from "./volunteerTableColumns"; +import { getFirstName, truncateList } from "./helpers"; + +interface TableRowProps { + volunteer: ApiVolunteerGetList; + isLast: boolean; + engagementLabels: ReturnType; + typeLabels: ReturnType; +} + +export function VolunteerReadOnlyTableRow({ volunteer, isLast, typeLabels }: TableRowProps) { + const { id, name, statusType, locations } = volunteer; + + const districtTitles = locations + .map((loc) => (typeof loc === "string" ? loc : loc?.title)) + .filter(Boolean) as string[]; + const abbreviatedDistricts = districtTitles.map((title) => { + if (title.includes("-")) { + const abbreviation = title + .split("-") + .filter((word) => word.trim()) + .map((word) => word.trim()[0]) + .join("-"); + return abbreviation.toUpperCase(); + } + return title; + }); + const districtText = truncateList(abbreviatedDistricts, 1) || "—"; + + return ( + + + {getFirstName(name)} + + + {statusType ? typeLabels[statusType] : "—"} + + + {districtText} + + + ); +} diff --git a/src/components/Dashboard/Volunteers/VolunteerTableList.tsx b/src/components/Dashboard/Volunteers/VolunteerTableList.tsx index 4d7175ab..5f533ef9 100644 --- a/src/components/Dashboard/Volunteers/VolunteerTableList.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerTableList.tsx @@ -8,10 +8,12 @@ import { createMatchStatusLabelMap, createStatusLabelMap, } from "@/components/Dashboard/Profile/sections/VolunteerAgents/types"; -import { createVolunteerTableColumns } from "./volunteerTableColumns"; +import { createReadOnlyVolunteerTableColumns, createVolunteerTableColumns } from "./volunteerTableColumns"; import { VolunteerTableRow } from "./VolunteerTableRow"; import { EntityTableList } from "../common/EntityTableList"; import { CopyButton } from "../common/CopyButton"; +import { useCurrentUser } from "@/hooks/useCurrentUser"; +import { VolunteerReadOnlyTableRow } from "./VolunteerReadOnlyTableRow"; interface TableListProps { volunteers: ApiVolunteerGetList[]; @@ -35,6 +37,7 @@ export function VolunteerTableList({ isCopying, }: TableListProps) { const { t } = useTranslation(); + const { isAuthorized } = useCurrentUser(); const engagementLabels = useMemo(() => createEngagementStatusLabelMap(t), [t]); const typeLabels = useMemo(() => createStatusLabelMap(t), [t]); const columns = useMemo(() => { @@ -48,23 +51,36 @@ export function VolunteerTableList({ ); return createVolunteerTableColumns(t, copyButton); }, [t, onCopyEmails, isCopying]); + const readOnlyColumns = useMemo(() => { + return createReadOnlyVolunteerTableColumns(t); + }, [t, onCopyEmails, isCopying]); const matchLabels = useMemo(() => createMatchStatusLabelMap(t), [t]); return ( ( - - )} + renderRow={(volunteer, isLast) => + isAuthorized ? ( + + ) : ( + + ) + } count={count} itemsPerPage={itemsPerPage} currentPage={currentPage} diff --git a/src/components/Dashboard/Volunteers/styles.ts b/src/components/Dashboard/Volunteers/styles.ts new file mode 100644 index 00000000..ca43aaae --- /dev/null +++ b/src/components/Dashboard/Volunteers/styles.ts @@ -0,0 +1,64 @@ +import { BaseCard } from "@/components/styled/container"; +import styled from "styled-components"; + +export const Card = styled(BaseCard)<{ $cursor?: string }>` + background-color: var(--color-orchid-subtle); + width: var(--dashboard-volunteers-card-width); + height: var(--dashboard-volunteers-card-height); + gap: var(--dashboard-volunteers-card-gap); + padding: var(--dashboard-volunteers-card-padding); + transition: + transform 0.3s ease-in-out, + box-shadow 0.3s ease-in-out; + cursor: ${(props) => props.$cursor || "pointer"}; + + &:hover { + background-color: var(--color-orchid); + } +`; + +export const StatusTagsDiv = styled.div` + display: flex; + flex-direction: row; + gap: var(--dashboard-volunteers-card-status-tags-div-gap); + margin-top: var(--dashboard-volunteers-card-status-tags-div-margin-top); +`; + +export const StatusDiv = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + background: var(--color-white); + height: var(--dashboard-volunteers-card-status-div-height); + border-bottom-left-radius: var(--dashboard-volunteers-card-status-div-bordor-bottom); + border-bottom-right-radius: var(--dashboard-volunteers-card-status-div-bordor-bottom); + gap: var(--dashboard-volunteers-card-status-div-gap); + padding: var(--dashboard-volunteers-card-status-div-padding); +`; + +export const TagDiv = styled.div` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + background: var(--color-green-200); + height: var(--dashboard-volunteers-card-status-div-height); + border-bottom-left-radius: var(--dashboard-volunteers-card-status-div-bordor-bottom); + border-bottom-right-radius: var(--dashboard-volunteers-card-status-div-bordor-bottom); + padding: var(--dashboard-volunteers-card-tag-div-padding); + gap: var(--dashboard-volunteers-card-tag-div-gap); +`; + +export const ProfileDiv = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: var(--dashboard-volunteers-card-profile-div-gap); +`; + +export const LanguageDetailContainer = styled.div` + display: flex; + flex-direction: row; + gap: var(--dashboard-volunteers-card-detail-gap); +`; diff --git a/src/components/Dashboard/Volunteers/volunteerTableColumns.ts b/src/components/Dashboard/Volunteers/volunteerTableColumns.ts index 7b382bf9..79761ad8 100644 --- a/src/components/Dashboard/Volunteers/volunteerTableColumns.ts +++ b/src/components/Dashboard/Volunteers/volunteerTableColumns.ts @@ -31,3 +31,9 @@ export const createVolunteerTableColumns = (t: TFunction, copyButton: ReactNode) headerAction: copyButton, }, ]; + +export const createReadOnlyVolunteerTableColumns = (t: TFunction): Column[] => [ + { key: "title", label: t("dashboard.agents.table.title") }, + { key: "type", label: t("dashboard.agents.table.type"), width: VOLUNTEER_COL_WIDTHS.type }, + { key: "district", label: t("dashboard.agents.table.district"), width: VOLUNTEER_COL_WIDTHS.district }, +]; diff --git a/src/components/core/common/Table/styles.ts b/src/components/core/common/Table/styles.ts index 6a3f77e2..259c5bdb 100644 --- a/src/components/core/common/Table/styles.ts +++ b/src/components/core/common/Table/styles.ts @@ -105,8 +105,9 @@ export const ActionButton = styled.button` } `; -export const ClickableRow = styled(TableRow)` - cursor: pointer; +export const ClickableRow = styled(TableRow)<{ $cursor?: string }>` + cursor: ${(props) => props.$cursor || "pointer"}; + &:hover { background: var(--color-pink-50); } diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/useCurrentUser.ts index 69b14753..e33c1d45 100644 --- a/src/hooks/useCurrentUser.ts +++ b/src/hooks/useCurrentUser.ts @@ -1,7 +1,7 @@ import { apiPathMe, cacheTTL } from "@/config/constants"; import { useGetQuery } from "@/hooks"; import { getCookie } from "@/utils/helpers"; -import { ApiUserGet } from "need4deed-sdk"; +import { ApiUserGet, UserRole } from "need4deed-sdk"; export const useCurrentUser = (enabled?: boolean) => { const hasAuthHint = getCookie("is_logged_in") === "true"; @@ -13,5 +13,7 @@ export const useCurrentUser = (enabled?: boolean) => { enabled: hasAuthHint && enabled, }); - return data; + const isAuthorized = data?.role === UserRole.ADMIN || UserRole.COORDINATOR; + + return { ...data, isAuthorized } as ApiUserGet & { isAuthorized: boolean }; }; From 6ff28e6aa8447008025a64ae1ecfe5c6b5e562e1 Mon Sep 17 00:00:00 2001 From: DarrellRoberts Date: Thu, 11 Jun 2026 22:58:46 +0200 Subject: [PATCH 2/5] reverts changes to volunteers --- .../Volunteers/VolunteerCardList.tsx | 13 +--- .../Volunteers/VolunteerReadOnlyCard.tsx | 66 ------------------- .../Volunteers/VolunteerReadOnlyTableRow.tsx | 51 -------------- .../Volunteers/VolunteerTableList.tsx | 42 ++++-------- src/components/Dashboard/Volunteers/styles.ts | 64 ------------------ .../Volunteers/volunteerTableColumns.ts | 6 -- 6 files changed, 16 insertions(+), 226 deletions(-) delete mode 100644 src/components/Dashboard/Volunteers/VolunteerReadOnlyCard.tsx delete mode 100644 src/components/Dashboard/Volunteers/VolunteerReadOnlyTableRow.tsx delete mode 100644 src/components/Dashboard/Volunteers/styles.ts diff --git a/src/components/Dashboard/Volunteers/VolunteerCardList.tsx b/src/components/Dashboard/Volunteers/VolunteerCardList.tsx index 993d56ed..1b6c6fe7 100644 --- a/src/components/Dashboard/Volunteers/VolunteerCardList.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerCardList.tsx @@ -3,8 +3,6 @@ import { PaginatedGrid } from "@/components/core/paginatedGrid"; import VolunteerCard from "./VolunteerCard"; import { ApiVolunteerGetList } from "need4deed-sdk"; import styled from "styled-components"; -import { useCurrentUser } from "@/hooks/useCurrentUser"; -import { VolunteerReadOnlyCard } from "./VolunteerReadOnlyCard"; interface VolunteerCardListProps { volunteers: ApiVolunteerGetList[]; @@ -32,14 +30,9 @@ export function VolunteerCardList({ setCurrentPage, opportunityId, }: VolunteerCardListProps) { - const { isAuthorized } = useCurrentUser(); - const items = volunteers.map((volunteer) => - isAuthorized ? ( - - ) : ( - - ), - ); + const items = volunteers.map((volunteer) => ( + + )); return ( diff --git a/src/components/Dashboard/Volunteers/VolunteerReadOnlyCard.tsx b/src/components/Dashboard/Volunteers/VolunteerReadOnlyCard.tsx deleted file mode 100644 index bea60f6e..00000000 --- a/src/components/Dashboard/Volunteers/VolunteerReadOnlyCard.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { SparkleIcon } from "@phosphor-icons/react"; -import { ApiVolunteerGetList } from "need4deed-sdk"; -import { Paragraph } from "@/components/styled/text"; -import CardDetail from "./CardDetail"; -import { getFirstName, truncateList } from "./helpers"; -import { IconName } from "./icon"; -import { Card, ProfileDiv, StatusTagsDiv, TagDiv } from "./styles"; -import { useTranslation } from "react-i18next"; - -interface Props { - volunteer: ApiVolunteerGetList; -} - -export function VolunteerReadOnlyCard({ volunteer }: Props) { - const { t } = useTranslation(); - - const { name, locations, statusType } = volunteer; - const MAX_CARD_ITEMS = 2; - const locationTitles = - truncateList( - locations.map((loc) => loc.title), - MAX_CARD_ITEMS, - ) || "—"; - - return ( - - - <> - {statusType && ( - <> - - - {statusType.toUpperCase()} - - - - - )} - - - - - - {getFirstName(name)} - - - - - - {locationTitles} - - - - ); -} diff --git a/src/components/Dashboard/Volunteers/VolunteerReadOnlyTableRow.tsx b/src/components/Dashboard/Volunteers/VolunteerReadOnlyTableRow.tsx deleted file mode 100644 index 52d639b8..00000000 --- a/src/components/Dashboard/Volunteers/VolunteerReadOnlyTableRow.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import type { - createEngagementStatusLabelMap, - createStatusLabelMap, -} from "@/components/Dashboard/Profile/sections/VolunteerAgents/types"; -import { ClickableRow, TableCell, TruncatedText } from "@/components/core/common/Table"; -import { ApiVolunteerGetList } from "need4deed-sdk"; -import { VOLUNTEER_COL_WIDTHS } from "./volunteerTableColumns"; -import { getFirstName, truncateList } from "./helpers"; - -interface TableRowProps { - volunteer: ApiVolunteerGetList; - isLast: boolean; - engagementLabels: ReturnType; - typeLabels: ReturnType; -} - -export function VolunteerReadOnlyTableRow({ volunteer, isLast, typeLabels }: TableRowProps) { - const { id, name, statusType, locations } = volunteer; - - const districtTitles = locations - .map((loc) => (typeof loc === "string" ? loc : loc?.title)) - .filter(Boolean) as string[]; - const abbreviatedDistricts = districtTitles.map((title) => { - if (title.includes("-")) { - const abbreviation = title - .split("-") - .filter((word) => word.trim()) - .map((word) => word.trim()[0]) - .join("-"); - return abbreviation.toUpperCase(); - } - return title; - }); - const districtText = truncateList(abbreviatedDistricts, 1) || "—"; - - return ( - - - {getFirstName(name)} - - - {statusType ? typeLabels[statusType] : "—"} - - - {districtText} - - - ); -} diff --git a/src/components/Dashboard/Volunteers/VolunteerTableList.tsx b/src/components/Dashboard/Volunteers/VolunteerTableList.tsx index 5f533ef9..4d7175ab 100644 --- a/src/components/Dashboard/Volunteers/VolunteerTableList.tsx +++ b/src/components/Dashboard/Volunteers/VolunteerTableList.tsx @@ -8,12 +8,10 @@ import { createMatchStatusLabelMap, createStatusLabelMap, } from "@/components/Dashboard/Profile/sections/VolunteerAgents/types"; -import { createReadOnlyVolunteerTableColumns, createVolunteerTableColumns } from "./volunteerTableColumns"; +import { createVolunteerTableColumns } from "./volunteerTableColumns"; import { VolunteerTableRow } from "./VolunteerTableRow"; import { EntityTableList } from "../common/EntityTableList"; import { CopyButton } from "../common/CopyButton"; -import { useCurrentUser } from "@/hooks/useCurrentUser"; -import { VolunteerReadOnlyTableRow } from "./VolunteerReadOnlyTableRow"; interface TableListProps { volunteers: ApiVolunteerGetList[]; @@ -37,7 +35,6 @@ export function VolunteerTableList({ isCopying, }: TableListProps) { const { t } = useTranslation(); - const { isAuthorized } = useCurrentUser(); const engagementLabels = useMemo(() => createEngagementStatusLabelMap(t), [t]); const typeLabels = useMemo(() => createStatusLabelMap(t), [t]); const columns = useMemo(() => { @@ -51,36 +48,23 @@ export function VolunteerTableList({ ); return createVolunteerTableColumns(t, copyButton); }, [t, onCopyEmails, isCopying]); - const readOnlyColumns = useMemo(() => { - return createReadOnlyVolunteerTableColumns(t); - }, [t, onCopyEmails, isCopying]); const matchLabels = useMemo(() => createMatchStatusLabelMap(t), [t]); return ( - isAuthorized ? ( - - ) : ( - - ) - } + renderRow={(volunteer, isLast) => ( + + )} count={count} itemsPerPage={itemsPerPage} currentPage={currentPage} diff --git a/src/components/Dashboard/Volunteers/styles.ts b/src/components/Dashboard/Volunteers/styles.ts deleted file mode 100644 index ca43aaae..00000000 --- a/src/components/Dashboard/Volunteers/styles.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { BaseCard } from "@/components/styled/container"; -import styled from "styled-components"; - -export const Card = styled(BaseCard)<{ $cursor?: string }>` - background-color: var(--color-orchid-subtle); - width: var(--dashboard-volunteers-card-width); - height: var(--dashboard-volunteers-card-height); - gap: var(--dashboard-volunteers-card-gap); - padding: var(--dashboard-volunteers-card-padding); - transition: - transform 0.3s ease-in-out, - box-shadow 0.3s ease-in-out; - cursor: ${(props) => props.$cursor || "pointer"}; - - &:hover { - background-color: var(--color-orchid); - } -`; - -export const StatusTagsDiv = styled.div` - display: flex; - flex-direction: row; - gap: var(--dashboard-volunteers-card-status-tags-div-gap); - margin-top: var(--dashboard-volunteers-card-status-tags-div-margin-top); -`; - -export const StatusDiv = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - background: var(--color-white); - height: var(--dashboard-volunteers-card-status-div-height); - border-bottom-left-radius: var(--dashboard-volunteers-card-status-div-bordor-bottom); - border-bottom-right-radius: var(--dashboard-volunteers-card-status-div-bordor-bottom); - gap: var(--dashboard-volunteers-card-status-div-gap); - padding: var(--dashboard-volunteers-card-status-div-padding); -`; - -export const TagDiv = styled.div` - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - background: var(--color-green-200); - height: var(--dashboard-volunteers-card-status-div-height); - border-bottom-left-radius: var(--dashboard-volunteers-card-status-div-bordor-bottom); - border-bottom-right-radius: var(--dashboard-volunteers-card-status-div-bordor-bottom); - padding: var(--dashboard-volunteers-card-tag-div-padding); - gap: var(--dashboard-volunteers-card-tag-div-gap); -`; - -export const ProfileDiv = styled.div` - display: flex; - flex-direction: row; - align-items: center; - gap: var(--dashboard-volunteers-card-profile-div-gap); -`; - -export const LanguageDetailContainer = styled.div` - display: flex; - flex-direction: row; - gap: var(--dashboard-volunteers-card-detail-gap); -`; diff --git a/src/components/Dashboard/Volunteers/volunteerTableColumns.ts b/src/components/Dashboard/Volunteers/volunteerTableColumns.ts index 79761ad8..7b382bf9 100644 --- a/src/components/Dashboard/Volunteers/volunteerTableColumns.ts +++ b/src/components/Dashboard/Volunteers/volunteerTableColumns.ts @@ -31,9 +31,3 @@ export const createVolunteerTableColumns = (t: TFunction, copyButton: ReactNode) headerAction: copyButton, }, ]; - -export const createReadOnlyVolunteerTableColumns = (t: TFunction): Column[] => [ - { key: "title", label: t("dashboard.agents.table.title") }, - { key: "type", label: t("dashboard.agents.table.type"), width: VOLUNTEER_COL_WIDTHS.type }, - { key: "district", label: t("dashboard.agents.table.district"), width: VOLUNTEER_COL_WIDTHS.district }, -]; From af8318276114df1b7b1af889d6f32709aa6ac8c2 Mon Sep 17 00:00:00 2001 From: DarrellRoberts Date: Fri, 12 Jun 2026 23:48:18 +0200 Subject: [PATCH 3/5] removes some filters for unauth view & evens out unauth view of table --- .../Dashboard/Agents/AgentCardList.tsx | 4 +- .../Agents/AgentReadOnlyTableRow.tsx | 9 +---- .../Dashboard/Agents/AgentTableList.tsx | 5 ++- .../Agents/Filters/FiltersContent.tsx | 6 ++- .../Dashboard/Agents/agentsTableColumns.ts | 12 ++++-- .../Opportunities/Filters/FiltersContent.tsx | 14 ++++--- .../Opportunities/OpportunityCardList.tsx | 4 +- .../Opportunities/OpportunityReadOnlyCard.tsx | 40 +++++++++++++++++-- .../OpportunityReadOnlyTableRow.tsx | 25 +++++++++--- .../Opportunities/OpportunityTableList.tsx | 4 +- .../opportunitiesTableColumns.ts | 24 +++++++++-- .../EntityTableList/EntityTableList.tsx | 3 +- .../Dashboard/common/EntityTableList/types.ts | 1 + .../BecomeVolunteer/OpportunityTitle.tsx | 19 +++++++++ src/hooks/useAuth.ts | 16 ++++++++ src/hooks/useCurrentUser.ts | 6 +-- 16 files changed, 150 insertions(+), 42 deletions(-) create mode 100644 src/components/forms/BecomeVolunteer/OpportunityTitle.tsx create mode 100644 src/hooks/useAuth.ts diff --git a/src/components/Dashboard/Agents/AgentCardList.tsx b/src/components/Dashboard/Agents/AgentCardList.tsx index 0fdccb10..5f0a48dd 100644 --- a/src/components/Dashboard/Agents/AgentCardList.tsx +++ b/src/components/Dashboard/Agents/AgentCardList.tsx @@ -2,8 +2,8 @@ import type { ApiAgentGetList, OptionItem } from "need4deed-sdk"; import { PaginatedGrid } from "@/components/core/paginatedGrid"; import { AgentCard } from "./AgentCard"; import { AgentCardListContainer } from "./styles"; -import { useCurrentUser } from "@/hooks/useCurrentUser"; import { AgentReadOnlyCard } from "./AgentReadOnlyCard"; +import { useAuth } from "@/hooks/useAuth"; type Props = { agents: ApiAgentGetList[]; @@ -16,7 +16,7 @@ type Props = { }; export function AgentCardList({ agents, count, columns, rows, currentPage, setCurrentPage, districtsList }: Props) { - const { isAuthorized } = useCurrentUser(); + const isAuthorized = useAuth(); const items = agents.map((agent) => isAuthorized ? ( diff --git a/src/components/Dashboard/Agents/AgentReadOnlyTableRow.tsx b/src/components/Dashboard/Agents/AgentReadOnlyTableRow.tsx index 1cc23b4a..9f3b7113 100644 --- a/src/components/Dashboard/Agents/AgentReadOnlyTableRow.tsx +++ b/src/components/Dashboard/Agents/AgentReadOnlyTableRow.tsx @@ -1,5 +1,4 @@ import { ApiAgentGetList, OptionItem } from "need4deed-sdk"; -import { AGENT_COL_WIDTHS } from "./agentsTableColumns"; import { ClickableRow, TableCell } from "@/components/core/common/Table"; interface Props { @@ -16,12 +15,8 @@ export function AgentReadOnlyTableRow({ agent, isLast, districtsList }: Props) { return ( {title} - - {type} - - - {districtTitle || "—"} - + {type} + {districtTitle || "—"} ); } diff --git a/src/components/Dashboard/Agents/AgentTableList.tsx b/src/components/Dashboard/Agents/AgentTableList.tsx index 0ee8d83e..1a31edb7 100644 --- a/src/components/Dashboard/Agents/AgentTableList.tsx +++ b/src/components/Dashboard/Agents/AgentTableList.tsx @@ -7,8 +7,8 @@ import { createAgentTableColumns, createReadOnlyAgentTableColumns } from "./agen import { useMemo } from "react"; import { AgentTableRow } from "./AgentTableRow"; import { createAgentTypeMap, createVolunteerSearchMap } from "./constants"; -import { useCurrentUser } from "@/hooks/useCurrentUser"; import { AgentReadOnlyTableRow } from "./AgentReadOnlyTableRow"; +import { useAuth } from "@/hooks/useAuth"; interface TableListProps { agents: ApiAgentGetList[]; @@ -28,7 +28,7 @@ export function AgentTableList({ districtsList, }: TableListProps) { const { t } = useTranslation(); - const { isAuthorized } = useCurrentUser(); + const isAuthorized = useAuth(); const columns = useMemo(() => createAgentTableColumns(t), [t]); const readOnlyColumns = useMemo(() => createReadOnlyAgentTableColumns(t), [t]); @@ -65,6 +65,7 @@ export function AgentTableList({ currentPage={currentPage} setCurrentPage={setCurrentPage} testIdPrefix="agents" + noFixedWidth={!isAuthorized} /> ); } diff --git a/src/components/Dashboard/Agents/Filters/FiltersContent.tsx b/src/components/Dashboard/Agents/Filters/FiltersContent.tsx index 567c0d92..77a18560 100644 --- a/src/components/Dashboard/Agents/Filters/FiltersContent.tsx +++ b/src/components/Dashboard/Agents/Filters/FiltersContent.tsx @@ -4,6 +4,7 @@ import AccordionFilter from "../../common/CardsFilter/AccordionFilter"; import { SetFilter } from "../../common/CardsFilter/types"; import { createAgentFilterItems } from "./helpers"; import { FiltersContentContainer } from "./styles"; +import { useAuth } from "@/hooks/useAuth"; type Props = { filter: AgentCardsFilter; @@ -12,6 +13,7 @@ type Props = { export default function FiltersContent({ setFilter, filter }: Props) { const { t } = useTranslation(); + const isAuthorized = useAuth(); const { districtFilters, volunteerSearchFilters, typeFilters, engagementStatusFilters, servicesFilters } = createAgentFilterItems(filter, setFilter, t); @@ -19,7 +21,9 @@ export default function FiltersContent({ setFilter, filter }: Props) { return ( - + {isAuthorized && ( + + )} diff --git a/src/components/Dashboard/Agents/agentsTableColumns.ts b/src/components/Dashboard/Agents/agentsTableColumns.ts index 5190201f..23686f40 100644 --- a/src/components/Dashboard/Agents/agentsTableColumns.ts +++ b/src/components/Dashboard/Agents/agentsTableColumns.ts @@ -11,6 +11,12 @@ export const AGENT_COL_WIDTHS = { email: COLUMN_WIDTH.LG, }; +export const AGENT_READ_ONLY_COL_WIDTHS = { + title: COLUMN_WIDTH.XXXL, + type: COLUMN_WIDTH.XXXL, + district: COLUMN_WIDTH.XXXL, +}; + export const createAgentTableColumns = (t: TFunction): Column[] => [ { key: "title", label: t("dashboard.agents.table.title") }, { key: "type", label: t("dashboard.agents.table.type"), width: AGENT_COL_WIDTHS.type }, @@ -34,7 +40,7 @@ export const createAgentTableColumns = (t: TFunction): Column[] => [ ]; export const createReadOnlyAgentTableColumns = (t: TFunction): Column[] => [ - { key: "title", label: t("dashboard.agents.table.title") }, - { key: "type", label: t("dashboard.agents.table.type"), width: AGENT_COL_WIDTHS.type }, - { key: "district", label: t("dashboard.agents.table.district"), width: AGENT_COL_WIDTHS.district }, + { key: "title", label: t("dashboard.agents.table.title"), width: AGENT_READ_ONLY_COL_WIDTHS.title }, + { key: "type", label: t("dashboard.agents.table.type"), width: AGENT_READ_ONLY_COL_WIDTHS.type }, + { key: "district", label: t("dashboard.agents.table.district"), width: AGENT_READ_ONLY_COL_WIDTHS.district }, ]; diff --git a/src/components/Dashboard/Opportunities/Filters/FiltersContent.tsx b/src/components/Dashboard/Opportunities/Filters/FiltersContent.tsx index 016febbd..ff90441a 100644 --- a/src/components/Dashboard/Opportunities/Filters/FiltersContent.tsx +++ b/src/components/Dashboard/Opportunities/Filters/FiltersContent.tsx @@ -4,6 +4,7 @@ import AccordionFilter from "../../common/CardsFilter/AccordionFilter"; import { SetFilter } from "../../common/CardsFilter/types"; import { createOpportunityFilterItems } from "./helpers"; import { FiltersContentContainer } from "./styles"; +import { useAuth } from "@/hooks/useAuth"; type Props = { filter: OpportunityCardsFilter; @@ -12,6 +13,7 @@ type Props = { export default function FiltersContent({ setFilter, filter }: Props) { const { t } = useTranslation(); + const isAuthorized = useAuth(); const { districtFilters, languageFilters, statusFilters, typeFilters, activityFilters, availabilityFilters } = createOpportunityFilterItems(filter, setFilter, t); @@ -22,11 +24,13 @@ export default function FiltersContent({ setFilter, filter }: Props) { - + {isAuthorized && ( + + )} ); } diff --git a/src/components/Dashboard/Opportunities/OpportunityCardList.tsx b/src/components/Dashboard/Opportunities/OpportunityCardList.tsx index a9c343b3..da9ded28 100644 --- a/src/components/Dashboard/Opportunities/OpportunityCardList.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityCardList.tsx @@ -2,8 +2,8 @@ import { ApiVolunteerOpportunityGetList, OptionItem } from "need4deed-sdk"; import { PaginatedGrid } from "@/components/core/paginatedGrid"; import { OpportunityCard } from "./OpportunityCard"; import { OpportunityCardListContainer } from "./styles"; -import { useCurrentUser } from "@/hooks/useCurrentUser"; import { OpportunityReadOnlyCard } from "./OpportunityReadOnlyCard"; +import { useAuth } from "@/hooks/useAuth"; type Props = { activitiesList?: OptionItem[]; @@ -28,7 +28,7 @@ export function OpportunityCardList({ activitiesList, districtsList, }: Props) { - const { isAuthorized } = useCurrentUser(); + const isAuthorized = useAuth(); const items = opportunities.map((opp) => isAuthorized ? ( diff --git a/src/components/Dashboard/Opportunities/OpportunityReadOnlyCard.tsx b/src/components/Dashboard/Opportunities/OpportunityReadOnlyCard.tsx index 2e8a0ef5..566bce18 100644 --- a/src/components/Dashboard/Opportunities/OpportunityReadOnlyCard.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityReadOnlyCard.tsx @@ -1,11 +1,12 @@ -import { ApiVolunteerOpportunityGetList, OptionItem, ProfileVolunteeringType } from "need4deed-sdk"; +import { ApiVolunteerOpportunityGetList, LangPurpose, OptionItem, ProfileVolunteeringType } from "need4deed-sdk"; import { useTranslation } from "react-i18next"; import { Paragraph } from "@/components/styled/text"; import CardDetail from "../Volunteers/CardDetail"; import { CardParagraph } from "../Volunteers/VolunteerCard"; import { IconName } from "../Volunteers/icon"; -import { volunteerTypeIconMap } from "./OpportunityCard.helpers"; -import { Card, StatusTagsDiv, TagDiv, TitleParagraph } from "./styles"; +import { matchStatusColorMap, matchStatusIconMap, volunteerTypeIconMap } from "./OpportunityCard.helpers"; +import { Card, LanguageRow, StatusDiv, StatusTagsDiv, TagDiv, TitleParagraph } from "./styles"; +import { getLanguagesByPurpose } from "./helpers"; type Props = { opportunity: ApiVolunteerOpportunityGetList; @@ -17,16 +18,32 @@ type Props = { export function OpportunityReadOnlyCard({ opportunity, districtsList }: Props) { const { t } = useTranslation(); - const { title, volunteerType, district } = opportunity as ApiVolunteerOpportunityGetList & { + const { title, volunteerType, district, languages, statusMatch } = opportunity as ApiVolunteerOpportunityGetList & { accompanyingDetails?: { appointmentDate?: string; appointmentTime?: string }; statusMatch?: string; district?: { id: number }; }; + const mainCommunication = getLanguagesByPurpose(languages, LangPurpose.GENERAL); + const recipientLanguage = getLanguagesByPurpose(languages, LangPurpose.RECIPIENT); + const districtTitle = district?.id ? (districtsList?.find((d) => d.id === district.id)?.title ?? null) : null; return ( + {statusMatch && ( + + {matchStatusIconMap[statusMatch]} + + {t(`dashboard.opportunities.matchStatus.${statusMatch}`)} + + + )} {volunteerType && ( {title} + + {mainCommunication && ( + + + + + )} + {recipientLanguage && ( + + + + + )} + + {districtTitle && } diff --git a/src/components/Dashboard/Opportunities/OpportunityReadOnlyTableRow.tsx b/src/components/Dashboard/Opportunities/OpportunityReadOnlyTableRow.tsx index 364197c5..b6672f08 100644 --- a/src/components/Dashboard/Opportunities/OpportunityReadOnlyTableRow.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityReadOnlyTableRow.tsx @@ -1,9 +1,10 @@ "use client"; -import type { ApiVolunteerOpportunityGetList, OptionItem } from "need4deed-sdk"; +import { LangPurpose, type ApiVolunteerOpportunityGetList, type OptionItem } from "need4deed-sdk"; import { useTranslation } from "react-i18next"; import { ClickableRow, TableCell } from "@/components/core/common/Table"; -import { OPPORTUNITY_COL_WIDTHS } from "./opportunitiesTableColumns"; +import { OPPORTUNITY_READ_ONLY_COL_WIDTHS } from "./opportunitiesTableColumns"; +import { getLanguagesByPurpose } from "./helpers"; interface TableRowProps { opportunity: ApiVolunteerOpportunityGetList; @@ -15,15 +16,27 @@ interface TableRowProps { export function OpportunityReadOnlyTableRow({ opportunity, isLast, districtsList }: TableRowProps) { const { t } = useTranslation(); - const { id, title, volunteerType, district } = opportunity; + const { id, title, volunteerType, district, statusMatch, languages } = opportunity; + const mainCommunication = getLanguagesByPurpose(languages, LangPurpose.GENERAL); const districtTitle = district?.id ? (districtsList?.find((d) => d.id === district.id)?.title ?? null) : null; return ( - {title} - + + {title} + + {t(`dashboard.opportunities.type.${volunteerType}`)} - + + {t(`dashboard.opportunities.matchStatus.${statusMatch}`)} + + + {mainCommunication || "—"} + + {districtTitle || "—"} diff --git a/src/components/Dashboard/Opportunities/OpportunityTableList.tsx b/src/components/Dashboard/Opportunities/OpportunityTableList.tsx index e627534c..9e4aea5e 100644 --- a/src/components/Dashboard/Opportunities/OpportunityTableList.tsx +++ b/src/components/Dashboard/Opportunities/OpportunityTableList.tsx @@ -6,8 +6,8 @@ import { useTranslation } from "react-i18next"; import { EntityTableList } from "../common/EntityTableList"; import { createOpportunityTableColumns, createReadOnlyAgentTableColumns } from "./opportunitiesTableColumns"; import { OpportunityTableRow } from "./OpportunityTableRow"; -import { useCurrentUser } from "@/hooks/useCurrentUser"; import { OpportunityReadOnlyTableRow } from "./OpportunityReadOnlyTableRow"; +import { useAuth } from "@/hooks/useAuth"; interface TableListProps { opportunities: ApiVolunteerOpportunityGetList[]; @@ -29,7 +29,7 @@ export function OpportunityTableList({ activitiesList, }: TableListProps) { const { t } = useTranslation(); - const { isAuthorized } = useCurrentUser(); + const isAuthorized = useAuth(); const columns = useMemo(() => createOpportunityTableColumns(t), [t]); const readOnlyColumns = useMemo(() => createReadOnlyAgentTableColumns(t), [t]); diff --git a/src/components/Dashboard/Opportunities/opportunitiesTableColumns.ts b/src/components/Dashboard/Opportunities/opportunitiesTableColumns.ts index f966b987..c2d07c1a 100644 --- a/src/components/Dashboard/Opportunities/opportunitiesTableColumns.ts +++ b/src/components/Dashboard/Opportunities/opportunitiesTableColumns.ts @@ -15,6 +15,14 @@ export const OPPORTUNITY_COL_WIDTHS = { agentTitle: COLUMN_WIDTH.MD, }; +export const OPPORTUNITY_READ_ONLY_COL_WIDTHS = { + title: COLUMN_WIDTH.XXXL, + volunteerType: COLUMN_WIDTH.XXXL, + statusMatch: COLUMN_WIDTH.XXXL, + languages: COLUMN_WIDTH.XXXL, + district: COLUMN_WIDTH.XXXL, +}; + export const createOpportunityTableColumns = (t: TFunction): Column[] => [ { key: "title", label: t("dashboard.opportunities.table.title"), width: OPPORTUNITY_COL_WIDTHS.title }, { key: "volunteerType", label: t("dashboard.opportunities.table.type"), width: OPPORTUNITY_COL_WIDTHS.volunteerType }, @@ -41,7 +49,17 @@ export const createOpportunityTableColumns = (t: TFunction): Column[] => [ ]; export const createReadOnlyAgentTableColumns = (t: TFunction): Column[] => [ - { key: "title", label: t("dashboard.agents.table.title") }, - { key: "type", label: t("dashboard.agents.table.type"), width: OPPORTUNITY_COL_WIDTHS.volunteerType }, - { key: "district", label: t("dashboard.agents.table.district"), width: OPPORTUNITY_COL_WIDTHS.district }, + { key: "title", label: t("dashboard.agents.table.title"), width: OPPORTUNITY_READ_ONLY_COL_WIDTHS.title }, + { key: "type", label: t("dashboard.agents.table.type"), width: OPPORTUNITY_READ_ONLY_COL_WIDTHS.volunteerType }, + { + key: "statusMatch", + label: t("dashboard.opportunities.table.matchingStatus"), + width: OPPORTUNITY_READ_ONLY_COL_WIDTHS.statusMatch, + }, + { + key: "languages", + label: t("dashboard.opportunities.table.languages"), + width: OPPORTUNITY_READ_ONLY_COL_WIDTHS.languages, + }, + { key: "district", label: t("dashboard.agents.table.district"), width: OPPORTUNITY_READ_ONLY_COL_WIDTHS.district }, ]; diff --git a/src/components/Dashboard/common/EntityTableList/EntityTableList.tsx b/src/components/Dashboard/common/EntityTableList/EntityTableList.tsx index eb96b422..4617e8eb 100644 --- a/src/components/Dashboard/common/EntityTableList/EntityTableList.tsx +++ b/src/components/Dashboard/common/EntityTableList/EntityTableList.tsx @@ -14,6 +14,7 @@ export function EntityTableList({ currentPage, setCurrentPage, testIdPrefix, + noFixedWidth = false, }: EntityTableListProps) { const totalPages = Math.ceil(count / itemsPerPage); const goToPage = (page: number) => { @@ -26,7 +27,7 @@ export function EntityTableList({ {columns.map((col) => ( - + {col.label} {col.headerAction} diff --git a/src/components/Dashboard/common/EntityTableList/types.ts b/src/components/Dashboard/common/EntityTableList/types.ts index 4302e0bb..de04e214 100644 --- a/src/components/Dashboard/common/EntityTableList/types.ts +++ b/src/components/Dashboard/common/EntityTableList/types.ts @@ -16,4 +16,5 @@ export interface EntityTableListProps { currentPage: number; setCurrentPage: (page: number) => void; testIdPrefix: string; + noFixedWidth?: boolean; } diff --git a/src/components/forms/BecomeVolunteer/OpportunityTitle.tsx b/src/components/forms/BecomeVolunteer/OpportunityTitle.tsx new file mode 100644 index 00000000..4c6aeb06 --- /dev/null +++ b/src/components/forms/BecomeVolunteer/OpportunityTitle.tsx @@ -0,0 +1,19 @@ +import { useSearchParams } from "next/navigation"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { OpportunityInfo } from "../types"; + +export function OpportunityTitle() { + const { t } = useTranslation(); + const opportunityParams = useSearchParams(); + + const opportunity: OpportunityInfo = { + id: opportunityParams.get("id") || "", + title: opportunityParams.get("title") || "", + }; + return ( +
+ {t("form.becomeVolunteer.thanks")}: {opportunity.title} +
+ ); +} diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts new file mode 100644 index 00000000..55d2e055 --- /dev/null +++ b/src/hooks/useAuth.ts @@ -0,0 +1,16 @@ +import { UserRole } from "need4deed-sdk"; +import { useCurrentUser } from "./useCurrentUser"; +import { useEffect, useState } from "react"; + +export const useAuth = () => { + const [isAuthorized, setIsAuthorized] = useState(false); + + const user = useCurrentUser(); + + useEffect(() => { + const userIsAuthorized = user?.role === UserRole.ADMIN || user?.role === UserRole.COORDINATOR; + if (userIsAuthorized) setIsAuthorized(true); + }, [user]); + + return isAuthorized; +}; diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/useCurrentUser.ts index e33c1d45..69b14753 100644 --- a/src/hooks/useCurrentUser.ts +++ b/src/hooks/useCurrentUser.ts @@ -1,7 +1,7 @@ import { apiPathMe, cacheTTL } from "@/config/constants"; import { useGetQuery } from "@/hooks"; import { getCookie } from "@/utils/helpers"; -import { ApiUserGet, UserRole } from "need4deed-sdk"; +import { ApiUserGet } from "need4deed-sdk"; export const useCurrentUser = (enabled?: boolean) => { const hasAuthHint = getCookie("is_logged_in") === "true"; @@ -13,7 +13,5 @@ export const useCurrentUser = (enabled?: boolean) => { enabled: hasAuthHint && enabled, }); - const isAuthorized = data?.role === UserRole.ADMIN || UserRole.COORDINATOR; - - return { ...data, isAuthorized } as ApiUserGet & { isAuthorized: boolean }; + return data; }; From d5a3b09aeb7284d20fb98e9551bc1adc0dad16a7 Mon Sep 17 00:00:00 2001 From: DarrellRoberts Date: Sat, 13 Jun 2026 00:01:27 +0200 Subject: [PATCH 4/5] reverts unneccessary file --- .../BecomeVolunteer/OpportunityTitle.tsx | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/components/forms/BecomeVolunteer/OpportunityTitle.tsx diff --git a/src/components/forms/BecomeVolunteer/OpportunityTitle.tsx b/src/components/forms/BecomeVolunteer/OpportunityTitle.tsx deleted file mode 100644 index 4c6aeb06..00000000 --- a/src/components/forms/BecomeVolunteer/OpportunityTitle.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useSearchParams } from "next/navigation"; -import React from "react"; -import { useTranslation } from "react-i18next"; -import { OpportunityInfo } from "../types"; - -export function OpportunityTitle() { - const { t } = useTranslation(); - const opportunityParams = useSearchParams(); - - const opportunity: OpportunityInfo = { - id: opportunityParams.get("id") || "", - title: opportunityParams.get("title") || "", - }; - return ( -
- {t("form.becomeVolunteer.thanks")}: {opportunity.title} -
- ); -} From 4e644178578f330f6fcac99f01b82ba357a5df89 Mon Sep 17 00:00:00 2001 From: DarrellRoberts Date: Sat, 13 Jun 2026 01:02:22 +0200 Subject: [PATCH 5/5] adds return func to unmount in useAuth --- src/hooks/useAuth.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 55d2e055..14e465ba 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -10,6 +10,8 @@ export const useAuth = () => { useEffect(() => { const userIsAuthorized = user?.role === UserRole.ADMIN || user?.role === UserRole.COORDINATOR; if (userIsAuthorized) setIsAuthorized(true); + + return () => setIsAuthorized(false); }, [user]); return isAuthorized;