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

{t("event.missing")}

; } + const upcomingEvents = eventData.filter((event) => event.active); + const pastEvents = eventData.filter((event) => !event.active); + 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")} - -

-
- {t("event.rsvp")}:{" "} - - {t("event.registration")} - -
-
-
-
-
- {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 ( +
+
+
+
+
+

{eventStatus}

+
+

{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 ( +
+
+
+

{eventStatus}

+
+

{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"