Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"date-fns": "^4.1.0",
"email-validator": "^2.0.4",
"i18next": "^25.3.2",
"need4deed-sdk": "0.0.145",
"need4deed-sdk": "0.0.146",
"next": "15.3.8",
"react": "^19.0.0",
"react-day-picker": "^9.13.0",
Expand Down
41 changes: 41 additions & 0 deletions public/locales/de/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1522,23 +1522,64 @@
},
"calendar": {
"createEvent": "Veranstaltung erstellen",
"management": "Veranstaltungsverwaltung",
"events": "Veranstaltungen",
"upcomingEvents": "Kommende Veranstaltungen",
"noUpcomingEvents": "Keine kommenden Veranstaltungen in diesem Monat",
"upcomingHelp": "Für Freiwillige sichtbare Veranstaltungen",
"calendarTitle": "Kalender",
"calendarHelp": "Wählen Sie ein freies Datum, um eine Veranstaltung zu erstellen",
"pastEvents": "Vergangene Veranstaltungen",
"pastHelp": "Bereits stattgefundene Veranstaltungen ansehen",
"noPastEvents": "Keine vergangenen Veranstaltungen vorhanden.",
"published": "Veröffentlicht",
"draft": "Entwurf",
"editEvent": "Veranstaltung bearbeiten",
"deleteEvent": "Veranstaltung löschen",
"openRegistration": "Anmeldelink öffnen",
"registrationUnavailable": "Anmeldelink nicht verfügbar",
"editLoading": "Veranstaltung wird geladen…",
"editLoadError": "Diese Veranstaltung konnte nicht geladen werden. Möglicherweise wurde sie gelöscht.",
"backToCalendar": "Zurück zum Kalender",
"emptyTitle": "Noch keine Veranstaltungen",
"emptyText": "Erstellen Sie über die Schaltfläche eine Veranstaltung für Freiwillige.",
"loading": "Veranstaltungen werden geladen…",
"loadError": "Veranstaltungen konnten nicht geladen werden. Bitte versuchen Sie es erneut.",
"previousMonth": "Vorheriger Monat",
"nextMonth": "Nächster Monat",
"deleteConfirmTitle": "Diese Veranstaltung löschen?",
"deleteConfirmText": "\"{{title}}\" wird dauerhaft gelöscht.",
"messages": {
"created": "Veranstaltung erfolgreich erstellt",
"updated": "Veranstaltung erfolgreich aktualisiert",
"deleted": "Veranstaltung erfolgreich gelöscht"
},
"createForm": {
"eventTitle": "Veranstaltungstitel",
"typeLabel": "Veranstaltungsart",
"typeWorkshop": "Workshop",
"typeParty": "Feier",
"titleLabel": "Titel der ehrenamtlichen Tätigkeit",
"titlePlaceholder": "Titel",
"titleHelper": "z.B. \"Begleitung zur Augenklinik\", \"Basteln & Malen\"",
"descriptionLabel": "Beschreibung",
"descriptionPlaceholder": "Eine kurze Beschreibung der Veranstaltung",
"descriptionHelper": "Maximal {{max}} Zeichen",
"location": "Standort",
"street": "Straßenname",
"streetHelper": "z.B. \"Elsenstr.\"",
"houseNumber": "Hausnummer",
"houseNumberHelper": "z.B. \"87\"",
"postcode": "Postleitzahl",
"postcodeHelper": "z.B. \"12435\"",
"registrationLink": "Anmeldelink",
"dateTime": "Datum & Uhrzeit",
"date": "Termindatum",
"time": "Terminzeit",
"startDate": "Startdatum",
"startTime": "Startzeit",
"endDate": "Enddatum",
"endTime": "Endzeit",
"cancel": "Abbrechen",
"back": "Zurück",
"next": "Weiter",
Expand Down
41 changes: 41 additions & 0 deletions public/locales/en/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1510,23 +1510,64 @@
},
"calendar": {
"createEvent": "Create an event",
"management": "Event management",
"events": "Events",
"upcomingEvents": "Upcoming events",
"noUpcomingEvents": "No upcoming events this month",
"upcomingHelp": "Events visible to volunteers",
"calendarTitle": "Calendar",
"calendarHelp": "Select an empty date to create an event",
"pastEvents": "Past events",
"pastHelp": "Review events that have already taken place",
"noPastEvents": "No past events to display.",
"published": "Published",
"draft": "Draft",
"editEvent": "Edit event",
"deleteEvent": "Delete event",
"openRegistration": "Open registration link",
"registrationUnavailable": "Registration link unavailable",
"editLoading": "Loading event…",
"editLoadError": "This event could not be loaded. It may have been deleted.",
"backToCalendar": "Back to calendar",
"emptyTitle": "No events yet",
"emptyText": "Use the Create an event button to publish an event for volunteers.",
"loading": "Loading events…",
"loadError": "Events could not be loaded. Please try again.",
"previousMonth": "Previous month",
"nextMonth": "Next month",
"deleteConfirmTitle": "Delete this event?",
"deleteConfirmText": "\"{{title}}\" will be permanently deleted.",
"messages": {
"created": "Event created successfully",
"updated": "Event updated successfully",
"deleted": "Event deleted successfully"
},
"createForm": {
"eventTitle": "Event Title",
"typeLabel": "Event type",
"typeWorkshop": "Workshop",
"typeParty": "Party",
"titleLabel": "Title of the voluntary offer",
"titlePlaceholder": "Title",
"titleHelper": "e.g. \"Escort to the eye clinic\", \"Arts & Crafts\"",
"descriptionLabel": "Description",
"descriptionPlaceholder": "A short description of the event",
"descriptionHelper": "Maximum {{max}} characters",
"location": "Location",
"street": "Street name",
"streetHelper": "e.g. \"Elsenstr.\"",
"houseNumber": "House number",
"houseNumberHelper": "e.g. \"87\"",
"postcode": "Postcode",
"postcodeHelper": "e.g. \"12435\"",
"registrationLink": "Registration link",
"dateTime": "Date & Time",
"date": "Event date",
"time": "Event time",
"startDate": "Start date",
"startTime": "Start time",
"endDate": "End date",
"endTime": "End time",
"cancel": "Cancel",
"back": "Back",
"next": "Next",
Expand Down
9 changes: 9 additions & 0 deletions src/app/[lang]/dashboard/calendar/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { CreateEvent } from "@/components/Dashboard/Calendar/CreateEvent";
import { notFound } from "next/navigation";

export default async function EditEventPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const eventId = Number(id);
if (!Number.isInteger(eventId) || eventId <= 0) notFound();
return <CreateEvent eventId={eventId} />;
}
150 changes: 150 additions & 0 deletions src/components/Dashboard/Calendar/Calendar/CalendarGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react";
import type { ApiEventN4DGetList } from "need4deed-sdk";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";

import { calendarCells, dateKey, eventDateKeys } from "@/utils/calendar";

interface Props {
events: ApiEventN4DGetList[];
monthDate: Date;
selectedDateKey: string | null;
onPreviousMonth: () => void;
onNextMonth: () => void;
onDateClick: (date: Date, hasEvents: boolean) => void;
}

export function CalendarGrid(props: Props) {
const { events, monthDate, selectedDateKey, onPreviousMonth, onNextMonth, onDateClick } = props;
const { t, i18n } = useTranslation();
const today = useMemo(() => new Date(), []);
const year = monthDate.getFullYear();
const month = monthDate.getMonth();
const eventCounts = events.reduce<Record<string, number>>((counts, event) => {
eventDateKeys(event).forEach((key) => {
counts[key] = (counts[key] ?? 0) + 1;
});
return counts;
}, {});

return (
<CalendarBox>
<CalendarHeader>
<Nav type="button" onClick={onPreviousMonth} aria-label={t("dashboard.calendar.previousMonth")}>
<CaretLeftIcon />
</Nav>
<strong>{monthDate.toLocaleString(i18n.language, { month: "long", year: "numeric" })}</strong>
<Nav type="button" onClick={onNextMonth} aria-label={t("dashboard.calendar.nextMonth")}>
<CaretRightIcon />
</Nav>
</CalendarHeader>
<Days>
{Array.from({ length: 7 }, (_, index) => (
<DayName key={index}>
{new Date(2024, 0, 1 + index).toLocaleString(i18n.language, { weekday: "narrow" })}
</DayName>
))}
{calendarCells(year, month).map((cell, index) => {
const date = new Date(year, month, cell.day);
const key = dateKey(date);
const eventCount = cell.current ? (eventCounts[key] ?? 0) : 0;
const label = date.toLocaleDateString(i18n.language, { dateStyle: "full" });
return (
<Day
key={index}
type="button"
disabled={!cell.current}
$faded={!cell.current}
$today={cell.current && key === dateKey(today)}
$selected={cell.current && key === selectedDateKey}
$hasEvents={eventCount > 0}
onClick={() => onDateClick(date, eventCount > 0)}
aria-label={`${label}${eventCount ? `, ${eventCount} ${t("dashboard.calendar.events")}` : ""}`}
aria-pressed={cell.current ? key === selectedDateKey : undefined}
>
{cell.day}
{eventCount > 0 && <Count aria-hidden="true">{eventCount}</Count>}
</Day>
);
})}
</Days>
</CalendarBox>
);
}

const CalendarBox = styled.div`
padding: var(--spacing-20);
border: var(--border-width-thin) solid var(--color-orchid);
border-radius: var(--border-radius-large);
background: var(--color-white);
box-sizing: border-box;
`;
const CalendarHeader = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-16);
strong {
text-transform: capitalize;
}
`;
const Nav = styled.button`
display: flex;
border: 0;
border-radius: 50%;
padding: var(--spacing-8);
background: var(--color-orchid-subtle);
color: var(--color-midnight);
cursor: pointer;
`;
const Days = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: var(--spacing-4);
`;
const DayName = styled.div`
padding: var(--spacing-8) 0;
text-align: center;
color: var(--color-midnight);
font-size: var(--font-size-xs);
font-weight: bold;
`;
const Day = styled.button<{ $faded: boolean; $today: boolean; $selected: boolean; $hasEvents: boolean }>`
position: relative;
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
border: ${({ $today, $selected }) =>
$selected
? "var(--border-width-medium) solid var(--color-aubergine)"
: $today
? "var(--border-width-medium) solid var(--color-salmon)"
: "var(--border-width-medium) solid transparent"};
border-radius: 50%;
background: ${({ $selected, $hasEvents }) =>
$selected ? "var(--color-aubergine)" : $hasEvents ? "var(--color-orchid-subtle)" : "transparent"};
color: ${({ $faded, $selected }) =>
$faded ? "var(--color-grey-300)" : $selected ? "var(--color-white)" : "var(--color-midnight)"};
cursor: ${({ $faded }) => ($faded ? "default" : "pointer")};
font-weight: ${({ $today, $selected, $hasEvents }) => ($today || $selected || $hasEvents ? "bold" : "normal")};
&:hover:not(:disabled) {
background: ${({ $selected }) => ($selected ? "var(--color-aubergine)" : "var(--color-orchid)")};
}
`;
const Count = styled.span`
position: absolute;
right: 1px;
bottom: 1px;
display: grid;
place-items: center;
min-width: 16px;
height: 16px;
padding: 0 3px;
border-radius: 999px;
background: var(--color-salmon);
color: var(--color-midnight);
font-size: 10px;
box-sizing: border-box;
`;
Loading
Loading