From bb923e79ee7dd85fc19d9bc9ab8c145dad4e81a6 Mon Sep 17 00:00:00 2001 From: Justyna Bereza Date: Tue, 1 Sep 2026 15:25:13 +0200 Subject: [PATCH] feat: implement landing page events section (#917) --- public/locales/de/translations.json | 6 + public/locales/en/translations.json | 6 + .../EventsSection/EventsSection.tsx | 166 +++++++++++++++++- src/config/constants.ts | 2 +- 4 files changed, 178 insertions(+), 2 deletions(-) diff --git a/public/locales/de/translations.json b/public/locales/de/translations.json index fd5f891f..e4dc734d 100644 --- a/public/locales/de/translations.json +++ b/public/locales/de/translations.json @@ -32,6 +32,12 @@ "iframeTitles": { "howToVolunteer": "Wie Du Geflüchtete in Deiner Nachbarschaft unterstützen können" }, + "events": { + "headline": "Kommende Veranstaltungen", + "button": "Mehr erfahren & anmelden", + "loading": "Kommende Veranstaltungen werden geladen…", + "empty": "Aktuell ist keine kommende Veranstaltung verfügbar." + }, "testimonials": { "header": "ERFAHRUNGSBERICHTE", "headerParagraph": "Was Ehrenamtliche und Unterkünfte sagen" diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 58f8ea39..2b28f589 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -34,6 +34,12 @@ "iframeTitles": { "howToVolunteer": "How you can support refugees in your community" }, + "events": { + "headline": "Upcoming Events", + "button": "Learn More & Register", + "loading": "Loading upcoming events…", + "empty": "No upcoming event is currently available." + }, "testimonials": { "header": "TESTIMONIALS", "headerParagraph": "What Volunteers & Coordinators Say" diff --git a/src/components/EventsSection/EventsSection.tsx b/src/components/EventsSection/EventsSection.tsx index af491597..a1a3207a 100644 --- a/src/components/EventsSection/EventsSection.tsx +++ b/src/components/EventsSection/EventsSection.tsx @@ -1,3 +1,167 @@ +"use client"; + +import { Button } from "@/components/core/button"; +import { ImageWithGradient } from "@/components/core/image"; +import { FullWidthContainer, OverlayingSectionContainer } from "@/components/styled/container"; +import { CustomHeading, Heading1, Heading4 } from "@/components/styled/text"; +import { eventsPublicLandingUrl, ScreenTypes } from "@/config/constants"; +import { useScreenType } from "@/context/DeviceContext"; +import { useEvents } from "@/hooks/useEvents"; +import { getImageUrl } from "@/utils/helpers"; +import type { ApiEventN4DGetList } from "need4deed-sdk"; +import { EventN4DType } from "need4deed-sdk"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import styled from "styled-components"; + +const EventsSectionContainer = styled(OverlayingSectionContainer)` + height: var(--homepage-events-section-container-height); + justify-content: center; + align-items: center; + gap: var(--homepage-events-section-content-container-gap); +`; + +const ContentContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; + gap: var(--homepage-events-section-content-container-gap); +`; + +const EventContainer = styled.div` + display: flex; + flex-direction: var(--homepage-events-section-event-container-flex-direction); + gap: var(--homepage-events-section-event-container-gap); +`; + +const EventCard = styled.div` + display: flex; + flex-direction: column; + width: var(--homepage-events-section-event-card-width, 100%); + gap: var(--homepage-events-section-event-card-gap); +`; + +const EventHeadline = styled.div` + display: flex; + flex-flow: row wrap; + align-items: center; + gap: var(--homepage-events-section-event-card-headline-gap); +`; + +const EventTitleTag = styled.div` + padding: var(--homepage-events-section-event-card-event-title-tag-padding); + border-radius: var(--homepage-events-section-event-card-event-title-tag-border-radius); + background-color: var(--color-orchid); +`; + +const ButtonContainer = styled.div` + display: flex; + align-items: flex-end; + justify-content: var(--homepage-events-section-button-container-justify-content); +`; + +const imageNamesMap: Record> = { + [EventN4DType.PARTY]: { + mobile: "events1-bg-mobile.webp", + tablet: "events1-bg-tablet.webp", + desktop: "events1-bg-desktop.webp", + }, + [EventN4DType.WORKSHOP]: { + mobile: "events2-bg-mobile.webp", + tablet: "events2-bg-tablet.webp", + desktop: "events2-bg-desktop.webp", + }, +}; + +function getUpcomingEvent(events?: ApiEventN4DGetList[]) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + return events + ?.filter((event) => event.active && new Date(event.dateEnd ?? event.date) >= today) + .sort((first, second) => new Date(first.date).getTime() - new Date(second.date).getTime())[0]; +} + +function formatEventDate(event: ApiEventN4DGetList, locale: string) { + const format = new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", year: "numeric" }); + const start = format.format(new Date(event.date)); + if (!event.dateEnd) return start; + + const end = format.format(new Date(event.dateEnd)); + return start === end ? start : `${start} – ${end}`; +} + export function EventsSection() { - return
Events Section (Coming in #137)
; + const { t, i18n } = useTranslation(); + const screenType = useScreenType(); + const { data: events, isLoading } = useEvents(); + const upcomingEvent = useMemo(() => getUpcomingEvent(events), [events]); + const eventType = upcomingEvent?.type ?? EventN4DType.PARTY; + const imageUrl = getImageUrl(imageNamesMap[eventType][screenType]); + + return ( + + + + + + + {t("homepage.events.headline")} + + + + + {isLoading ? ( + + {t("homepage.events.loading")} + + ) : upcomingEvent ? ( + <> + + + + {upcomingEvent.title} + + + + {formatEventDate(upcomingEvent, i18n.language)} + + + + {upcomingEvent.shortDescription.replace(/\\n/g, "\n")} + + + ) : ( + + {t("homepage.events.empty")} + + )} + + + {upcomingEvent && ( + +