diff --git a/package.json b/package.json
index fecc5445..51035ddc 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
"bootstrap": "^5.3.3",
"email-validator": "^2.0.4",
"i18next": "^23.10.1",
- "need4deed-sdk": "",
+ "need4deed-sdk": "*",
"react": "^18.2.0",
"react-bootstrap": "^2.10.1",
"react-cookie-consent": "^9.0.0",
diff --git a/src/components/Event/Event.tsx b/src/components/Event/Event.tsx
index d0a2aeec..b5ef0043 100644
--- a/src/components/Event/Event.tsx
+++ b/src/components/Event/Event.tsx
@@ -1,84 +1,48 @@
-import { EventN4D, Lang } from "need4deed-sdk";
+import { EventN4D } from "need4deed-sdk";
import { useTranslation } from "react-i18next";
-
-import { getImageUrl, getTimeFrameString } from "../../utils";
+import PastEventCard from "./PastEventCard/PastEventCard";
+import UpcomingEventCard from "./UpcomingEventCard/UpcomingEventCard";
interface Props {
- eventData: { event?: EventN4D };
+ eventData: EventN4D[];
}
-
const fallbackPicUrl = "event.webp";
export default function Event({ eventData }: Props) {
- const { event } = eventData || { event: undefined };
- const {
- i18n: { language },
- t,
- } = useTranslation();
+ const { t } = useTranslation();
- if (!event) {
+ if (!eventData || eventData.length === 0) {
return
-
{event.title}
-
{event.subTitle}
-
- {event.hostName}
-
- {event.time && (
-
- {event.time}
-
+
+ {upcomingEvents.length > 0 && (
+
+ {upcomingEvents.map((event) => (
+
+ ))}
+
)}
- {event.date && (
-
- {getTimeFrameString(language as Lang, event.date, event.dateEnd)}
-
+ {pastEvents.length > 0 && (
+
+ {pastEvents.map((event) => (
+
+ ))}
+
)}
-
-
})
-
{event.address.replace(/\\n/g, "\n")}
-
{event.locationComment}
-
-
- {t("event.locationLink")}
-
-
-
-
-
-
-
- {event.description.replace(/\\n/g, "\n")}
-
-
{event.additionalTitle}
-
- {event.additionalInfo &&
- event.additionalInfo.map((info) => (
- -
-
{info}
-
- ))}
-
-
-
);
}
diff --git a/src/components/Event/PastEventCard/PastEventCard.module.css b/src/components/Event/PastEventCard/PastEventCard.module.css
new file mode 100644
index 00000000..d532cc39
--- /dev/null
+++ b/src/components/Event/PastEventCard/PastEventCard.module.css
@@ -0,0 +1,136 @@
+.pastEventCard {
+ margin-bottom: 4rem;
+}
+
+
+.eventContent {
+ display: flex;
+ gap: 2rem;
+ margin-block: 1rem;
+ width: 100%;
+ flex-wrap: wrap;
+}
+
+.eventContent h1 {
+ margin-block: 1rem 0;
+}
+
+
+
+.eventContent strong {
+ font-weight: 700;
+}
+
+.eventImage {
+ width: 328px;
+ height: 328px;
+ border-radius: 8px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ flex-shrink: 0;
+}
+
+
+.eventStatus {
+ align-items: center;
+ font-size: 14px;
+ padding: 0.375rem 1rem;
+ background: var(--color-white);
+ width: fit-content;
+ margin-bottom: 1rem;
+}
+
+.eventStatus p {
+ color: var(--color-midnight-bright);
+ margin: 0;
+ font-weight: 600;
+}
+
+.eventTitle {
+ font-size: 32px;
+ font-weight: 700;
+ color: var(--color-midnight);
+}
+
+.eventSubtitle {
+ font-size: 14px;
+ font-weight: 550;
+ color: var(--color-midnight);
+}
+
+.eventDate h6 {
+ color: var(--color-midnight);
+ font-size: 20px;
+ font-weight: 600;
+ margin: 2rem auto;
+}
+
+
+.eventDescription {
+ font-size: 20px;
+ font-weight: 400;
+ white-space: pre-line;
+ margin-top: 1rem;
+ color: var(--color-midnight);
+}
+
+.additionalTitle {
+ margin-top: 1rem;
+ font-weight: bold;
+ color: var(--color-midnight);
+}
+
+
+.additionalInfoList {
+ margin-top: 1rem;
+ padding-left: 20px;
+}
+
+.additionalInfoItem {
+ margin-bottom: 0.5rem;
+ color: var(--color-midnight);
+}
+
+
+
+@media (max-width: 1024px) {
+ .eventDetails {
+ width: 350px
+ }
+}
+
+/* Responsive styles */
+@media (max-width: 576px) {
+ .eventContent {
+ grid-template-columns: unset;
+ gap: 1rem;
+ }
+
+
+
+ .eventImage {
+ height: 200px;
+ width: 200px;
+ }
+
+ .eventImage {
+ width: 328px;
+ height: 328px;
+ border-radius: 8px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ flex-shrink: 0;
+ }
+
+ .eventDescription {
+ font-size: 16px;
+ font-weight: 400;
+ white-space: pre-line;
+ margin-top: 1rem;
+ color: var(--color-midnight);
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/components/Event/PastEventCard/PastEventCard.tsx b/src/components/Event/PastEventCard/PastEventCard.tsx
new file mode 100644
index 00000000..9040d9ae
--- /dev/null
+++ b/src/components/Event/PastEventCard/PastEventCard.tsx
@@ -0,0 +1,50 @@
+import { EventN4D } from "need4deed-sdk";
+import { useTranslation } from "react-i18next";
+import { formatDateRange, getImageUrl } from "../../../utils";
+import styles from "./PastEventCard.module.css";
+
+interface PastEventCardProps {
+ event: EventN4D;
+ fallbackPicUrl: string;
+}
+
+function PastEventCard({ event, fallbackPicUrl }: PastEventCardProps) {
+ const { t } = useTranslation();
+ const eventStatus = event.active
+ ? t("homepage.events.headLine")
+ : t("event.past");
+
+ return (
+
+
+
+
+
+
{event.title}
+
+ {event.date && (
+
+
+ {formatDateRange(
+ new Date(event.date),
+ event.dateEnd && new Date(event.dateEnd),
+ )}
+
+
+ )}
+
{event.shortDescription}
+
+
+
+ );
+}
+
+export default PastEventCard;
diff --git a/src/components/Event/UpcomingEventCard/UpcomingEventCard.module.css b/src/components/Event/UpcomingEventCard/UpcomingEventCard.module.css
new file mode 100644
index 00000000..15226819
--- /dev/null
+++ b/src/components/Event/UpcomingEventCard/UpcomingEventCard.module.css
@@ -0,0 +1,197 @@
+.eventContainer {
+ background: var(--layout-static-page-background-default);
+ color: var(--color-midnight);
+
+
+}
+
+/* Ensure all child elements inherit the default text color */
+.eventContainer h1,
+.eventContainer h2,
+.eventContainer h3,
+.eventContainer h4,
+.eventContainer h5,
+.eventContainer h6,
+.eventContainer p,
+.eventContainer li {
+ color: inherit;
+}
+
+.eventTitle {
+ font-weight: 700;
+ font-size: 32px;
+ margin-bottom: 1rem;
+}
+
+
+.eventSubtitle {
+ font-weight: 600;
+ letter-spacing: 0.5%;
+ font-size: 20px;
+ text-transform: uppercase;
+ margin-bottom: 1rem;
+}
+
+.eventDate h6 {
+ font-weight: 600;
+ font-size: 20px;
+ margin-bottom: 1rem;
+}
+
+.eventAddress {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.eventAddress h6,
+.eventAddress span {
+ font-weight: 500;
+ font-size: 20px;
+}
+
+.cityZip {
+ background-color: var(--color-white);
+ padding: 6px 8px;
+ border-radius: 4px;
+}
+
+.locationComment {
+ font-weight: 400;
+ font-size: 20px;
+ margin-bottom: 1rem;
+}
+
+
+.eventImage {
+ width: 100%;
+ max-width: 880px;
+ height: auto;
+ aspect-ratio: 16 / 9;
+ border-radius: 8px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ flex-shrink: 0;
+ margin: 2rem 0;
+}
+
+.eventDescription h6 {
+ white-space: pre-wrap;
+ font-weight: 600;
+ font-size: 20px;
+ margin-bottom: 1rem;
+}
+
+.additionalInfoList {
+ font-size: 20px;
+ font-weight: 400;
+ margin-top: 1rem;
+}
+
+.eventStatus {
+ align-items: center;
+ padding: 0.375rem 1rem;
+ background: var(--color-white);
+ width: fit-content;
+ margin-bottom: 1rem;
+ border-radius: 4px;
+}
+
+.eventStatus p {
+ color: var(--n4d-secondary-darker);
+ margin: 0;
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.eventActions {
+ margin: 3rem 0;
+ display: flex;
+}
+
+/* Responsive styles for tablets and mobile devices */
+@media (max-width: 1024px) {
+ .eventContainer {
+ padding: 1.5rem;
+ }
+
+ .eventTitle {
+ font-size: 28px;
+ }
+
+ .eventSubtitle {
+ font-size: 18px;
+ }
+
+ .eventDate h6 {
+ font-size: 18px;
+ }
+
+ .eventAddress h6,
+ .eventAddress span {
+ font-size: 18px;
+ }
+
+ .eventDescription h6 {
+ font-size: 18px;
+ }
+
+ .additionalInfoList {
+ font-size: 18px;
+ }
+
+ .eventImage {
+ width: 100%;
+ height: auto;
+ aspect-ratio: 16 / 9;
+ }
+}
+
+@media (max-width: 576px) {
+ .eventContainer {
+ padding: 1rem;
+ }
+
+ .eventTitle {
+ font-size: 24px;
+ }
+
+ .eventSubtitle {
+ font-size: 16px;
+ }
+
+ .eventDate h6 {
+ font-size: 16px;
+ }
+
+ .eventAddress {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .eventAddress h6,
+ .eventAddress span {
+ font-size: 16px;
+ }
+
+ .eventDescription h6 {
+ font-size: 16px;
+ }
+
+ .additionalInfoList {
+ font-size: 16px;
+ }
+
+ .eventImage {
+ width: 100%;
+ height: auto;
+ aspect-ratio: 4 / 3;
+ }
+
+ .eventActions {
+ flex-direction: column;
+ gap: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/src/components/Event/UpcomingEventCard/UpcomingEventCard.tsx b/src/components/Event/UpcomingEventCard/UpcomingEventCard.tsx
new file mode 100644
index 00000000..560311fb
--- /dev/null
+++ b/src/components/Event/UpcomingEventCard/UpcomingEventCard.tsx
@@ -0,0 +1,79 @@
+import { EventN4D } from "need4deed-sdk";
+import { useTranslation } from "react-i18next";
+import { formatDateRange, getImageUrl } from "../../../utils";
+import styles from "./UpcomingEventCard.module.css";
+import { Button } from "../../core/button";
+
+interface UpcomingEventCardProps {
+ event: EventN4D;
+ fallbackPicUrl: string;
+}
+
+function UpcomingEventCard({ event, fallbackPicUrl }: UpcomingEventCardProps) {
+ const { t } = useTranslation();
+
+ const eventStatus = event.active
+ ? t("homepage.events.headLine")
+ : t("event.past");
+
+ const [street, cityZip, floor] = event.address.split("\\n");
+ const formattedAddress = `${street}, ${floor}`;
+
+ return (
+
+
+
+
{event.title}
+
{event.subTitle}
+
+ {event.date && (
+
+
+ {formatDateRange(
+ new Date(event.date),
+ event.dateEnd && new Date(event.dateEnd),
+ )}
+
+
+ )}
+
+
+
{formattedAddress}
+ {cityZip}
+
+
+
{event.locationComment}
+
+
+
{event.description.replace(/\\n/g, "\n")}
+
+
{event.additionalTitle}
+
+ {event.additionalInfo &&
+ event.additionalInfo.map((info) => (
+ -
+
{info}
+
+ ))}
+
+
+
+
+
+ );
+}
+
+export default UpcomingEventCard;
diff --git a/src/components/Event/index.css b/src/components/Event/index.css
index fc4d5295..2235d9d1 100644
--- a/src/components/Event/index.css
+++ b/src/components/Event/index.css
@@ -1,111 +1,9 @@
-.event-container h1 {
- margin-block: 1rem 0;
-}
-
-.event-container h6 {
- font-weight: 550;
-}
-
-.event-container strong {
- font-weight: 700;
-}
-
-.event-timeslot {
- font-weight: bold;
-}
-
-.event-location {
- margin-block-start: 2rem;
-}
-
-.event-list-container {
- margin-block-start: 2rem;
-}
-
-.bullet-list {
- margin-block-start: 1rem;
-}
-
-.bullet-list li {
- margin-block-end: 0.2rem;
-}
-
-.event-rsvp {
+.n4d-section {
display: flex;
flex-direction: column;
- align-items: center;
- margin-block-start: 2rem;
+ gap: 5rem
}
-.event-rsvp p:first-of-type {
- margin-block: 0;
-}
-
-.event-rsvp p:nth-of-type(2) {
- margin-block: 1rem;
-}
-
-.event-rsvp a {
- display: block;
- width: fit-content;
- text-decoration: none;
- color: var(--n4d-neutral-900);
- background: var(--n4d-primary);
- padding: 0.3rem;
- border-radius: 0.3rem;
-}
-
-.event-rsvp a:hover {
- background: var(--n4d-primary-darker);
-}
-
-.pic-and-text {
- display: grid;
- grid-template-columns: 1fr 2fr;
- gap: 1rem;
- margin-block: 1rem;
- width: 100%;
-}
-
-.event-donate {
-}
-
-.event-donate a {
- color: var(--n4d-primary);
-}
-
-.event-lines-together {
- display: flex;
- flex-direction: column;
- gap: 0.15rem;
-}
-
-.event-lines-together * {
- margin-block: 0;
- padding-block: 0;
-}
-
-.event-pic {
- background-size: cover;
- background-repeat: no-repeat;
- width: 100%;
- height: 100%;
-}
-
-@media (max-width: 576px) {
- .pic-and-text {
- grid-template-columns: unset;
- }
- .event-pic {
- height: 12rem;
- }
- .sommer-fest-pic {
- width: 100%;
- height: 15rem;
- }
-}
-
-.volunteerEmail {
- color: var(--n4d-tertiary);
- text-decoration: underline;
-}
+.upcoming-events {
+ background: var(--layout-static-page-background-default);
+}
\ No newline at end of file
diff --git a/src/components/FooterPartners/footer/ContactSocials.tsx b/src/components/FooterPartners/footer/ContactSocials.tsx
index e008ffeb..fcae0be4 100644
--- a/src/components/FooterPartners/footer/ContactSocials.tsx
+++ b/src/components/FooterPartners/footer/ContactSocials.tsx
@@ -2,6 +2,7 @@ import {
FacebookLogo,
InstagramLogo,
LinkedinLogo,
+ ApplePodcastsLogo,
} from "@phosphor-icons/react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
@@ -83,6 +84,13 @@ function Socials() {
color="var(--color-orchid-light)"
/>
+
+
+
);
}
diff --git a/src/components/OpportunityCards/Filters/constants.tsx b/src/components/OpportunityCards/Filters/constants.tsx
index 1648a87f..bebb7fcc 100644
--- a/src/components/OpportunityCards/Filters/constants.tsx
+++ b/src/components/OpportunityCards/Filters/constants.tsx
@@ -1,6 +1,5 @@
-import { CardsFilter } from "../types";
+import { CardFilterKeys, CardsFilter } from "../types";
-/* eslint-disable import/prefer-default-export */
export const defaultFilter: CardsFilter = {
searchInput: "",
accompanying: false,
@@ -44,3 +43,19 @@ export const defaultFilter: CardsFilter = {
},
},
};
+
+export const FILTER_KEY = {
+ SEARCH_INPUT: "searchInput",
+ ACTIVITY_TYPE: "activityType",
+ DISTRICT: "district",
+ DAYS: "days",
+ ACCOMPANYING: "accompanying",
+} as const satisfies Record
;
+
+export const FILTER_KEY_LIST = Object.values(FILTER_KEY);
+
+export type FilterKey = (typeof FILTER_KEY)[keyof typeof FILTER_KEY];
+
+export const DASH = "-";
+
+export const langQueryParamKey = "language";
diff --git a/src/components/OpportunityCards/OpportunityCards.tsx b/src/components/OpportunityCards/OpportunityCards.tsx
index 1fe93ffb..1926df1b 100644
--- a/src/components/OpportunityCards/OpportunityCards.tsx
+++ b/src/components/OpportunityCards/OpportunityCards.tsx
@@ -1,3 +1,4 @@
+import { useLocation, useSearchParams } from "react-router-dom";
import styled from "styled-components";
import { Lang, OpportunityType } from "need4deed-sdk";
import { useTranslation } from "react-i18next";
@@ -7,7 +8,9 @@ import { urlApiOpportunity } from "../../config/constants";
import OpportunityCardsHeader from "./OpportunityCardsHeader";
import MapView from "./MapView";
import Filters from "./Filters/Filters";
-import { defaultFilter } from "./Filters/constants";
+import { defaultFilter, FILTER_KEY_LIST } from "./Filters/constants";
+import { CardsFilter } from "./types";
+import { deserializeFilters, openFilters, serializeFilters } from "./helpers";
const OpportunitiesContainer = styled.div`
display: flex;
@@ -21,14 +24,47 @@ const OpportunitiesContainer = styled.div`
`;
export function OpportunityCards() {
+ const location = useLocation();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const query = new URLSearchParams(location.search);
const { i18n, t } = useTranslation();
+ const language = i18n.language as Lang;
const [numOfOpportunities, setNumOfOpportunities] = useState(0);
const [cardsFilter, setCardsFilter] = useState(defaultFilter);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
+ const handleFilterUpdate = (
+ newFilter: CardsFilter | ((prev: CardsFilter) => CardsFilter),
+ ) => {
+ const updatedFilter =
+ typeof newFilter === "function" ? newFilter(cardsFilter) : newFilter;
+
+ setCardsFilter(updatedFilter);
+
+ setSearchParams(serializeFilters(updatedFilter, language));
+ };
+
+ const initializeFilter = (
+ incomingFilter: CardsFilter | ((prev: CardsFilter) => CardsFilter),
+ ) => {
+ const baseFilter =
+ typeof incomingFilter === "function"
+ ? incomingFilter(cardsFilter)
+ : incomingFilter;
+
+ const hasFilterParams = FILTER_KEY_LIST.some((key) => query.has(key));
+
+ const finalFilter = hasFilterParams
+ ? deserializeFilters(query, baseFilter)
+ : baseFilter;
+
+ setCardsFilter(finalFilter);
+ setIsFiltersOpen(isFiltersOpen || openFilters(searchParams));
+ };
+
const onSearchInputChange = (searchInput: string) => {
- setCardsFilter({ ...cardsFilter, searchInput });
+ handleFilterUpdate((prev) => ({ ...prev, searchInput }));
};
const tabs = [t("opportunityPage.tabs.tab1"), t("opportunityPage.tabs.tab2")];
@@ -37,7 +73,7 @@ export function OpportunityCards() {
@@ -45,6 +81,7 @@ export function OpportunityCards() {
// Todo: temporarily just show numOfOpportunities as 0. when map view is available refactor below line.
numOfOpportunities={selectedTabIndex === 0 ? numOfOpportunities : 0}
onSearchInputChange={onSearchInputChange}
+ cardsFilter={cardsFilter}
tabs={tabs}
selectedTabIndex={selectedTabIndex}
setSelectedTabIndex={setSelectedTabIndex}
@@ -63,12 +100,12 @@ export function OpportunityCards() {
],
},
primaryKeys: ["title", "name"],
- language: i18n.language as Lang,
+ language,
}}
popup
setNumOfOpportunities={setNumOfOpportunities}
cardsFilter={cardsFilter}
- setCardsFilter={setCardsFilter}
+ setCardsFilter={initializeFilter}
isFiltersOpen={isFiltersOpen}
/>
) : (
diff --git a/src/components/OpportunityCards/OpportunityCardsHeader.tsx b/src/components/OpportunityCards/OpportunityCardsHeader.tsx
index 8a43dc9d..bea559fa 100644
--- a/src/components/OpportunityCards/OpportunityCardsHeader.tsx
+++ b/src/components/OpportunityCards/OpportunityCardsHeader.tsx
@@ -7,6 +7,7 @@ import useScreenType from "../../hooks/useScreenType";
import { ScreenTypes } from "../../config/types";
import ResultsFound from "./ResultsFound";
import { hyphenationStyles } from "../styled/mixins";
+import { CardsFilter } from "./types";
const HeaderContainer = styled.div`
display: flex;
@@ -64,6 +65,7 @@ interface Props {
selectedTabIndex: number;
setSelectedTabIndex: (index: number) => void;
setIsFiltersOpen: (isOpen: boolean) => void;
+ cardsFilter?: CardsFilter;
}
export default function OpportunityCardsHeader({
@@ -73,11 +75,11 @@ export default function OpportunityCardsHeader({
setSelectedTabIndex,
tabs,
setIsFiltersOpen,
+ cardsFilter,
}: Props) {
const { t } = useTranslation();
const screenSize = useScreenType();
const isMobile = screenSize === ScreenTypes.MOBILE;
-
return (
{t("opportunityPage.header")}
@@ -108,6 +110,7 @@ export default function OpportunityCardsHeader({
placeHolder={`${t("opportunityPage.searchPlaceHolder")} ...`}
onInputChange={onSearchInputChange}
width="var(--opportunities-header-searchbar-width)"
+ value={cardsFilter?.searchInput ?? ""}
/>
{isMobile ? (
diff --git a/src/components/OpportunityCards/helpers.ts b/src/components/OpportunityCards/helpers.ts
index bb19d24c..bc04e4cb 100644
--- a/src/components/OpportunityCards/helpers.ts
+++ b/src/components/OpportunityCards/helpers.ts
@@ -1,10 +1,17 @@
/* eslint-disable no-restricted-syntax */
-import { OpportunityType } from "need4deed-sdk";
+import { Lang, OpportunityType } from "need4deed-sdk";
import { TFunction } from "i18next";
import { Opportunity } from "../VolunteeringOpportunities/types";
import { CardsFilter, Day, DayKeys, Days, DaysKeys } from "./types";
-import { TimeSlot } from "../forms/types";
+import { TimeSlot, Weekday } from "../forms/types";
import { CategoryTitle } from "../VolunteeringOpportunities/utils";
+import {
+ DASH,
+ FILTER_KEY,
+ FILTER_KEY_LIST,
+ FilterKey,
+ langQueryParamKey,
+} from "./Filters/constants";
const dayEnumMap: Record = {
1: "monday",
@@ -244,3 +251,111 @@ export const extractCardsFilter = (
};
export const isObjectEmpty = (obj: object) => Object.keys(obj).length === 0;
+
+export const hasKey = (
+ obj: T | null | undefined,
+ key: PropertyKey,
+): key is keyof T => !!obj && Object.prototype.hasOwnProperty.call(obj, key);
+
+export function serializeFilters(filters: CardsFilter, language: Lang) {
+ const params = new URLSearchParams();
+
+ if (filters.searchInput) {
+ params.set(FILTER_KEY.SEARCH_INPUT, filters.searchInput);
+ }
+
+ if (filters.accompanying) {
+ params.set(FILTER_KEY.ACCOMPANYING, "true");
+ }
+
+ if (filters.activityType) {
+ Object.entries(filters.activityType).forEach(([key, value]) => {
+ if (value === true) {
+ params.append(FILTER_KEY.ACTIVITY_TYPE, key);
+ }
+ });
+ }
+
+ if (filters.district) {
+ Object.entries(filters.district).forEach(([key, value]) => {
+ if (value === true) {
+ params.append(FILTER_KEY.DISTRICT, key);
+ }
+ });
+ }
+
+ if (filters.days) {
+ Object.entries(filters.days).forEach(([day, timeSlots]) => {
+ const dayKey = day as Weekday;
+
+ Object.entries(timeSlots as TimeSlot).forEach(([slot, value]) => {
+ if (value) {
+ params.append(FILTER_KEY.DAYS, `${dayKey}${DASH}${slot}`);
+ }
+ });
+ });
+ }
+
+ if (params.size) params.set(langQueryParamKey, language);
+ else params.delete(langQueryParamKey, language);
+
+ return params;
+}
+
+export function deserializeFilters(
+ query: URLSearchParams,
+ filter: CardsFilter,
+): CardsFilter {
+ const filters: CardsFilter = structuredClone(filter);
+
+ const search = query.get(FILTER_KEY.SEARCH_INPUT);
+ if (search !== null) {
+ filters.searchInput = search;
+ }
+
+ const accompanying = query.get(FILTER_KEY.ACCOMPANYING);
+ if (accompanying === "true") {
+ filters.accompanying = true;
+ }
+
+ const activityTypes = query.getAll(FILTER_KEY.ACTIVITY_TYPE);
+ activityTypes.forEach((type) => {
+ if (hasKey(filters.activityType, type)) {
+ filters.activityType[type] = true;
+ }
+ });
+
+ const districts = query.getAll(FILTER_KEY.DISTRICT);
+ districts.forEach((dist) => {
+ if (hasKey(filters.district, dist)) {
+ filters.district[dist] = true;
+ }
+ });
+
+ const daySlots = query.getAll(FILTER_KEY.DAYS);
+ daySlots.forEach((slot) => {
+ const [day, time] = slot.split(DASH);
+ const dayKey = day as DaysKeys;
+ const timeKey = time as DayKeys;
+
+ if (filters.days[dayKey] && filters.days[dayKey][timeKey] !== undefined) {
+ filters.days[dayKey][timeKey] = true;
+ }
+ });
+
+ return filters;
+}
+
+export const getFilterKeysExcluding = (
+ exclude: FilterKey[] = [],
+): FilterKey[] => {
+ return FILTER_KEY_LIST.filter((key) => !exclude.includes(key));
+};
+
+export const openFilters = (searchParams: URLSearchParams) => {
+ const hasRelevantFilters = getFilterKeysExcluding([
+ FILTER_KEY.SEARCH_INPUT,
+ ]).some((key) => searchParams.has(key));
+
+ return hasRelevantFilters;
+};
diff --git a/src/components/OpportunityCards/types.ts b/src/components/OpportunityCards/types.ts
index 9bc40e35..344daa3b 100644
--- a/src/components/OpportunityCards/types.ts
+++ b/src/components/OpportunityCards/types.ts
@@ -26,5 +26,6 @@ export interface Day {
export type DaysKeys = keyof Days;
export type DayKeys = keyof Day;
+export type CardFilterKeys = keyof CardsFilter;
export type SetFilter = Dispatch>;
diff --git a/src/components/core/common/Search.tsx b/src/components/core/common/Search.tsx
index 48ad9f03..f0c702ea 100644
--- a/src/components/core/common/Search.tsx
+++ b/src/components/core/common/Search.tsx
@@ -1,6 +1,6 @@
import styled from "styled-components";
import { MagnifyingGlassIcon } from "@phosphor-icons/react";
-import { ChangeEvent, useState } from "react";
+import { ChangeEvent } from "react";
interface SearchContainerProps {
width?: string;
@@ -21,7 +21,6 @@ const SearchContainer = styled.div`
const StyledInput = styled.input`
font-size: var(--search-input-font-size);
border: none;
-
&:focus {
outline: none;
}
@@ -31,26 +30,24 @@ interface Props {
placeHolder?: string;
onInputChange: (input: string) => void;
width?: string;
+ value?: string;
}
export function Search({
placeHolder = "Search",
onInputChange,
width,
+ value,
}: Props) {
- const [inputValue, setInputValue] = useState("");
-
const handleInputChange = (e: ChangeEvent) => {
- const newValue = e.target.value;
- setInputValue(newValue);
- onInputChange(newValue);
+ onInputChange(e.target.value);
};
return (
diff --git a/src/components/forms/utils.ts b/src/components/forms/utils.ts
index 1df88c19..4ee7811c 100644
--- a/src/components/forms/utils.ts
+++ b/src/components/forms/utils.ts
@@ -45,7 +45,7 @@ export function getSelectedIds(state: Selected[]): OptionId[] {
}
export function parseFormStateDTOVolunteer(value: VolunteerData) {
- const data = {} as VolunteerParsedData;
+ const data: Record = {};
data.origin_opportunity = value.opportunityId
? +value.opportunityId
: undefined;
@@ -66,7 +66,7 @@ export function parseFormStateDTOVolunteer(value: VolunteerData) {
data.comments = value.comments;
data.language = value.language;
- return data;
+ return data as unknown as VolunteerParsedData;
}
export function parseFormStateDTOOpportunity(value: OpportunityData) {
diff --git a/src/config/i18next.ts b/src/config/i18next.ts
index 4fdf688c..81cb5bb7 100644
--- a/src/config/i18next.ts
+++ b/src/config/i18next.ts
@@ -4,11 +4,17 @@ import { initReactI18next } from "react-i18next";
import legal from "../../public/locales/de/legal.json";
import deTranslation from "../../public/locales/de/translations.json";
import enTranslation from "../../public/locales/en/translations.json";
-import { getStoredLang } from "../utils";
+import { getQueryParamLang, getStoredLang, setStoredLang } from "../utils";
import { Env } from "./types";
+const urlLang = getQueryParamLang();
+const storedLang = getStoredLang();
+const initialLang = urlLang || storedLang || Lang.EN;
+
+setStoredLang(initialLang);
+
i18next.use(initReactI18next).init({
- lng: getStoredLang() || Lang.EN,
+ lng: initialLang,
fallbackLng: Lang.EN,
debug: process.env.NODE_ENV === Env.DEVELOP,
resources: {
diff --git a/src/pages/EventPage.tsx b/src/pages/EventPage.tsx
index 9a7a426e..5c77482a 100644
--- a/src/pages/EventPage.tsx
+++ b/src/pages/EventPage.tsx
@@ -1,5 +1,4 @@
import { Lang } from "need4deed-sdk";
-import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import Event from "../components/Event/Event";
@@ -10,13 +9,9 @@ export default function EventPage() {
const { i18n } = useTranslation();
const [events] = useEvents(i18n.language as Lang);
- const eventActive = useMemo(
- () => events?.find((event) => event.active),
- [events],
- );
return (
-
+
);
}
diff --git a/src/pages/Subpage.tsx b/src/pages/Subpage.tsx
index 53bf461f..90d90454 100644
--- a/src/pages/Subpage.tsx
+++ b/src/pages/Subpage.tsx
@@ -1,5 +1,5 @@
import { Lang } from "need4deed-sdk";
-import { useContext, useEffect, useMemo } from "react";
+import { useContext, useEffect } from "react";
import ReactGA from "react-ga4";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
@@ -34,11 +34,6 @@ function Subpage({ type }: Props) {
const containerRef = useContext(AppContainerContext);
const [events] = useEvents(i18n.language as Lang);
- const eventActive = useMemo(
- () => events?.find((event) => event.active),
- [events],
- );
-
useEffect(() => {
if (isEnumValue(Lang, lng)) {
i18n.changeLanguage(lng);
@@ -70,7 +65,7 @@ function Subpage({ type }: Props) {
case Subpages.ANNOUNCEMENT:
return ;
case Subpages.EVENT:
- return ;
+ return ;
case Subpages.EVENTS:
return ;
case Subpages.COOKIES:
diff --git a/src/utils/index.ts b/src/utils/index.ts
index f47d68ce..ce4a09b2 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -470,3 +470,10 @@ export function setStoredLang(lang: Lang) {
console.warn(`Invalid language code: ${lang}`);
}
}
+
+export function getQueryParamLang(): Lang | null {
+ const params = new URLSearchParams(window.location.search);
+ const lang = params.get("lang") as Lang;
+
+ return Object.values(Lang).includes(lang) ? lang : null;
+}
diff --git a/yarn.lock b/yarn.lock
index 43be9d47..9524124e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10947,10 +10947,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@:
- version "0.0.12"
- resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.12.tgz#8d66a411f4ef6ce3ed5cb867457e51faf63df74c"
- integrity sha512-SxTPpBMnbOD8csadN1UV/li7lXKPa9psUB0HexmC1o6PDIowMcp9ky+Ylw1dHAlQVJCYm7JdpNYq/YMobguMQA==
+need4deed-sdk@*:
+ version "0.0.14"
+ resolved "https://registry.yarnpkg.com/need4deed-sdk/-/need4deed-sdk-0.0.14.tgz#4102b41266689760f0294c4b6c64f550e7c02583"
+ integrity sha512-E7fl2uCp6ScsUNDDkhVc9iPgB9IytE6fl2+N6oeZdLNF0lult8qr39s1yGJO7+OoOS14HQLQ/JZdy+7bvjKDtg==
neo-async@^2.6.0:
version "2.6.2"