Skip to content

Commit b0b3a6e

Browse files
Strehkclaude
andauthored
feat (Full Stack): Add access card ID and attendance tracking (#395)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0d4cf17 commit b0b3a6e

File tree

41 files changed

+3087
-589
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3087
-589
lines changed

CLAUDE-UI.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,40 @@ Slide-out drawer showing full entry details including place information, map, an
271271

272272
---
273273

274+
## Kbd Component
275+
276+
`src/lib/components/Kbd.svelte`
277+
278+
Renders a keyboard shortcut hint with OS-aware modifier formatting. On macOS, replaces modifier names with symbols (`alt```, `shift```, `ctrl```, `enter```). On Windows/Linux, keeps text as-is. SSR-safe (defaults to text modifiers).
279+
280+
### Props
281+
282+
| Prop | Type | Description |
283+
| -------- | -------------- | ---------------------------------- |
284+
| `hotkey` | `string` | Hotkey string, e.g. `"alt+a"` |
285+
| `size` | `'xs' \| 'sm'` | DaisyUI kbd size (default: `'sm'`) |
286+
287+
### Usage Examples
288+
289+
```svelte
290+
<script lang="ts">
291+
import Kbd from '$lib/components/Kbd.svelte';
292+
</script>
293+
294+
<!-- In a button -->
295+
<button class="btn btn-primary">
296+
Save <Kbd hotkey="alt+a" />
297+
</button>
298+
299+
<!-- Small size for inline badges -->
300+
<span class="hidden sm:inline-block"><Kbd hotkey="alt+n" size="xs" /></span>
301+
302+
<!-- Compound shortcuts -->
303+
<Kbd hotkey="shift+enter" />
304+
```
305+
306+
---
307+
274308
## Form Components (Critical)
275309

276310
Forms use `sveltekit-superforms` for validation and state management. Always structure forms consistently.
@@ -449,6 +483,96 @@ Use `Drawer` for slide-out panels (e.g., detail views, edit forms).
449483

450484
---
451485

486+
## TopDrawer Component
487+
488+
Use `TopDrawer` for overlay panels that slide down from the top of the screen. Built on `vaul-svelte`, it provides a gesture-friendly drawer with drag-to-close support. Used in management tool pages (accessFlow, postalRegistration, payments) for showing scanned/searched item details.
489+
490+
### Props
491+
492+
| Prop | Type | Description |
493+
| --------------- | -------------------- | --------------------------------------------- |
494+
| `open` | `boolean` (bindable) | Controls visibility |
495+
| `maxWidth` | `string` | Max width class (default: `'max-w-2xl'`) |
496+
| `title` | `string` | Header title text |
497+
| `titleIcon` | `string` | FontAwesome icon class (e.g. `'fa-id-badge'`) |
498+
| `headerActions` | `Snippet` | Buttons in header (profile link, close) |
499+
| `children` | `Snippet` | Scrollable content area |
500+
| `footer` | `Snippet` | Sticky footer with action buttons |
501+
502+
### TopDrawer Example
503+
504+
```svelte
505+
<script lang="ts">
506+
import TopDrawer from '$lib/components/TopDrawer.svelte';
507+
508+
let drawerOpen = $state(false);
509+
</script>
510+
511+
<TopDrawer bind:open={drawerOpen} title="Identity Check" titleIcon="fa-id-badge">
512+
{#snippet headerActions()}
513+
<button class="btn btn-ghost btn-sm btn-square" onclick={() => (drawerOpen = false)}>
514+
<i class="fa-solid fa-xmark text-lg"></i>
515+
</button>
516+
{/snippet}
517+
518+
<p>Scrollable content goes here...</p>
519+
520+
{#snippet footer()}
521+
<button class="btn btn-primary flex-1">Save & Next</button>
522+
<button class="btn btn-error" onclick={() => (drawerOpen = false)}>Close</button>
523+
{/snippet}
524+
</TopDrawer>
525+
```
526+
527+
**Note:** `TopDrawer` is different from `Drawer` — TopDrawer uses vaul-svelte for gesture/swipe support and slides from the top; Drawer is a right-side slide-out panel.
528+
529+
---
530+
531+
## BarcodeScanner Component
532+
533+
Use `BarcodeScanner` for pages that need barcode scanning via camera or manual text input. Encapsulates camera management, barcode detection, device switching, and manual input fallback.
534+
535+
### Props
536+
537+
| Prop | Type | Description |
538+
| ------------------- | --------------------------- | ---------------------------------------------------------- |
539+
| `scannedCode` | `string \| null` (bindable) | The detected/entered code |
540+
| `persistKey` | `string` | localStorage key for camera preference |
541+
| `barcodeFormats` | `BarcodeFormat[]` | Formats to detect (default: `['data_matrix', 'code_128']`) |
542+
| `manualPlaceholder` | `string` | Placeholder for manual input field |
543+
| `scanPromptText` | `string` | Prompt shown while camera is scanning |
544+
| `cameraZIndex` | `string` | z-index class for camera preview (default: `'z-30'`) |
545+
| `extraControls` | `Snippet` | Optional controls between camera settings and input |
546+
547+
### Exposed Methods
548+
549+
- `reset()` — Clears scanned code and restarts camera or refocuses manual input
550+
551+
### BarcodeScanner Example
552+
553+
```svelte
554+
<script lang="ts">
555+
import BarcodeScanner from '$lib/components/Scanner/BarcodeScanner.svelte';
556+
import { queryParameters } from 'sveltekit-search-params';
557+
558+
let params = queryParameters({ queryUserId: true });
559+
let scannerRef: BarcodeScanner;
560+
</script>
561+
562+
<BarcodeScanner
563+
bind:this={scannerRef}
564+
bind:scannedCode={$params.queryUserId}
565+
barcodeFormats={['data_matrix', 'code_128']}
566+
persistKey="useCameraForMyPage"
567+
manualPlaceholder="Enter code..."
568+
scanPromptText="Present the barcode..."
569+
/>
570+
571+
<!-- Call scannerRef.reset() after saving to prepare for next scan -->
572+
```
573+
574+
---
575+
452576
## Dashboard Components
453577

454578
Dashboard pages use consistent section layouts.

messages/de.json

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
"abroadTransactionWarning": "Wenn Sie Überweisungen aus dem Ausland vornehmen: Bitte achten Sie besonders genau darauf, dass alle Informationen richtig sind. Stellen Sie außerdem sicher, dass Sie selbst die Kosten für die Auslandsüberweisung tragen und die Gebühr nicht von der Summe, die wir erhalten, abgezogen wird. Bitte achten Sie auch auf die richtige Währung ({currency}). Wenn Sie keine SWIFT-Überweisung durchführen, wenden Sie sich bitte zuerst an die Teilnehmendenbetreuung.",
99
"absent": "Abwesend",
1010
"acceptAll": "Alle akzeptieren",
11+
"accessAndAttendance": "Zugang & Anwesenheit",
12+
"accessCardId": "Zugangsausweis-Nr.",
13+
"accessFlow": "Zugangskontrolle",
14+
"accessFlowDescription": "Teilnehmenden-Badges scannen, um Zugangsausweise zuzuweisen und Anwesenheit zu erfassen",
15+
"accessFlowSaved": "Zugangsausweis zugewiesen und Anwesenheit erfasst.",
1116
"accountExists": "Konto vorhanden",
1217
"accountHolder": "Kontoinhaber*in",
1318
"actions": "Aktionen",
@@ -159,7 +164,11 @@
159164
"atLeastXChars": "Mindestens {amount} Zeichen notwendig.",
160165
"atThisConference": "bei dieser Konferenz",
161166
"attendance": "Anwesenheit",
167+
"attendanceLog": "Anwesenheitsprotokoll",
162168
"attendancePercentage": "Anwesenheitsanteil",
169+
"attendanceRecorded": "Anwesenheit erfasst",
170+
"attendanceScanner": "Anwesenheitsscanner",
171+
"attendanceScannerDescription": "Teilnehmendenbadges scannen, um die Anwesenheit zu erfassen",
163172
"attendanceStatus": "Anwesenheitsstatus",
164173
"authErrorAccessDeniedDescription": "Du hast den Anmeldevorgang abgebrochen oder die erforderlichen Berechtigungen abgelehnt. Falls das unbeabsichtigt war, versuche es bitte erneut.",
165174
"authErrorAccessDeniedTitle": "Anmeldung abgebrochen",
@@ -190,6 +199,7 @@
190199
"badgeDataTitle": "Namensschild-Daten",
191200
"bankName": "Name der Bank",
192201
"bankingInformation": "Überweisungsinformationen",
202+
"barcodeDetectError": "Fehler bei der Barcode-Erkennung: {error}",
193203
"basicInfo": "Allgemeine Informationen",
194204
"before": "Vorher",
195205
"bic": "BIC",
@@ -279,6 +289,12 @@
279289
"calendarTrack": "Track",
280290
"calendarTracks": "Tracks",
281291
"calendarWorkshop": "Workshop",
292+
"cameraAborted": "Kamerazugriff wurde abgebrochen. Dies kann passieren, wenn der Berechtigungsdialog geschlossen wurde.",
293+
"cameraAccessDenied": "Kamerazugriff verweigert. Bitte erteile die Berechtigung in deinen Browsereinstellungen.",
294+
"cameraConstraintsError": "Kameraeinstellungen konnten nicht erfüllt werden. Versuche eine andere Kamera.",
295+
"cameraFailed": "Kamerazugriff fehlgeschlagen.",
296+
"cameraGenericError": "Fehler beim Kamerazugriff: {error}",
297+
"cameraInUse": "Kamera wird bereits verwendet oder ist nicht verfügbar.",
282298
"cameraSettings": "Kameraeinstellung",
283299
"cancel": "Abbruch",
284300
"cannotBeUndone": "Bitte bestätige die Eingabe! Die Änderung kann nicht rückgängig gemacht werden!",
@@ -414,6 +430,7 @@
414430
"conferenceTitle": "Konferenzname",
415431
"conferenceWebsite": "Website",
416432
"confirm": "Bestätigen",
433+
"confirmAll": "Alle bestätigen",
417434
"confirmDeleteAgendaItem": "Möchtest du das Thema \"{title}\" wirklich löschen?",
418435
"confirmDeleteOption": "Option löschen",
419436
"confirmDeleteOptionDescription": "Bist du sicher, dass du die Option \"{title}\" löschen möchtest? Dadurch werden auch alle Antworten für diese Option gelöscht und dies kann nicht rückgängig gemacht werden.",
@@ -506,6 +523,8 @@
506523
"deleteAllApplications": "Alle Bewerbungen löschen",
507524
"deleteAllApplicationsConfirmation": "Möchtest du wirklich alle Bewerbungen löschen? Um die Aktion rückgängig zu machen, musst du dich erneut anmelden.",
508525
"deleteApplicationConfirmation": "Möchtest diese Bewerbung wirklich löschen? Um die Aktion rückgängig zu machen, musst du dich erneut anmelden.",
526+
"deleteAttendanceEntry": "Anwesenheitseintrag löschen",
527+
"deleteAttendanceEntryConfirm": "Sind Sie sicher, dass Sie diesen Anwesenheitseintrag löschen möchten?",
509528
"deleteDelegation": "Delegation löschen",
510529
"deleteDelegationConfirmation": "Möchtest du diese Delegation wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
511530
"deleteEntry": "Löschen",
@@ -523,6 +542,7 @@
523542
"didMyStudentsAlreadyCreateADelegation": "Haben meine Schüler*innen schon eine oder mehrere Delegationen erstellt?",
524543
"didSomeoneOfTheseAlreadyCreateADelegation": "Hat eine der Personen schon\neine Delegation angelegt?",
525544
"diet": "Ernährungsweise",
545+
"dismiss": "Verwerfen",
526546
"distribution": "Verteilung",
527547
"diverse": "Divers",
528548
"doIhavePeopleToApplyWith": "Habe ich Personen, mit denen\nich mich als Gruppe bewerben kann?",
@@ -533,6 +553,7 @@
533553
"done": "Fertig",
534554
"doneToRegister": "für Anmeldung erledigt",
535555
"download": "Download",
556+
"downloadBackup": "Backup herunterladen",
536557
"downloadCertificate": "Zertifikat herunterladen",
537558
"downloadCommitteeData": "Daten für das Gremium \"{committee}\" herunterladen",
538559
"downloadCurrentRegistrationData": "Aktuelle Anmeldedaten herunterladen",
@@ -543,6 +564,7 @@
543564
"downloadResults": "Alle Ergebnisse herunterladen",
544565
"downloads": "Downloads",
545566
"downloadsPageDescription": "Exportiere Konferenzdaten in verschiedenen Formaten für externe Tools und Berichte.",
567+
"duplicateScan": "Diese teilnehmende Person wurde in dieser Sitzung bereits gescannt",
546568
"edit": "Bearbeiten",
547569
"editAgendaItem": "Thema bearbeiten",
548570
"editCommittee": "Gremium bearbeiten",
@@ -569,6 +591,7 @@
569591
"emergencyContactsPlaceholder": "Maria Musterfrau, +49123456789, m.musterfrau@mail.de",
570592
"emptyPlaceholders": "Textbaustein enthält leere Platzhalter (\\{\\{\\}\\}). Bitte füge einen Namen in die Klammern ein.",
571593
"end": "Ende",
594+
"endSession": "Sitzung beenden",
572595
"enterAtLeastOneEmail": "Bitte gib mindestens eine E-Mail-Adresse ein",
573596
"enterCode": "Code eingeben",
574597
"enterDateOfdateReceipt": "Empfangsdatum eingeben",
@@ -580,6 +603,7 @@
580603
"enterYourReviewComments": "Gib deine Bewertungskommentare hier ein",
581604
"entryCode": "Eintrittscode",
582605
"errorGeneratingPostalRegistrationPDF": "Fehler beim Generieren des PDFs für die postalische Anmeldung",
606+
"errors": "Fehler",
583607
"expand": "Ausklappen",
584608
"experience": "Erfahrung",
585609
"expired": "abgelaufen",
@@ -683,6 +707,7 @@
683707
"httpGenericError": "Es ist ein unbekannter Fehler aufgetreten.",
684708
"iban": "IBAN",
685709
"ibanMustBe22Characters": "IBAN muss 22 Zeichen ohne Leerzeichen haben",
710+
"identityCheck": "Identitätsprüfung",
686711
"impersonation": "Stellvertretung",
687712
"impersonationActive": "Stellvertretung aktiv",
688713
"impersonationFailed": "Stellvertretung fehlgeschlagen",
@@ -762,6 +787,8 @@
762787
"manageSnippets": "Textbausteine verwalten",
763788
"markAsProblem": "Als Problem markieren",
764789
"markAsRecieved": "Als Erhalten markieren",
790+
"markdownSupportedPlaceholder": "# Markdown wird unterstützt...",
791+
"markdownSyntaxHint": "Dieser Editor unterstützt <strong>Markdown</strong>-Formatierung. Siehe den <a href=\"https://www.markdownguide.org/basic-syntax/\" target=\"_blank\" class=\"link\">Markdown Guide</a> für eine Syntaxreferenz.",
765792
"mediaAgreement": "Einwilligung Bildnutzung",
766793
"mediaConsentStatus": "Fotostatus",
767794
"members": "Mitglieder",
@@ -780,6 +807,10 @@
780807
"nations": "Länder",
781808
"nationsPool": "Länderpool",
782809
"nationsPoolDescription": "Hier sind die Länder aufgelistet, die euch zur Auswahl stehen. Jeder Haken bedeutet, dass das jeweilige Land in dem entsprechenden Gremium einen Sitz hat. Die Spalte am Ende zeigt an, wie viele Delegierte das Land insgesamt auf der Konferenz hat. Ihr könnt kein Land vertreten, das weniger Plätze hat, als ihr Mitglieder in der Delegation habt.",
810+
"navInfo": "Kommunikation",
811+
"navMaintenance": "Wartung",
812+
"navWorkflows": "Arbeitsabläufe",
813+
"networkErrorRetrying": "Netzwerkfehler — automatischer erneuter Versuch",
783814
"newPaper": "Neues Papier",
784815
"newPieceUnlocked": "Neues Puzzlestück freigeschaltet!",
785816
"newStatus": "Neuer Status",
@@ -790,8 +821,11 @@
790821
"nextNumber": "Nächste Nummer",
791822
"no": "Nein",
792823
"noAccess": "Hier hast du keinen Zugriff",
824+
"noActiveSession": "Starte eine Sitzung, um mit dem Scannen zu beginnen",
793825
"noAnswer": "Keine Antwort",
794826
"noAnswerYet": "Noch nicht beantwortet",
827+
"noAttendanceEntries": "Keine Anwesenheitseinträge erfasst",
828+
"noCameraFound": "Keine Kamera gefunden. Bitte stelle sicher, dass eine Kamera angeschlossen ist.",
795829
"noChangesBetweenVersions": "Keine Änderungen zwischen diesen Versionen",
796830
"noCommittees": "Keine Gremien vorhanden",
797831
"noConferenceHeading": "Keine Konferenz",
@@ -849,6 +883,8 @@
849883
"nsaPool": "Nichtstaatliche Akteure",
850884
"nsaPoolDescription": "Hier sind die nichtstaatlichen Akteure (NAs) aufgelistet, die dir zur Auswahl stehen. Delegierte von nichtstaatlichen Akteuren sind keinen konkreten Gremien zugeordnet. Dennoch ist auch die Anzahl der Delegierten pro NA begrenzt. Wenn du hier keine NA aufgelistet siehst, bedeutet das, dass keine NAs für die Konferenz vorgesehen sind oder die Anzahl der Mitglieder der Delegation die Anzahl der verfügbaren Plätze übersteigt.",
851885
"nsaSeats": "Sitze von Nichtstaatlichen Akteuren",
886+
"occasion": "Anlass",
887+
"occasionForSession": "Anlass f. diese Sitzung",
852888
"ofAge": "Volljährig",
853889
"off": "Aus",
854890
"omnivore": "Omnivor",
@@ -1064,6 +1100,7 @@
10641100
"receiveGeneralInformation": "E-Mails zu MUN-SH, MUNBW und anderen DMUN-Projekten erhalten",
10651101
"receiveJoinTeamInformation": "E-Mails mit Ausschreibungen für Teampositionen erhalten",
10661102
"recievedDate": "Empfangen am",
1103+
"recordEntry": "Eintrag erfassen",
10671104
"redo": "Wiederherstellen",
10681105
"reference": "Referenz",
10691106
"referenceDataDescription": "Exportiere Nationenlisten und andere Referenzdaten.",
@@ -1194,12 +1231,18 @@
11941231
"rotateCode": "Code erneuern",
11951232
"save": "Speichern",
11961233
"saveAndContinue": "Speichern und weiter",
1234+
"saveAndNext": "Speichern & Weiter",
11971235
"saveNextDocumentNumber": "Nächste Nummer speichern",
11981236
"saveSettings": "Einstellungen speichern",
1199-
"saved": "Gespeichert",
1237+
"saved": "Gespeichert",
12001238
"saving": "Speichern...",
1201-
"scanPostalRegistrationCode": "Scanne den quadratischen Code (Data-Matrix) in der rechten oberen Ecke der postalischen Anmeldung, um den Status zu ändern. Mit <span class=\"kbd kbd-sm\">enter</span> kannst du alles auf einmal bestätigen, mit <span class=\"kbd kbd-sm\">1</span>, <span class=\"kbd kbd-sm\">2</span>, <span class=\"kbd kbd-sm\">3</span> und <span class=\"kbd kbd-sm\">4</span> kannst du die einzelnen Dokumente bestätigen.",
1239+
"scanCount": "{count} Scans",
1240+
"scanPending": "ausstehend",
1241+
"scanPostalRegistrationCode": "Scanne den quadratischen Code (Data-Matrix) in der rechten oberen Ecke der postalischen Anmeldung, um den Status zu ändern.",
1242+
"scanPostalRegistrationCodeHotkeyConfirmAll": "kannst du alle Dokumente und die Medienfreigabe auf einmal bestätigen.",
1243+
"scanPostalRegistrationCodeHotkeyMedia": "kannst du den Medienfreigabe-Status einzeln setzen.",
12021244
"scanPostalRegistrationCodePrompt": "Präsentiere den Code...",
1245+
"scanSynced": "synchronisiert",
12031246
"schoolOrInstitution": "Schule / Institution",
12041247
"search": "Suche",
12051248
"searchByDelegation": "Nach Delegationsname suchen...",
@@ -1221,13 +1264,16 @@
12211264
"separateEmailsHint": "Trenne mehrere E-Mails mit Kommas, Semikolons oder neuen Zeilen",
12221265
"servicesPage": "Dienste",
12231266
"servicesPageDescription": "Zugriff auf Konferenz-Tools und Dienste",
1267+
"sessionActive": "Sitzung aktiv",
12241268
"sessionExpiredCause1": "Der Anmeldevorgang länger als 5 Minuten gedauert hat",
12251269
"sessionExpiredCause2": "Du die Seite während der Anmeldung aktualisiert hast",
12261270
"sessionExpiredCause3": "Du mehrere Anmelde-Tabs geöffnet hast",
12271271
"sessionExpiredCauses": "Das passiert normalerweise, wenn:",
12281272
"sessionExpiredDescription": "Deine Anmeldesitzung ist abgelaufen oder ungültig. Das kann verschiedene Gründe haben. Bitte versuche, dich erneut anzumelden.",
12291273
"sessionExpiredTitle": "Anmeldesitzung abgelaufen",
12301274
"sessionExpiredTryAgain": "Erneut anmelden",
1275+
"sessionOccasion": "Sitzungsname",
1276+
"sessionOccasionPlaceholder": "z.B. Eröffnungsfeier",
12311277
"setAttendanceFalse": "Alle auf \"Abwesend\" setzen",
12321278
"setAttendanceTrue": "Alle auf \"Anwesend\" setzen",
12331279
"setDelegationPreferences": "Wünsche festlegen",
@@ -1288,6 +1334,7 @@
12881334
"start": "Start",
12891335
"startAssignment": "Assistent starten",
12901336
"startCamera": "Kamera starten",
1337+
"startSession": "Sitzung starten",
12911338
"statistics": "Statistiken",
12921339
"statsAcceptanceRate": "Annahmequote",
12931340
"statsAcceptedPapers": "Angenommen",
@@ -1423,7 +1470,7 @@
14231470
"tabExplanationCommittees": "Verwalte hier die <strong>Gremien</strong> und <strong>Tagesordnungspunkte</strong> deiner Konferenz. Jedes Gremium kann mehrere Themen haben, zu denen Teilnehmende Paper einreichen können.",
14241471
"tabExplanationDocuments": "Lade <strong>PDF-Vorlagen</strong> für den Postversand und Teilnahmezertifikate hoch. Die Vorlagen werden mit Teilnehmendendaten gefüllt.",
14251472
"tabExplanationGeneral": "Grundlegende Informationen über deine Konferenz. Diese Daten werden auf der Startseite und in der Übersicht angezeigt.",
1426-
"tabExplanationLinks": "Hier konfigurierst du <strong>externe Links</strong> und <strong>Ankündigungen</strong>, die Teilnehmenden im Dashboard angezeigt werden.",
1473+
"tabExplanationLinks": "Hier konfigurierst du <strong>externe Links</strong>, die Teilnehmenden im Dashboard angezeigt werden.",
14271474
"tabExplanationPayments": "Bankverbindung für die <strong>Teilnahmegebühren</strong>. Diese Informationen werden Teilnehmenden angezeigt, wenn sie ihre Zahlung tätigen.",
14281475
"tabExplanationStatus": "Der <strong>Konferenzstatus</strong> bestimmt, welche Funktionen für Teilnehmende verfügbar sind. Die <strong>Feature-Schalter</strong> aktivieren oder deaktivieren einzelne Funktionen unabhängig vom Status.",
14291476
"tableSize": "Tabellengröße",
@@ -1523,6 +1570,7 @@
15231570
"userId": "Benutzer-ID",
15241571
"userIdInput": "Code-Eingabe (UserId)",
15251572
"userNotFound": "Nutzer*in nicht gefunden",
1573+
"userNotFoundForAccessFlow": "Kein*e Teilnehmer*in mit dieser ID gefunden.",
15261574
"userNotFoundForPostalRegistration": "Kein*e Nutzer*in gefunden",
15271575
"usersAddedDirectly": "{count} Benutzer*innen direkt hinzugefügt",
15281576
"usubscribeSomeOnly": "Du willst stattdessen nur einzelne Newsletter abmelden?",

0 commit comments

Comments
 (0)