feat: implement i18n — extract all UI strings, add EN + ES-MX translations, add language selector #9

Merged
marcel merged 1 commits from feat/i18n into main 2026-03-19 15:13:57 +01:00
20 changed files with 733 additions and 199 deletions

37
frontend/e2e/lang.spec.ts Normal file
View File

@@ -0,0 +1,37 @@
import { test, expect } from '@playwright/test';
test.describe('Language selector', () => {
test('shows DE, EN, ES buttons in the header', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('banner').getByRole('button', { name: 'DE' })).toBeVisible();
await expect(page.getByRole('banner').getByRole('button', { name: 'EN' })).toBeVisible();
await expect(page.getByRole('banner').getByRole('button', { name: 'ES' })).toBeVisible();
});
test('switching to EN translates the navigation', async ({ page }) => {
await page.goto('/');
await page.getByRole('banner').getByRole('button', { name: 'EN' }).click();
await expect(page.getByRole('navigation').getByRole('link', { name: 'Documents' })).toBeVisible();
await expect(page.getByRole('navigation').getByRole('link', { name: 'Persons' })).toBeVisible();
});
test('language choice persists after navigation', async ({ page }) => {
await page.goto('/');
await page.getByRole('banner').getByRole('button', { name: 'EN' }).click();
await page.goto('/persons');
await expect(page.getByRole('navigation').getByRole('link', { name: 'Documents' })).toBeVisible();
});
test('switching back to DE restores German', async ({ page }) => {
await page.goto('/');
await page.getByRole('banner').getByRole('button', { name: 'EN' }).click();
await page.getByRole('banner').getByRole('button', { name: 'DE' }).click();
await expect(page.getByRole('navigation').getByRole('link', { name: 'Dokumente' })).toBeVisible();
});
test('active language button is visually highlighted', async ({ page }) => {
await page.goto('/');
const deBtn = page.getByRole('banner').getByRole('button', { name: 'DE' });
await expect(deBtn).toHaveClass(/font-bold/);
});
});

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from de!",
"error_document_not_found": "Das Dokument wurde nicht gefunden.", "error_document_not_found": "Das Dokument wurde nicht gefunden.",
"error_document_no_file": "Diesem Dokument ist noch keine Datei zugeordnet.", "error_document_no_file": "Diesem Dokument ist noch keine Datei zugeordnet.",
"error_file_not_found": "Die Datei konnte im Speicher nicht gefunden werden.", "error_file_not_found": "Die Datei konnte im Speicher nicht gefunden werden.",
@@ -10,5 +10,159 @@
"error_unauthorized": "Sie sind nicht angemeldet.", "error_unauthorized": "Sie sind nicht angemeldet.",
"error_forbidden": "Sie haben keine Berechtigung für diese Aktion.", "error_forbidden": "Sie haben keine Berechtigung für diese Aktion.",
"error_validation_error": "Die Eingabe ist ungültig.", "error_validation_error": "Die Eingabe ist ungültig.",
"error_internal_error": "Ein unerwarteter Fehler ist aufgetreten." "error_internal_error": "Ein unerwarteter Fehler ist aufgetreten.",
"nav_documents": "Dokumente",
"nav_persons": "Personen",
"nav_conversations": "Konversationen",
"nav_admin": "Admin",
"nav_logout": "Abmelden",
"btn_save": "Speichern",
"btn_cancel": "Abbrechen",
"btn_edit": "Bearbeiten",
"btn_create": "Erstellen",
"btn_delete": "Löschen",
"btn_back_to_overview": "Zurück zur Übersicht",
"btn_back": "Zurück",
"btn_back_to_document": "Zurück zum Dokument",
"form_label_first_name": "Vorname",
"form_label_last_name": "Nachname",
"form_label_alias": "Rufname / Alias",
"form_placeholder_alias": "z.B. Oma Frieda, Onkel Karl…",
"form_label_date": "Datum",
"form_placeholder_date": "TT.MM.JJJJ",
"form_date_error": "Bitte im Format TT.MM.JJJJ eingeben, z.B. 20.12.2026",
"form_label_location": "Ort",
"form_placeholder_location": "z.B. Berlin, Wien…",
"form_label_sender": "Absender",
"form_label_receivers": "Empfänger",
"form_label_title": "Titel *",
"form_label_tags": "Schlagworte",
"form_label_content": "Inhalt",
"form_placeholder_content": "Kurze Beschreibung des Inhalts…",
"form_label_transcription": "Transkription",
"form_placeholder_transcription": "Vollständiger Text des Dokuments…",
"form_label_archive_location": "Aufbewahrungsort",
"form_placeholder_archive_location": "z.B. Schrank 3, Mappe B",
"form_helper_archive_location": "Wo befindet sich das Originaldokument?",
"login_heading": "Anmelden",
"login_label_username": "Benutzername",
"login_label_password": "Passwort",
"login_btn_submit": "Anmelden",
"docs_search_placeholder": "Suche in Titel, Inhalt, Ort...",
"docs_btn_filter": "Filter",
"docs_btn_reset_title": "Filter zurücksetzen",
"docs_filter_label_tags": "Schlagworte",
"docs_filter_label_sender": "Absender",
"docs_filter_label_receivers": "Empfänger",
"docs_filter_label_from": "Von",
"docs_filter_label_to": "Bis",
"docs_btn_new": "Neues Dokument",
"docs_empty_heading": "Keine Dokumente gefunden",
"docs_empty_text": "Versuchen Sie, die Filter anzupassen oder den Suchbegriff zu ändern.",
"docs_empty_btn_clear": "Alle Filter löschen",
"docs_list_from": "Von",
"docs_list_to": "An",
"docs_list_unknown": "Unbekannt",
"doc_section_who_when": "Wer & Wann",
"doc_section_description": "Beschreibung",
"doc_section_file": "Datei",
"doc_file_upload_label": "Datei hochladen",
"doc_file_upload_note": "(optional)",
"doc_file_replace_label": "Neue Datei hochladen",
"doc_file_replace_note": "(ersetzt die aktuelle Datei)",
"doc_current_file_label": "Aktuelle Datei:",
"doc_new_heading": "Neues Dokument",
"doc_edit_heading": "Bearbeiten",
"doc_section_details": "Details",
"doc_label_document_date": "Dokumentendatum",
"doc_label_creation_location": "Erstellungsort",
"doc_label_archive_location_original": "Aufbewahrungsort (Original)",
"doc_section_persons": "Personen",
"doc_sender_not_specified": "Nicht angegeben",
"doc_no_receivers": "Keine Empfänger",
"doc_section_content": "Inhalt",
"doc_label_summary": "Zusammenfassung",
"doc_loading": "Lade Dokument...",
"doc_download_link": "Direkter Download versuchen",
"doc_no_scan": "Kein Scan vorhanden",
"persons_heading": "Personenverzeichnis",
"persons_subtitle": "Durchsuchen Sie den Index aller erfassten Personen im Familienarchiv.",
"persons_btn_new": "Neue Person",
"persons_search_placeholder": "Namen suchen...",
"persons_empty_heading": "Keine Personen gefunden.",
"persons_empty_text": "Versuchen Sie einen anderen Suchbegriff.",
"persons_new_heading": "Neue Person",
"persons_section_details": "Angaben zur Person",
"person_edit_heading": "Person bearbeiten",
"person_label_full_name": "Voller Name",
"person_merge_heading": "Person zusammenführen",
"person_merge_description": "Diese Person wird in die gewählte Zielperson überführt. Alle Dokumente und Verknüpfungen werden übertragen, danach wird diese Person gelöscht.",
"person_merge_target_label": "Zusammenführen mit",
"person_btn_merge": "Zusammenführen",
"person_btn_merge_confirm": "Ja, zusammenführen",
"person_merge_warning": "Achtung: Diese Aktion ist nicht rückgängig zu machen.",
"person_docs_heading": "Gesendete Dokumente",
"person_no_docs": "Diese Person ist noch nicht als Absender verknüpft.",
"conv_heading": "Konversationen",
"conv_subtitle": "Verfolgen Sie den Schriftverkehr zwischen zwei Personen chronologisch.",
"conv_label_person_a": "Person A (Absender)",
"conv_label_person_b": "Person B (Empfänger)",
"conv_label_from": "Zeitraum von",
"conv_label_to": "Zeitraum bis",
"conv_sort_label": "Sortierung:",
"conv_sort_newest": "Neueste zuerst",
"conv_sort_oldest": "Älteste zuerst",
"conv_empty_heading": "Wählen Sie zwei Personen aus",
"conv_empty_text": "Die Korrespondenz wird hier angezeigt.",
"conv_no_results_heading": "Keine Dokumente gefunden.",
"conv_no_results_text": "Versuchen Sie, den Zeitraum anzupassen.",
"admin_heading": "Admin Dashboard",
"admin_tab_users": "Benutzer",
"admin_tab_groups": "Gruppen",
"admin_tab_tags": "Schlagworte",
"admin_section_users": "Benutzerverwaltung",
"admin_col_login": "Login",
"admin_col_groups": "Gruppen",
"admin_col_password": "Passwort",
"admin_multiselect_hint": "Strg+Klick für Auswahl",
"admin_password_placeholder": "Neues PW (optional)",
"admin_no_groups": "Keine Gruppen",
"admin_btn_delete_user_title": "Benutzer löschen",
"admin_section_new_user": "Neuen Benutzer anlegen",
"admin_multiselect_hint_multi": "Strg+Klick für mehrere",
"admin_multiselect_hint_full": "Strg+Klick für Mehrfachauswahl",
"admin_section_tags": "Schlagworte",
"admin_tags_warning": "Warnung: Umbenennen oder Löschen wirkt sich auf alle verknüpften Dokumente aus.",
"admin_btn_edit_tag_label": "Schlagwort bearbeiten",
"admin_tag_delete_confirm": "Wirklich löschen? Das Schlagwort wird aus allen Dokumenten entfernt.",
"admin_btn_delete_tag_label": "Schlagwort löschen",
"admin_section_groups": "Gruppenverwaltung",
"admin_col_name": "Name",
"admin_col_permissions": "Berechtigungen",
"admin_col_actions": "Aktionen",
"admin_group_delete_confirm": "Gruppe wirklich löschen?",
"admin_section_new_group": "Neue Gruppe anlegen",
"admin_group_name_placeholder": "Gruppenname (z.B. Editoren)",
"comp_typeahead_placeholder": "Namen tippen...",
"comp_typeahead_loading": "Suche...",
"comp_multiselect_placeholder": "Namen tippen...",
"comp_multiselect_remove": "Entfernen",
"comp_multiselect_loading": "Suche...",
"comp_taginput_placeholder_create": "Schlagworte hinzufügen...",
"comp_taginput_placeholder_filter": "Nach Schlagworten filtern...",
"comp_taginput_remove": "Schlagwort entfernen",
"comp_taginput_create_hint": "Enter drücken um Schlagwort zu erstellen."
} }

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from en!",
"error_document_not_found": "Document not found.", "error_document_not_found": "Document not found.",
"error_document_no_file": "No file is associated with this document.", "error_document_no_file": "No file is associated with this document.",
"error_file_not_found": "The file could not be found in storage.", "error_file_not_found": "The file could not be found in storage.",
@@ -10,5 +10,159 @@
"error_unauthorized": "You are not logged in.", "error_unauthorized": "You are not logged in.",
"error_forbidden": "You do not have permission for this action.", "error_forbidden": "You do not have permission for this action.",
"error_validation_error": "The input is invalid.", "error_validation_error": "The input is invalid.",
"error_internal_error": "An unexpected error occurred." "error_internal_error": "An unexpected error occurred.",
"nav_documents": "Documents",
"nav_persons": "Persons",
"nav_conversations": "Conversations",
"nav_admin": "Admin",
"nav_logout": "Sign out",
"btn_save": "Save",
"btn_cancel": "Cancel",
"btn_edit": "Edit",
"btn_create": "Create",
"btn_delete": "Delete",
"btn_back_to_overview": "Back to overview",
"btn_back": "Back",
"btn_back_to_document": "Back to document",
"form_label_first_name": "First name",
"form_label_last_name": "Last name",
"form_label_alias": "Nickname / Alias",
"form_placeholder_alias": "e.g. Grandma Frieda, Uncle Karl…",
"form_label_date": "Date",
"form_placeholder_date": "DD.MM.YYYY",
"form_date_error": "Please enter in DD.MM.YYYY format, e.g. 20.12.2026",
"form_label_location": "Location",
"form_placeholder_location": "e.g. Berlin, Vienna…",
"form_label_sender": "Sender",
"form_label_receivers": "Recipients",
"form_label_title": "Title *",
"form_label_tags": "Tags",
"form_label_content": "Content",
"form_placeholder_content": "Brief description of the content…",
"form_label_transcription": "Transcription",
"form_placeholder_transcription": "Full text of the document…",
"form_label_archive_location": "Storage location",
"form_placeholder_archive_location": "e.g. Cabinet 3, Folder B",
"form_helper_archive_location": "Where is the original document stored?",
"login_heading": "Sign in",
"login_label_username": "Username",
"login_label_password": "Password",
"login_btn_submit": "Sign in",
"docs_search_placeholder": "Search in title, content, location...",
"docs_btn_filter": "Filter",
"docs_btn_reset_title": "Reset filter",
"docs_filter_label_tags": "Tags",
"docs_filter_label_sender": "Sender",
"docs_filter_label_receivers": "Recipients",
"docs_filter_label_from": "From",
"docs_filter_label_to": "To",
"docs_btn_new": "New document",
"docs_empty_heading": "No documents found",
"docs_empty_text": "Try adjusting the filters or changing the search term.",
"docs_empty_btn_clear": "Clear all filters",
"docs_list_from": "From",
"docs_list_to": "To",
"docs_list_unknown": "Unknown",
"doc_section_who_when": "Who & When",
"doc_section_description": "Description",
"doc_section_file": "File",
"doc_file_upload_label": "Upload file",
"doc_file_upload_note": "(optional)",
"doc_file_replace_label": "Upload new file",
"doc_file_replace_note": "(replaces the current file)",
"doc_current_file_label": "Current file:",
"doc_new_heading": "New document",
"doc_edit_heading": "Edit",
"doc_section_details": "Details",
"doc_label_document_date": "Document date",
"doc_label_creation_location": "Place of creation",
"doc_label_archive_location_original": "Storage location (original)",
"doc_section_persons": "Persons",
"doc_sender_not_specified": "Not specified",
"doc_no_receivers": "No recipients",
"doc_section_content": "Content",
"doc_label_summary": "Summary",
"doc_loading": "Loading document...",
"doc_download_link": "Try direct download",
"doc_no_scan": "No scan available",
"persons_heading": "Person directory",
"persons_subtitle": "Browse the index of all recorded persons in the family archive.",
"persons_btn_new": "New person",
"persons_search_placeholder": "Search names...",
"persons_empty_heading": "No persons found.",
"persons_empty_text": "Try a different search term.",
"persons_new_heading": "New person",
"persons_section_details": "Person details",
"person_edit_heading": "Edit person",
"person_label_full_name": "Full name",
"person_merge_heading": "Merge person",
"person_merge_description": "This person will be merged into the selected target person. All documents and links will be transferred, then this person will be deleted.",
"person_merge_target_label": "Merge with",
"person_btn_merge": "Merge",
"person_btn_merge_confirm": "Yes, merge",
"person_merge_warning": "Warning: This action cannot be undone.",
"person_docs_heading": "Sent documents",
"person_no_docs": "This person has not yet been linked as a sender.",
"conv_heading": "Conversations",
"conv_subtitle": "Follow the correspondence between two persons chronologically.",
"conv_label_person_a": "Person A (Sender)",
"conv_label_person_b": "Person B (Recipient)",
"conv_label_from": "Period from",
"conv_label_to": "Period to",
"conv_sort_label": "Sort:",
"conv_sort_newest": "Newest first",
"conv_sort_oldest": "Oldest first",
"conv_empty_heading": "Select two persons",
"conv_empty_text": "The correspondence will be shown here.",
"conv_no_results_heading": "No documents found.",
"conv_no_results_text": "Try adjusting the time period.",
"admin_heading": "Admin Dashboard",
"admin_tab_users": "Users",
"admin_tab_groups": "Groups",
"admin_tab_tags": "Tags",
"admin_section_users": "User management",
"admin_col_login": "Login",
"admin_col_groups": "Groups",
"admin_col_password": "Password",
"admin_multiselect_hint": "Ctrl+Click to select",
"admin_password_placeholder": "New PW (optional)",
"admin_no_groups": "No groups",
"admin_btn_delete_user_title": "Delete user",
"admin_section_new_user": "Create new user",
"admin_multiselect_hint_multi": "Ctrl+Click for multiple",
"admin_multiselect_hint_full": "Ctrl+Click for multiple selection",
"admin_section_tags": "Tags",
"admin_tags_warning": "Warning: Renaming or deleting affects all linked documents.",
"admin_btn_edit_tag_label": "Edit tag",
"admin_tag_delete_confirm": "Really delete? The tag will be removed from all documents.",
"admin_btn_delete_tag_label": "Delete tag",
"admin_section_groups": "Group management",
"admin_col_name": "Name",
"admin_col_permissions": "Permissions",
"admin_col_actions": "Actions",
"admin_group_delete_confirm": "Really delete group?",
"admin_section_new_group": "Create new group",
"admin_group_name_placeholder": "Group name (e.g. Editors)",
"comp_typeahead_placeholder": "Type a name...",
"comp_typeahead_loading": "Searching...",
"comp_multiselect_placeholder": "Type a name...",
"comp_multiselect_remove": "Remove",
"comp_multiselect_loading": "Searching...",
"comp_taginput_placeholder_create": "Add tags...",
"comp_taginput_placeholder_filter": "Filter by tags...",
"comp_taginput_remove": "Remove tag",
"comp_taginput_create_hint": "Press Enter to create tag."
} }

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://inlang.com/schema/inlang-message-format", "$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from es!",
"error_document_not_found": "Documento no encontrado.", "error_document_not_found": "Documento no encontrado.",
"error_document_no_file": "No hay ningún archivo asociado a este documento.", "error_document_no_file": "No hay ningún archivo asociado a este documento.",
"error_file_not_found": "El archivo no pudo encontrarse en el almacenamiento.", "error_file_not_found": "El archivo no pudo encontrarse en el almacenamiento.",
@@ -10,5 +10,159 @@
"error_unauthorized": "No ha iniciado sesión.", "error_unauthorized": "No ha iniciado sesión.",
"error_forbidden": "No tiene permiso para realizar esta acción.", "error_forbidden": "No tiene permiso para realizar esta acción.",
"error_validation_error": "La entrada no es válida.", "error_validation_error": "La entrada no es válida.",
"error_internal_error": "Se ha producido un error inesperado." "error_internal_error": "Se ha producido un error inesperado.",
"nav_documents": "Documentos",
"nav_persons": "Personas",
"nav_conversations": "Conversaciones",
"nav_admin": "Admin",
"nav_logout": "Cerrar sesión",
"btn_save": "Guardar",
"btn_cancel": "Cancelar",
"btn_edit": "Editar",
"btn_create": "Crear",
"btn_delete": "Eliminar",
"btn_back_to_overview": "Volver al resumen",
"btn_back": "Volver",
"btn_back_to_document": "Volver al documento",
"form_label_first_name": "Nombre",
"form_label_last_name": "Apellido",
"form_label_alias": "Apodo / Alias",
"form_placeholder_alias": "p.ej. Abuela Frieda, Tío Karl…",
"form_label_date": "Fecha",
"form_placeholder_date": "DD.MM.AAAA",
"form_date_error": "Introduzca en formato DD.MM.AAAA, p.ej. 20.12.2026",
"form_label_location": "Lugar",
"form_placeholder_location": "p.ej. Berlín, Viena…",
"form_label_sender": "Remitente",
"form_label_receivers": "Destinatarios",
"form_label_title": "Título *",
"form_label_tags": "Etiquetas",
"form_label_content": "Contenido",
"form_placeholder_content": "Breve descripción del contenido…",
"form_label_transcription": "Transcripción",
"form_placeholder_transcription": "Texto completo del documento…",
"form_label_archive_location": "Ubicación de almacenamiento",
"form_placeholder_archive_location": "p.ej. Armario 3, Carpeta B",
"form_helper_archive_location": "¿Dónde se encuentra el documento original?",
"login_heading": "Iniciar sesión",
"login_label_username": "Usuario",
"login_label_password": "Contraseña",
"login_btn_submit": "Iniciar sesión",
"docs_search_placeholder": "Buscar en título, contenido, lugar...",
"docs_btn_filter": "Filtrar",
"docs_btn_reset_title": "Restablecer filtro",
"docs_filter_label_tags": "Etiquetas",
"docs_filter_label_sender": "Remitente",
"docs_filter_label_receivers": "Destinatarios",
"docs_filter_label_from": "Desde",
"docs_filter_label_to": "Hasta",
"docs_btn_new": "Nuevo documento",
"docs_empty_heading": "No se encontraron documentos",
"docs_empty_text": "Intente ajustar los filtros o cambiar el término de búsqueda.",
"docs_empty_btn_clear": "Borrar todos los filtros",
"docs_list_from": "De",
"docs_list_to": "Para",
"docs_list_unknown": "Desconocido",
"doc_section_who_when": "Quién & Cuándo",
"doc_section_description": "Descripción",
"doc_section_file": "Archivo",
"doc_file_upload_label": "Subir archivo",
"doc_file_upload_note": "(opcional)",
"doc_file_replace_label": "Subir nuevo archivo",
"doc_file_replace_note": "(reemplaza el archivo actual)",
"doc_current_file_label": "Archivo actual:",
"doc_new_heading": "Nuevo documento",
"doc_edit_heading": "Editar",
"doc_section_details": "Detalles",
"doc_label_document_date": "Fecha del documento",
"doc_label_creation_location": "Lugar de creación",
"doc_label_archive_location_original": "Ubicación de almacenamiento (original)",
"doc_section_persons": "Personas",
"doc_sender_not_specified": "No especificado",
"doc_no_receivers": "Sin destinatarios",
"doc_section_content": "Contenido",
"doc_label_summary": "Resumen",
"doc_loading": "Cargando documento...",
"doc_download_link": "Intentar descarga directa",
"doc_no_scan": "No hay escaneo disponible",
"persons_heading": "Directorio de personas",
"persons_subtitle": "Explore el índice de todas las personas registradas en el archivo familiar.",
"persons_btn_new": "Nueva persona",
"persons_search_placeholder": "Buscar nombres...",
"persons_empty_heading": "No se encontraron personas.",
"persons_empty_text": "Pruebe con otro término de búsqueda.",
"persons_new_heading": "Nueva persona",
"persons_section_details": "Datos de la persona",
"person_edit_heading": "Editar persona",
"person_label_full_name": "Nombre completo",
"person_merge_heading": "Fusionar persona",
"person_merge_description": "Esta persona se fusionará con la persona de destino seleccionada. Todos los documentos y enlaces se transferirán y esta persona será eliminada.",
"person_merge_target_label": "Fusionar con",
"person_btn_merge": "Fusionar",
"person_btn_merge_confirm": "Sí, fusionar",
"person_merge_warning": "Atención: Esta acción no se puede deshacer.",
"person_docs_heading": "Documentos enviados",
"person_no_docs": "Esta persona aún no está vinculada como remitente.",
"conv_heading": "Conversaciones",
"conv_subtitle": "Siga la correspondencia entre dos personas cronológicamente.",
"conv_label_person_a": "Persona A (Remitente)",
"conv_label_person_b": "Persona B (Destinatario)",
"conv_label_from": "Período desde",
"conv_label_to": "Período hasta",
"conv_sort_label": "Ordenar:",
"conv_sort_newest": "Más reciente primero",
"conv_sort_oldest": "Más antiguo primero",
"conv_empty_heading": "Seleccione dos personas",
"conv_empty_text": "La correspondencia se mostrará aquí.",
"conv_no_results_heading": "No se encontraron documentos.",
"conv_no_results_text": "Intente ajustar el período de tiempo.",
"admin_heading": "Panel de administración",
"admin_tab_users": "Usuarios",
"admin_tab_groups": "Grupos",
"admin_tab_tags": "Etiquetas",
"admin_section_users": "Gestión de usuarios",
"admin_col_login": "Login",
"admin_col_groups": "Grupos",
"admin_col_password": "Contraseña",
"admin_multiselect_hint": "Ctrl+Clic para seleccionar",
"admin_password_placeholder": "Nueva contraseña (opcional)",
"admin_no_groups": "Sin grupos",
"admin_btn_delete_user_title": "Eliminar usuario",
"admin_section_new_user": "Crear nuevo usuario",
"admin_multiselect_hint_multi": "Ctrl+Clic para varios",
"admin_multiselect_hint_full": "Ctrl+Clic para selección múltiple",
"admin_section_tags": "Etiquetas",
"admin_tags_warning": "Advertencia: Renombrar o eliminar afecta a todos los documentos vinculados.",
"admin_btn_edit_tag_label": "Editar etiqueta",
"admin_tag_delete_confirm": "¿Realmente eliminar? La etiqueta se eliminará de todos los documentos.",
"admin_btn_delete_tag_label": "Eliminar etiqueta",
"admin_section_groups": "Gestión de grupos",
"admin_col_name": "Nombre",
"admin_col_permissions": "Permisos",
"admin_col_actions": "Acciones",
"admin_group_delete_confirm": "¿Realmente eliminar el grupo?",
"admin_section_new_group": "Crear nuevo grupo",
"admin_group_name_placeholder": "Nombre del grupo (p.ej. Editores)",
"comp_typeahead_placeholder": "Escriba un nombre...",
"comp_typeahead_loading": "Buscando...",
"comp_multiselect_placeholder": "Escriba un nombre...",
"comp_multiselect_remove": "Eliminar",
"comp_multiselect_loading": "Buscando...",
"comp_taginput_placeholder_create": "Añadir etiquetas...",
"comp_taginput_placeholder_filter": "Filtrar por etiquetas...",
"comp_taginput_remove": "Eliminar etiqueta",
"comp_taginput_create_hint": "Pulse Enter para crear etiqueta."
} }

View File

@@ -7,10 +7,10 @@
"plugin.inlang.messageFormat": { "plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json" "pathPattern": "./messages/{locale}.json"
}, },
"baseLocale": "en", "baseLocale": "de",
"locales": [ "locales": [
"de",
"en", "en",
"es", "es"
"de"
] ]
} }

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';
type Person = components['schemas']['Person']; type Person = components['schemas']['Person'];
interface Props { interface Props {
@@ -76,7 +77,7 @@
type="button" type="button"
onclick={() => removePerson(person.id)} onclick={() => removePerson(person.id)}
class="text-brand-navy/50 hover:text-red-500 focus:outline-none ml-0.5" class="text-brand-navy/50 hover:text-red-500 focus:outline-none ml-0.5"
aria-label="Entfernen" aria-label={m.comp_multiselect_remove()}
> >
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
@@ -92,7 +93,7 @@
bind:value={searchTerm} bind:value={searchTerm}
oninput={handleInput} oninput={handleInput}
onfocus={() => { updateDropdownPosition(); showDropdown = true; }} onfocus={() => { updateDropdownPosition(); showDropdown = true; }}
placeholder={selectedPersons.length === 0 ? 'Namen tippen...' : ''} placeholder={selectedPersons.length === 0 ? m.comp_multiselect_placeholder() : ''}
class="flex-1 min-w-[120px] border-none p-1 focus:ring-0 text-sm bg-transparent outline-none" class="flex-1 min-w-[120px] border-none p-1 focus:ring-0 text-sm bg-transparent outline-none"
/> />
</div> </div>
@@ -103,7 +104,7 @@
class="z-50 bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" class="z-50 bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm"
> >
{#if loading} {#if loading}
<div class="p-2 text-gray-500 text-sm">Suche...</div> <div class="p-2 text-gray-500 text-sm">{m.comp_multiselect_loading()}</div>
{:else} {:else}
{#each results as person} {#each results as person}
<div <div

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { components } from '$lib/generated/api'; import type { components } from '$lib/generated/api';
import { m } from '$lib/paraglide/messages.js';
type Person = components['schemas']['Person']; type Person = components['schemas']['Person'];
interface Props { interface Props {
@@ -95,7 +96,7 @@
bind:value={searchTerm} bind:value={searchTerm}
oninput={handleInput} oninput={handleInput}
onfocus={() => { updateDropdownPosition(); showDropdown = true; }} onfocus={() => { updateDropdownPosition(); showDropdown = true; }}
placeholder="Namen tippen..." placeholder={m.comp_typeahead_placeholder()}
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2 focus:ring-blue-500 focus:border-blue-500" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2 focus:ring-blue-500 focus:border-blue-500"
/> />
@@ -105,7 +106,7 @@
class="z-50 bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" class="z-50 bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
> >
{#if loading} {#if loading}
<div class="p-2 text-gray-500 text-sm">Suche...</div> <div class="p-2 text-gray-500 text-sm">{m.comp_typeahead_loading()}</div>
{:else} {:else}
{#each results as person} {#each results as person}
<div <div

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { m } from '$lib/paraglide/messages.js';
interface Props { interface Props {
tags?: string[]; tags?: string[];
allowCreation?: boolean; allowCreation?: boolean;
@@ -88,7 +90,7 @@
<button <button
type="button" type="button"
onclick={() => removeTag(i)} onclick={() => removeTag(i)}
aria-label="Schlagwort entfernen" aria-label={m.comp_taginput_remove()}
class="text-brand-navy/50 hover:text-red-500 focus:outline-none" class="text-brand-navy/50 hover:text-red-500 focus:outline-none"
> >
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"
@@ -113,8 +115,8 @@
onfocus={() => fetchSuggestions(inputVal)} onfocus={() => fetchSuggestions(inputVal)}
placeholder={tags.length === 0 placeholder={tags.length === 0
? allowCreation ? allowCreation
? 'Schlagworte hinzufügen...' ? m.comp_taginput_placeholder_create()
: 'Nach Schlagworten filtern...' : m.comp_taginput_placeholder_filter()
: ''} : ''}
class="w-full h-full border-none p-1 focus:ring-0 text-sm bg-transparent outline-none" class="w-full h-full border-none p-1 focus:ring-0 text-sm bg-transparent outline-none"
/> />
@@ -143,6 +145,6 @@
</div> </div>
</div> </div>
{#if allowCreation} {#if allowCreation}
<p class="text-xs text-gray-400 mt-1">Enter drücken um Schlagwort zu erstellen.</p> <p class="text-xs text-gray-400 mt-1">{m.comp_taginput_create_hint()}</p>
{/if} {/if}
</div> </div>

View File

@@ -3,9 +3,15 @@
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { page } from '$app/state'; import { page } from '$app/state';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
import { setLocale, getLocale } from '$lib/paraglide/runtime';
let { children } = $props(); let { children } = $props();
const locales = ['DE', 'EN', 'ES'] as const;
const localeMap = { DE: 'de', EN: 'en', ES: 'es' } as const;
const activeLocale = $derived(getLocale().toUpperCase());
const isAdmin = $derived(page.data.user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'))); const isAdmin = $derived(page.data.user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN')));
// Set after client-side hydration completes. Used by E2E tests to know the // Set after client-side hydration completes. Used by E2E tests to know the
@@ -40,7 +46,7 @@
? 'text-brand-navy bg-brand-purple/15 rounded' ? 'text-brand-navy bg-brand-purple/15 rounded'
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}" : 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
> >
Dokumente {m.nav_documents()}
</a> </a>
<a <a
@@ -50,7 +56,7 @@
? 'text-brand-navy bg-brand-purple/15 rounded' ? 'text-brand-navy bg-brand-purple/15 rounded'
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}" : 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
> >
Personen {m.nav_persons()}
</a> </a>
<a <a
@@ -60,7 +66,7 @@
? 'text-brand-navy bg-brand-purple/15 rounded' ? 'text-brand-navy bg-brand-purple/15 rounded'
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}" : 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
> >
Konversationen {m.nav_conversations()}
</a> </a>
{#if isAdmin} {#if isAdmin}
<a <a
@@ -70,25 +76,41 @@
? 'text-brand-navy bg-brand-purple/15 rounded' ? 'text-brand-navy bg-brand-purple/15 rounded'
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}" : 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
> >
Admin {m.nav_admin()}
</a> </a>
{/if} {/if}
</nav> </nav>
</div> </div>
<!-- Right Side --> <!-- Right Side -->
<div class="flex items-center"> <div class="flex items-center gap-3">
<!-- Language selector -->
<div class="flex items-center gap-1 border-r border-gray-200 pr-3">
{#each locales as locale}
<button
type="button"
onclick={() => setLocale(localeMap[locale])}
class="text-xs font-sans tracking-widest px-1.5 py-1 transition-colors
{activeLocale === locale
? 'font-bold text-brand-navy'
: 'font-normal text-gray-400 hover:text-brand-navy'}"
>
{locale}
</button>
{/each}
</div>
<form action="/logout" method="POST" use:enhance> <form action="/logout" method="POST" use:enhance>
<button <button
type="submit" type="submit"
class="inline-flex items-center gap-1.5 text-xs text-gray-400 hover:text-brand-navy font-bold uppercase font-sans tracking-widest px-3 py-2 transition-colors" class="inline-flex items-center gap-1.5 text-xs text-gray-400 hover:text-brand-navy font-bold uppercase font-sans tracking-widest px-3 py-2 transition-colors"
> >
<img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Account-SM.svg" alt="" aria-hidden="true" class="w-4 h-4 opacity-50" /> <img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Account-SM.svg" alt="" aria-hidden="true" class="w-4 h-4 opacity-50" />
Abmelden {m.nav_logout()}
</button> </button>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</header> </header>
{/if} {/if}

View File

@@ -4,6 +4,7 @@ import { goto } from '$app/navigation';
import TagInput from '$lib/components/TagInput.svelte'; import TagInput from '$lib/components/TagInput.svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
let { data } = $props(); let { data } = $props();
@@ -82,7 +83,7 @@ $effect(() => {
type="text" type="text"
bind:value={q} bind:value={q}
oninput={handleTextSearch} oninput={handleTextSearch}
placeholder="Suche in Titel, Inhalt, Ort..." placeholder={m.docs_search_placeholder()}
class="block w-full border-gray-300 py-2.5 pr-10 pl-3 placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-brand-navy" class="block w-full border-gray-300 py-2.5 pr-10 pl-3 placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-brand-navy"
/> />
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"> <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
@@ -96,14 +97,14 @@ $effect(() => {
class="flex items-center gap-2 border border-gray-300 bg-gray-50 px-4 py-2.5 text-sm font-bold tracking-wide text-gray-600 uppercase transition hover:bg-gray-100 hover:text-brand-navy" class="flex items-center gap-2 border border-gray-300 bg-gray-50 px-4 py-2.5 text-sm font-bold tracking-wide text-gray-600 uppercase transition hover:bg-gray-100 hover:text-brand-navy"
> >
<img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Chevron/Chevron-Down-SM.svg" alt="" aria-hidden="true" class="h-4 w-4 transform transition-transform duration-200 {showAdvanced ? 'rotate-180' : ''}" /> <img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Chevron/Chevron-Down-SM.svg" alt="" aria-hidden="true" class="h-4 w-4 transform transition-transform duration-200 {showAdvanced ? 'rotate-180' : ''}" />
Filter {m.docs_btn_filter()}
</button> </button>
<!-- Reset Button --> <!-- Reset Button -->
<a <a
href="/" href="/"
class="flex items-center justify-center border border-transparent px-3 py-2.5 text-gray-400 transition hover:text-red-500" class="flex items-center justify-center border border-transparent px-3 py-2.5 text-gray-400 transition hover:text-red-500"
title="Filter zurücksetzen" title={m.docs_btn_reset_title()}
> >
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Close-MD.svg" alt="" aria-hidden="true" class="h-5 w-5 opacity-40" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Close-MD.svg" alt="" aria-hidden="true" class="h-5 w-5 opacity-40" />
</a> </a>
@@ -118,7 +119,7 @@ $effect(() => {
<!-- Tag Filter --> <!-- Tag Filter -->
<div class="md:col-span-12"> <div class="md:col-span-12">
<p class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase"> <p class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase">
Schlagworte {m.docs_filter_label_tags()}
</p> </p>
<TagInput bind:tags={tagNames} allowCreation={false} /> <TagInput bind:tags={tagNames} allowCreation={false} />
</div> </div>
@@ -130,7 +131,7 @@ $effect(() => {
> >
<PersonTypeahead <PersonTypeahead
name="senderId" name="senderId"
label="Absender" label={m.docs_filter_label_sender()}
bind:value={senderId} bind:value={senderId}
initialName={data.initialValues?.senderName} initialName={data.initialValues?.senderName}
onchange={triggerSearch} onchange={triggerSearch}
@@ -145,7 +146,7 @@ $effect(() => {
> >
<PersonTypeahead <PersonTypeahead
name="receiverId" name="receiverId"
label="Empfänger" label={m.docs_filter_label_receivers()}
bind:value={receiverId} bind:value={receiverId}
initialName={data.initialValues?.receiverName} initialName={data.initialValues?.receiverName}
onchange={triggerSearch} onchange={triggerSearch}
@@ -159,7 +160,7 @@ $effect(() => {
<label <label
for="from" for="from"
class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase" class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase"
>Von</label >{m.docs_filter_label_from()}</label
> >
<input <input
type="date" type="date"
@@ -173,7 +174,7 @@ $effect(() => {
<label <label
for="to" for="to"
class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase" class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase"
>Bis</label >{m.docs_filter_label_to()}</label
> >
<input <input
type="date" type="date"
@@ -195,7 +196,7 @@ $effect(() => {
class="inline-flex items-center gap-1 text-sm font-medium text-brand-navy/60 transition-colors hover:text-brand-navy" class="inline-flex items-center gap-1 text-sm font-medium text-brand-navy/60 transition-colors hover:text-brand-navy"
> >
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Add/Add-General-MD.svg" alt="" aria-hidden="true" class="h-4 w-4" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Add/Add-General-MD.svg" alt="" aria-hidden="true" class="h-4 w-4" />
Neues Dokument {m.docs_btn_new()}
</a> </a>
</div> </div>
@@ -252,27 +253,27 @@ $effect(() => {
<div class="flex items-baseline"> <div class="flex items-baseline">
<span <span
class="w-10 font-sans text-xs font-bold tracking-wide text-gray-400 uppercase" class="w-10 font-sans text-xs font-bold tracking-wide text-gray-400 uppercase"
>Von</span >{m.docs_list_from()}</span
> >
{#if doc.sender} {#if doc.sender}
<span class="text-gray-900" <span class="text-gray-900"
>{doc.sender.firstName} {doc.sender.lastName}</span >{doc.sender.firstName} {doc.sender.lastName}</span
> >
{:else} {:else}
<span class="text-gray-400 italic">Unbekannt</span> <span class="text-gray-400 italic">{m.docs_list_unknown()}</span>
{/if} {/if}
</div> </div>
<div class="flex items-baseline"> <div class="flex items-baseline">
<span <span
class="w-10 font-sans text-xs font-bold tracking-wide text-gray-400 uppercase" class="w-10 font-sans text-xs font-bold tracking-wide text-gray-400 uppercase"
>An</span >{m.docs_list_to()}</span
> >
{#if doc.receivers && doc.receivers.length > 0} {#if doc.receivers && doc.receivers.length > 0}
<span class="text-gray-900"> <span class="text-gray-900">
{doc.receivers.map((p) => p.firstName + ' ' + p.lastName).join(', ')} {doc.receivers.map((p) => p.firstName + ' ' + p.lastName).join(', ')}
</span> </span>
{:else} {:else}
<span class="text-gray-400 italic">Unbekannt</span> <span class="text-gray-400 italic">{m.docs_list_unknown()}</span>
{/if} {/if}
</div> </div>
</div> </div>
@@ -312,15 +313,15 @@ $effect(() => {
> >
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Mag-Glass-MD.svg" alt="" aria-hidden="true" class="h-6 w-6" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Mag-Glass-MD.svg" alt="" aria-hidden="true" class="h-6 w-6" />
</div> </div>
<h3 class="font-serif text-lg font-medium text-brand-navy">Keine Dokumente gefunden</h3> <h3 class="font-serif text-lg font-medium text-brand-navy">{m.docs_empty_heading()}</h3>
<p class="mt-1 font-sans text-sm text-gray-500"> <p class="mt-1 font-sans text-sm text-gray-500">
Versuchen Sie, die Filter anzupassen oder den Suchbegriff zu ändern. {m.docs_empty_text()}
</p> </p>
<button <button
onclick={() => goto('/')} onclick={() => goto('/')}
class="mt-6 text-sm font-bold tracking-wide text-brand-mint uppercase transition hover:text-brand-navy" class="mt-6 text-sm font-bold tracking-wide text-brand-mint uppercase transition hover:text-brand-navy"
> >
Alle Filter löschen {m.docs_empty_btn_clear()}
</button> </button>
</div> </div>
{/if} {/if}

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { m } from '$lib/paraglide/messages.js';
let { data, form } = $props(); let { data, form } = $props();
@@ -41,7 +42,7 @@
<div class="max-w-7xl mx-auto py-8 sm:px-6 lg:px-8 font-sans"> <div class="max-w-7xl mx-auto py-8 sm:px-6 lg:px-8 font-sans">
<div class="flex items-center justify-between mb-8"> <div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-serif text-brand-navy">Admin Dashboard</h1> <h1 class="text-3xl font-serif text-brand-navy">{m.admin_heading()}</h1>
<!-- Tabs --> <!-- Tabs -->
<div class="flex bg-white rounded-lg p-1 shadow-sm border border-gray-200"> <div class="flex bg-white rounded-lg p-1 shadow-sm border border-gray-200">
@@ -50,21 +51,21 @@
'users' 'users'
? 'bg-brand-navy text-white' ? 'bg-brand-navy text-white'
: 'text-gray-500 hover:text-brand-navy'}" : 'text-gray-500 hover:text-brand-navy'}"
onclick={() => (activeTab = 'users')}>Benutzer</button onclick={() => (activeTab = 'users')}>{m.admin_tab_users()}</button
> >
<button <button
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab === class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
'groups' 'groups'
? 'bg-brand-navy text-white' ? 'bg-brand-navy text-white'
: 'text-gray-500 hover:text-brand-navy'}" : 'text-gray-500 hover:text-brand-navy'}"
onclick={() => (activeTab = 'groups')}>Gruppen</button onclick={() => (activeTab = 'groups')}>{m.admin_tab_groups()}</button
> >
<button <button
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab === class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
'tags' 'tags'
? 'bg-brand-navy text-white' ? 'bg-brand-navy text-white'
: 'text-gray-500 hover:text-brand-navy'}" : 'text-gray-500 hover:text-brand-navy'}"
onclick={() => (activeTab = 'tags')}>Schlagworte</button onclick={() => (activeTab = 'tags')}>{m.admin_tab_tags()}</button
> >
</div> </div>
</div> </div>
@@ -78,22 +79,22 @@
{#if activeTab === 'users'} {#if activeTab === 'users'}
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide> <div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
<div class="p-6 border-b border-gray-100 flex justify-between items-center"> <div class="p-6 border-b border-gray-100 flex justify-between items-center">
<h2 class="text-lg font-bold text-gray-700">Benutzerverwaltung</h2> <h2 class="text-lg font-bold text-gray-700">{m.admin_section_users()}</h2>
</div> </div>
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider" <th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
>Login</th >{m.admin_col_login()}</th
> >
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider" <th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
>Gruppen</th >{m.admin_col_groups()}</th
> >
{#if editingUserId} {#if editingUserId}
<th <th
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider" class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
>Passwort</th >{m.admin_col_password()}</th
> >
{/if} {/if}
</tr> </tr>
@@ -129,7 +130,7 @@
</option> </option>
{/each} {/each}
</select> </select>
<p class="text-[10px] text-gray-400 mt-1">Strg+Klick für Auswahl</p> <p class="text-[10px] text-gray-400 mt-1">{m.admin_multiselect_hint()}</p>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-right align-top"> <td class="px-6 py-4 whitespace-nowrap text-right align-top">
@@ -147,7 +148,7 @@
<input <input
type="password" type="password"
name="password" name="password"
placeholder="Neues PW (optional)" placeholder={m.admin_password_placeholder()}
class="w-32 py-1 px-2 text-xs border border-brand-mint rounded" class="w-32 py-1 px-2 text-xs border border-brand-mint rounded"
/> />
@@ -156,14 +157,14 @@
type="submit" type="submit"
class="bg-green-600 text-white px-2 py-1 rounded text-xs font-bold uppercase hover:bg-green-700" class="bg-green-600 text-white px-2 py-1 rounded text-xs font-bold uppercase hover:bg-green-700"
> >
Speichern {m.btn_save()}
</button> </button>
<button <button
type="button" type="button"
onclick={cancelEditUser} onclick={cancelEditUser}
class="bg-gray-200 text-gray-600 px-2 py-1 rounded text-xs font-bold uppercase hover:bg-gray-300" class="bg-gray-200 text-gray-600 px-2 py-1 rounded text-xs font-bold uppercase hover:bg-gray-300"
> >
Abbrechen {m.btn_cancel()}
</button> </button>
</div> </div>
</form> </form>
@@ -184,7 +185,7 @@
</span> </span>
{/each} {/each}
{:else} {:else}
<span class="text-xs text-gray-400 italic">Keine Gruppen</span> <span class="text-xs text-gray-400 italic">{m.admin_no_groups()}</span>
{/if} {/if}
</div> </div>
</td> </td>
@@ -194,7 +195,7 @@
onclick={() => startEditUser(user.id)} onclick={() => startEditUser(user.id)}
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide" class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
> >
Bearbeiten {m.btn_edit()}
</button> </button>
<form <form
@@ -213,7 +214,7 @@
<input type="hidden" name="id" value={user.id} /> <input type="hidden" name="id" value={user.id} />
<button <button
class="text-gray-300 hover:text-red-600 transition-colors p-1" class="text-gray-300 hover:text-red-600 transition-colors p-1"
title="Benutzer löschen" title={m.admin_btn_delete_user_title()}
> >
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path <path
@@ -236,7 +237,7 @@
<!-- Create User Form --> <!-- Create User Form -->
<div class="p-6 bg-gray-50 border-t border-gray-200"> <div class="p-6 bg-gray-50 border-t border-gray-200">
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide"> <h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
Neuen Benutzer anlegen {m.admin_section_new_user()}
</h3> </h3>
<form <form
method="POST" method="POST"
@@ -254,7 +255,7 @@
<input <input
type="password" type="password"
name="password" name="password"
placeholder="Passwort" placeholder={m.admin_col_password()}
required required
class="rounded border-gray-300 text-sm w-full" class="rounded border-gray-300 text-sm w-full"
/> />
@@ -265,19 +266,19 @@
multiple multiple
class="rounded border-gray-300 text-sm w-full h-[42px] py-1" class="rounded border-gray-300 text-sm w-full h-[42px] py-1"
required required
title="Strg+Klick für mehrere" title={m.admin_multiselect_hint_multi()}
> >
{#each data.groups as group} {#each data.groups as group}
<option value={group.id}>{group.name}</option> <option value={group.id}>{group.name}</option>
{/each} {/each}
</select> </select>
<p class="text-[10px] text-gray-400 mt-1">Strg+Klick für Mehrfachauswahl</p> <p class="text-[10px] text-gray-400 mt-1">{m.admin_multiselect_hint_full()}</p>
</div> </div>
<button <button
type="submit" type="submit"
class="bg-brand-navy text-white h-[42px] rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full" class="bg-brand-navy text-white h-[42px] rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full"
>Anlegen</button >{m.btn_create()}</button
> >
</form> </form>
</div> </div>
@@ -285,9 +286,9 @@
{:else if activeTab === 'tags'} {:else if activeTab === 'tags'}
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide> <div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
<div class="p-6 border-b border-gray-100 bg-yellow-50/50"> <div class="p-6 border-b border-gray-100 bg-yellow-50/50">
<h2 class="text-lg font-bold text-gray-700">Schlagworte</h2> <h2 class="text-lg font-bold text-gray-700">{m.admin_section_tags()}</h2>
<p class="text-xs text-yellow-800 mt-1"> <p class="text-xs text-yellow-800 mt-1">
Warnung: Umbenennen oder Löschen wirkt sich auf alle verknüpften Dokumente aus. {m.admin_tags_warning()}
</p> </p>
</div> </div>
@@ -312,7 +313,7 @@
bind:value={editingTagName} bind:value={editingTagName}
class="flex-1 border-brand-mint ring-1 ring-brand-mint rounded px-2 py-1 text-sm" class="flex-1 border-brand-mint ring-1 ring-brand-mint rounded px-2 py-1 text-sm"
/> />
<button aria-label="Speichern" class="text-green-600 hover:text-green-800" <button aria-label={m.btn_save()} class="text-green-600 hover:text-green-800"
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" ><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path ><path
stroke-linecap="round" stroke-linecap="round"
@@ -325,7 +326,7 @@
<button <button
type="button" type="button"
onclick={cancelEditTag} onclick={cancelEditTag}
aria-label="Abbrechen" aria-label={m.btn_cancel()}
class="text-gray-400 hover:text-gray-600" class="text-gray-400 hover:text-gray-600"
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" ><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path ><path
@@ -346,7 +347,7 @@
> >
<button <button
onclick={() => startEditTag(tag)} onclick={() => startEditTag(tag)}
aria-label="Schlagwort bearbeiten" aria-label={m.admin_btn_edit_tag_label()}
class="p-1 text-gray-400 hover:text-brand-navy" class="p-1 text-gray-400 hover:text-brand-navy"
> >
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
@@ -363,9 +364,7 @@
action="?/deleteTag" action="?/deleteTag"
use:enhance={({ cancel }) => { use:enhance={({ cancel }) => {
if ( if (
!confirm( !confirm(m.admin_tag_delete_confirm())
'Wirklich löschen? Das Schlagwort wird aus allen Dokumenten entfernt.'
)
) { ) {
cancel(); cancel();
} }
@@ -376,7 +375,7 @@
class="inline" class="inline"
> >
<input type="hidden" name="id" value={tag.id} /> <input type="hidden" name="id" value={tag.id} />
<button aria-label="Schlagwort löschen" class="p-1 text-gray-400 hover:text-red-600"> <button aria-label={m.admin_btn_delete_tag_label()} class="p-1 text-gray-400 hover:text-red-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path ><path
stroke-linecap="round" stroke-linecap="round"
@@ -396,21 +395,21 @@
{:else if activeTab === 'groups'} {:else if activeTab === 'groups'}
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide> <div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
<div class="p-6 border-b border-gray-100 flex justify-between items-center"> <div class="p-6 border-b border-gray-100 flex justify-between items-center">
<h2 class="text-lg font-bold text-gray-700">Gruppenverwaltung</h2> <h2 class="text-lg font-bold text-gray-700">{m.admin_section_groups()}</h2>
</div> </div>
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider" <th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
>Name</th >{m.admin_col_name()}</th
> >
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider" <th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
>Berechtigungen</th >{m.admin_col_permissions()}</th
> >
<th <th
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider" class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
>Aktionen</th >{m.admin_col_actions()}</th
> >
</tr> </tr>
</thead> </thead>
@@ -460,7 +459,7 @@
</div> </div>
<div class="flex gap-2 self-start sm:self-center"> <div class="flex gap-2 self-start sm:self-center">
<button type="submit" aria-label="Speichern" class="text-green-600 hover:text-green-800 p-1"> <button type="submit" aria-label={m.btn_save()} class="text-green-600 hover:text-green-800 p-1">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path ><path
stroke-linecap="round" stroke-linecap="round"
@@ -473,7 +472,7 @@
<button <button
type="button" type="button"
onclick={cancelEditGroup} onclick={cancelEditGroup}
aria-label="Abbrechen" aria-label={m.btn_cancel()}
class="text-gray-400 hover:text-red-500 p-1" class="text-gray-400 hover:text-red-500 p-1"
> >
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
@@ -513,14 +512,14 @@
onclick={() => startEditGroup(group.id)} onclick={() => startEditGroup(group.id)}
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide" class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
> >
Bearbeiten {m.btn_edit()}
</button> </button>
<form <form
method="POST" method="POST"
action="?/deleteGroup" action="?/deleteGroup"
use:enhance={({ cancel }) => { use:enhance={({ cancel }) => {
if (!confirm('Gruppe wirklich löschen?')) { if (!confirm(m.admin_group_delete_confirm())) {
cancel(); cancel();
} }
return async ({ update }) => { return async ({ update }) => {
@@ -531,7 +530,7 @@
<input type="hidden" name="id" value={group.id} /> <input type="hidden" name="id" value={group.id} />
<button <button
class="text-gray-300 hover:text-red-600 p-1 transition-colors" class="text-gray-300 hover:text-red-600 p-1 transition-colors"
title="Löschen" title={m.btn_delete()}
> >
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path <path
@@ -554,7 +553,7 @@
<!-- CREATE GROUP FORM --> <!-- CREATE GROUP FORM -->
<div class="p-6 bg-gray-50 border-t border-gray-200"> <div class="p-6 bg-gray-50 border-t border-gray-200">
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide"> <h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
Neue Gruppe anlegen {m.admin_section_new_group()}
</h3> </h3>
<form <form
method="POST" method="POST"
@@ -566,7 +565,7 @@
<input <input
type="text" type="text"
name="name" name="name"
placeholder="Gruppenname (z.B. Editoren)" placeholder={m.admin_group_name_placeholder()}
required required
class="rounded border-gray-300 text-sm w-full" class="rounded border-gray-300 text-sm w-full"
/> />
@@ -590,7 +589,7 @@
type="submit" type="submit"
class="bg-brand-navy text-white px-6 py-2 rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full md:w-auto" class="bg-brand-navy text-white px-6 py-2 rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full md:w-auto"
> >
Anlegen {m.btn_create()}
</button> </button>
</form> </form>
</div> </div>

View File

@@ -2,6 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte'; import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
let { data } = $props(); let { data } = $props();
@@ -39,9 +40,9 @@
<div class="max-w-5xl mx-auto py-10 px-4"> <div class="max-w-5xl mx-auto py-10 px-4">
<!-- Page Header --> <!-- Page Header -->
<div class="mb-8 border-b border-brand-navy/10 pb-4"> <div class="mb-8 border-b border-brand-navy/10 pb-4">
<h1 class="text-3xl font-serif font-medium text-brand-navy">Konversationen</h1> <h1 class="text-3xl font-serif font-medium text-brand-navy">{m.conv_heading()}</h1>
<p class="text-brand-navy/60 font-sans text-sm mt-2"> <p class="text-brand-navy/60 font-sans text-sm mt-2">
Verfolgen Sie den Schriftverkehr zwischen zwei Personen chronologisch. {m.conv_subtitle()}
</p> </p>
</div> </div>
@@ -54,7 +55,7 @@
> >
<PersonTypeahead <PersonTypeahead
name="senderId" name="senderId"
label="Person A (Absender)" label={m.conv_label_person_a()}
bind:value={senderId} bind:value={senderId}
initialName={data.initialValues.senderName} initialName={data.initialValues.senderName}
onchange={() => applyFilters()} onchange={() => applyFilters()}
@@ -67,7 +68,7 @@
> >
<PersonTypeahead <PersonTypeahead
name="receiverId" name="receiverId"
label="Person B (Empfänger)" label={m.conv_label_person_b()}
bind:value={receiverId} bind:value={receiverId}
initialName={data.initialValues.receiverName} initialName={data.initialValues.receiverName}
onchange={() => applyFilters()} onchange={() => applyFilters()}
@@ -81,7 +82,7 @@
<label <label
for="dateFrom" for="dateFrom"
class="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-2" class="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-2"
>Zeitraum von</label >{m.conv_label_from()}</label
> >
<input <input
id="dateFrom" id="dateFrom"
@@ -97,7 +98,7 @@
<label <label
for="dateTo" for="dateTo"
class="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-2" class="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-2"
>Zeitraum bis</label >{m.conv_label_to()}</label
> >
<input <input
id="dateTo" id="dateTo"
@@ -114,8 +115,8 @@
onclick={toggleSort} onclick={toggleSort}
class="w-full flex items-center justify-center h-[42px] border border-brand-sand text-xs font-bold uppercase tracking-wide text-brand-navy hover:bg-brand-navy hover:text-white transition-colors" class="w-full flex items-center justify-center h-[42px] border border-brand-sand text-xs font-bold uppercase tracking-wide text-brand-navy hover:bg-brand-navy hover:text-white transition-colors"
> >
<span class="mr-2">Sortierung:</span> <span class="mr-2">{m.conv_sort_label()}</span>
<span>{sortDir === 'DESC' ? 'Neueste zuerst' : 'Älteste zuerst'}</span> <span>{sortDir === 'DESC' ? m.conv_sort_newest() : m.conv_sort_oldest()}</span>
<svg <svg
class="w-4 h-4 ml-2 transform {sortDir === 'ASC' class="w-4 h-4 ml-2 transform {sortDir === 'ASC'
? 'rotate-180' ? 'rotate-180'
@@ -147,15 +148,15 @@
/></svg /></svg
> >
</div> </div>
<p class="text-brand-navy font-serif text-lg">Wählen Sie zwei Personen aus</p> <p class="text-brand-navy font-serif text-lg">{m.conv_empty_heading()}</p>
<p class="text-gray-500 font-sans text-sm mt-1">Die Korrespondenz wird hier angezeigt.</p> <p class="text-gray-500 font-sans text-sm mt-1">{m.conv_empty_text()}</p>
</div> </div>
{:else if data.documents.length === 0} {:else if data.documents.length === 0}
<div <div
class="flex flex-col items-center justify-center py-24 bg-white border border-brand-sand rounded-sm text-center shadow-sm" class="flex flex-col items-center justify-center py-24 bg-white border border-brand-sand rounded-sm text-center shadow-sm"
> >
<p class="text-brand-navy font-serif">Keine Dokumente gefunden.</p> <p class="text-brand-navy font-serif">{m.conv_no_results_heading()}</p>
<p class="text-gray-400 text-sm mt-2">Versuchen Sie, den Zeitraum anzupassen.</p> <p class="text-gray-400 text-sm mt-2">{m.conv_no_results_text()}</p>
</div> </div>
{:else} {:else}
<!-- CHAT CONTAINER --> <!-- CHAT CONTAINER -->

View File

@@ -7,7 +7,7 @@
<h1>{m.hello_world({ name: 'SvelteKit User' })}</h1> <h1>{m.nav_documents()}</h1>
<div> <div>
<button onclick={() => setLocale('en')}>en</button> <button onclick={() => setLocale('en')}>en</button>
<button onclick={() => setLocale('es')}>es</button> <button onclick={() => setLocale('es')}>es</button>

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { m } from '$lib/paraglide/messages.js';
let { data } = $props(); let { data } = $props();
const doc = $derived(data.document); const doc = $derived(data.document);
@@ -53,7 +55,7 @@
> >
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4" />
</div> </div>
<span>Zurück</span> <span>{m.btn_back()}</span>
</a> </a>
<div class="flex items-center gap-4 overflow-hidden border-l border-gray-200 pl-6"> <div class="flex items-center gap-4 overflow-hidden border-l border-gray-200 pl-6">
@@ -77,7 +79,7 @@
class="text-brand-navy bg-transparent border border-brand-navy hover:bg-brand-navy hover:text-white px-4 py-2 rounded text-sm font-medium transition flex items-center gap-2" class="text-brand-navy bg-transparent border border-brand-navy hover:bg-brand-navy hover:text-white px-4 py-2 rounded text-sm font-medium transition flex items-center gap-2"
> >
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg" alt="" aria-hidden="true" class="w-4 h-4" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg" alt="" aria-hidden="true" class="w-4 h-4" />
Bearbeiten {m.btn_edit()}
</a> </a>
{#if doc.filePath} {#if doc.filePath}
@@ -105,7 +107,7 @@
<h3 <h3
class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2" class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2"
> >
Details {m.doc_section_details()}
</h3> </h3>
<div class="space-y-5"> <div class="space-y-5">
<!-- Date --> <!-- Date -->
@@ -117,7 +119,7 @@
<span class="block font-serif text-lg text-brand-navy"> <span class="block font-serif text-lg text-brand-navy">
{doc.documentDate ? new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(doc.documentDate + 'T12:00:00')) : '—'} {doc.documentDate ? new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(doc.documentDate + 'T12:00:00')) : '—'}
</span> </span>
<span class="text-xs font-sans text-gray-500">Dokumentendatum</span> <span class="text-xs font-sans text-gray-500">{m.doc_label_document_date()}</span>
</div> </div>
</div> </div>
@@ -130,7 +132,7 @@
<span class="block font-serif text-lg text-brand-navy"> <span class="block font-serif text-lg text-brand-navy">
{doc.location ? doc.location : '—'} {doc.location ? doc.location : '—'}
</span> </span>
<span class="text-xs font-sans text-gray-500">Erstellungsort</span> <span class="text-xs font-sans text-gray-500">{m.doc_label_creation_location()}</span>
</div> </div>
</div> </div>
@@ -144,7 +146,7 @@
<span class="block font-serif text-lg text-brand-navy"> <span class="block font-serif text-lg text-brand-navy">
{doc.documentLocation} {doc.documentLocation}
</span> </span>
<span class="text-xs font-sans text-gray-500">Aufbewahrungsort (Original)</span> <span class="text-xs font-sans text-gray-500">{m.doc_label_archive_location_original()}</span>
</div> </div>
</div> </div>
{/if} {/if}
@@ -167,7 +169,7 @@
</a> </a>
{/each} {/each}
</div> </div>
<span class="text-xs font-sans text-gray-500">Schlagworte</span> <span class="text-xs font-sans text-gray-500">{m.form_label_tags()}</span>
</div> </div>
</div> </div>
{/if} {/if}
@@ -179,11 +181,11 @@
<h3 <h3
class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2" class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2"
> >
Personen {m.doc_section_persons()}
</h3> </h3>
<div class="mb-6"> <div class="mb-6">
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Absender</span> <span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.form_label_sender()}</span>
{#if doc.sender} {#if doc.sender}
<a <a
href="/persons/{doc.sender.id}" href="/persons/{doc.sender.id}"
@@ -209,12 +211,12 @@
</div> </div>
</a> </a>
{:else} {:else}
<span class="text-sm font-serif text-gray-400 italic">Nicht angegeben</span> <span class="text-sm font-serif text-gray-400 italic">{m.doc_sender_not_specified()}</span>
{/if} {/if}
</div> </div>
<div> <div>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Empfänger</span> <span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.form_label_receivers()}</span>
{#if doc.receivers && doc.receivers.length > 0} {#if doc.receivers && doc.receivers.length > 0}
<div class="space-y-2"> <div class="space-y-2">
{#each doc.receivers as receiver} {#each doc.receivers as receiver}
@@ -248,7 +250,7 @@
{/each} {/each}
</div> </div>
{:else} {:else}
<span class="text-sm font-serif text-gray-400 italic">Keine Empfänger</span> <span class="text-sm font-serif text-gray-400 italic">{m.doc_no_receivers()}</span>
{/if} {/if}
</div> </div>
</div> </div>
@@ -259,13 +261,13 @@
<h3 <h3
class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2" class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2"
> >
Inhalt {m.doc_section_content()}
</h3> </h3>
<div class="space-y-6"> <div class="space-y-6">
{#if doc.summary} {#if doc.summary}
<div> <div>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Zusammenfassung</span> <span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.doc_label_summary()}</span>
<div <div
class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap" class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap"
> >
@@ -276,7 +278,7 @@
{#if doc.transcription} {#if doc.transcription}
<div> <div>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Transkription</span> <span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.form_label_transcription()}</span>
<div <div
class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap" class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap"
> >
@@ -314,7 +316,7 @@
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> ></path>
</svg> </svg>
<span class="font-sans text-sm tracking-wide">Lade Dokument...</span> <span class="font-sans text-sm tracking-wide">{m.doc_loading()}</span>
</div> </div>
{:else if error} {:else if error}
<div class="text-gray-400 text-center px-4"> <div class="text-gray-400 text-center px-4">
@@ -325,7 +327,7 @@
target="_blank" target="_blank"
class="underline hover:text-white text-sm" class="underline hover:text-white text-sm"
> >
Direkter Download versuchen {m.doc_download_link()}
</a> </a>
{/if} {/if}
</div> </div>
@@ -334,7 +336,7 @@
<div class="bg-white/5 p-8 rounded-full mb-6"> <div class="bg-white/5 p-8 rounded-full mb-6">
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg" alt="" aria-hidden="true" class="w-12 h-12 opacity-50 invert" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg" alt="" aria-hidden="true" class="w-12 h-12 opacity-50 invert" />
</div> </div>
<p class="font-sans text-sm tracking-wide uppercase">Kein Scan vorhanden</p> <p class="font-sans text-sm tracking-wide uppercase">{m.doc_no_scan()}</p>
</div> </div>
{:else if fileUrl && doc.originalFilename.toLowerCase().endsWith('.pdf')} {:else if fileUrl && doc.originalFilename.toLowerCase().endsWith('.pdf')}
<iframe <iframe

View File

@@ -5,6 +5,7 @@
import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte'; import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { isoToGerman, germanToIso } from '$lib/utils'; import { isoToGerman, germanToIso } from '$lib/utils';
import { m } from '$lib/paraglide/messages.js';
let { data, form } = $props(); let { data, form } = $props();
@@ -43,10 +44,10 @@
<div class="mb-6"> <div class="mb-6">
<a href="/documents/{doc.id}" class="inline-flex items-center text-xs font-bold uppercase tracking-widest text-gray-500 hover:text-brand-navy transition-colors group mb-4"> <a href="/documents/{doc.id}" class="inline-flex items-center text-xs font-bold uppercase tracking-widest text-gray-500 hover:text-brand-navy transition-colors group mb-4">
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" />
Zurück zum Dokument {m.btn_back_to_document()}
</a> </a>
<h1 class="text-3xl font-serif text-brand-navy"> <h1 class="text-3xl font-serif text-brand-navy">
Bearbeiten<span class="text-brand-navy/70">{doc.title || doc.originalFilename}</span> {m.doc_edit_heading()}<span class="text-brand-navy/70">{doc.title || doc.originalFilename}</span>
</h1> </h1>
</div> </div>
@@ -58,20 +59,20 @@
<!-- ── Section 1: Wer & Wann ── --> <!-- ── Section 1: Wer & Wann ── -->
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6"> <div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Wer &amp; Wann</h2> <h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.doc_section_who_when()}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5"> <div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<!-- Datum --> <!-- Datum -->
<div> <div>
<label for="documentDate" class="block text-sm font-medium text-gray-700 mb-1">Datum</label> <label for="documentDate" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_date()}</label>
<input <input
id="documentDate" id="documentDate"
type="text" type="text"
inputmode="numeric" inputmode="numeric"
value={dateDisplay} value={dateDisplay}
oninput={handleDateInput} oninput={handleDateInput}
placeholder="TT.MM.JJJJ" placeholder={m.form_placeholder_date()}
maxlength="10" maxlength="10"
class="block w-full rounded border-gray-300 shadow-sm p-2 border text-sm class="block w-full rounded border-gray-300 shadow-sm p-2 border text-sm
{dateInvalid ? 'border-red-400 focus:border-red-500 focus:ring-red-500' : 'focus:border-brand-navy focus:ring-brand-navy'}" {dateInvalid ? 'border-red-400 focus:border-red-500 focus:ring-red-500' : 'focus:border-brand-navy focus:ring-brand-navy'}"
@@ -79,19 +80,19 @@
/> />
<input type="hidden" name="documentDate" value={dateIso} /> <input type="hidden" name="documentDate" value={dateIso} />
{#if dateInvalid} {#if dateInvalid}
<p id="date-error" class="mt-1 text-xs text-red-600">Bitte im Format TT.MM.JJJJ eingeben, z.B. 20.12.2026</p> <p id="date-error" class="mt-1 text-xs text-red-600">{m.form_date_error()}</p>
{/if} {/if}
</div> </div>
<!-- Ort --> <!-- Ort -->
<div> <div>
<label for="location" class="block text-sm font-medium text-gray-700 mb-1">Ort</label> <label for="location" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_location()}</label>
<input <input
id="location" id="location"
type="text" type="text"
name="location" name="location"
value={doc.location || ''} value={doc.location || ''}
placeholder="z.B. Berlin, Wien…" placeholder={m.form_placeholder_location()}
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm" class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm"
/> />
</div> </div>
@@ -100,7 +101,7 @@
<div> <div>
<PersonTypeahead <PersonTypeahead
name="senderId" name="senderId"
label="Absender" label={m.form_label_sender()}
bind:value={senderId} bind:value={senderId}
initialName={doc.sender ? `${doc.sender.firstName} ${doc.sender.lastName}` : ''} initialName={doc.sender ? `${doc.sender.firstName} ${doc.sender.lastName}` : ''}
/> />
@@ -108,7 +109,7 @@
<!-- Empfänger --> <!-- Empfänger -->
<div> <div>
<p class="block text-sm font-medium text-gray-700 mb-1">Empfänger</p> <p class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_receivers()}</p>
<PersonMultiSelect bind:selectedPersons={selectedReceivers} /> <PersonMultiSelect bind:selectedPersons={selectedReceivers} />
</div> </div>
@@ -117,13 +118,13 @@
<!-- ── Section 2: Beschreibung ── --> <!-- ── Section 2: Beschreibung ── -->
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6"> <div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Beschreibung</h2> <h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.doc_section_description()}</h2>
<div class="space-y-5"> <div class="space-y-5">
<!-- Titel --> <!-- Titel -->
<div> <div>
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Titel *</label> <label for="title" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_title()} *</label>
<input <input
id="title" id="title"
type="text" type="text"
@@ -136,33 +137,33 @@
<!-- Aufbewahrungsort --> <!-- Aufbewahrungsort -->
<div> <div>
<label for="documentLocation" class="block text-sm font-medium text-gray-700 mb-1">Aufbewahrungsort</label> <label for="documentLocation" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_archive_location()}</label>
<input <input
id="documentLocation" id="documentLocation"
type="text" type="text"
name="documentLocation" name="documentLocation"
value={doc.documentLocation || ''} value={doc.documentLocation || ''}
placeholder="z.B. Schrank 3, Mappe B" placeholder={m.form_placeholder_archive_location()}
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm" class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm"
/> />
<p class="mt-1 text-xs text-gray-400">Wo befindet sich das Originaldokument?</p> <p class="mt-1 text-xs text-gray-400">{m.form_helper_archive_location()}</p>
</div> </div>
<!-- Schlagworte --> <!-- Schlagworte -->
<div> <div>
<p class="block text-sm font-medium text-gray-700 mb-1">Schlagworte</p> <p class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_tags()}</p>
<TagInput bind:tags /> <TagInput bind:tags />
<input type="hidden" name="tags" value={tags.join(',')} /> <input type="hidden" name="tags" value={tags.join(',')} />
</div> </div>
<!-- Inhalt --> <!-- Inhalt -->
<div> <div>
<label for="summary" class="block text-sm font-medium text-gray-700 mb-1">Inhalt</label> <label for="summary" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_content()}</label>
<textarea <textarea
id="summary" id="summary"
name="summary" name="summary"
rows="5" rows="5"
placeholder="Kurze Beschreibung des Inhalts…" placeholder={m.form_placeholder_content()}
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm font-serif" class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm font-serif"
>{doc.summary || ''}</textarea> >{doc.summary || ''}</textarea>
</div> </div>
@@ -172,27 +173,27 @@
<!-- ── Section 3: Transkription ── --> <!-- ── Section 3: Transkription ── -->
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6"> <div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Transkription</h2> <h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.form_label_transcription()}</h2>
<textarea <textarea
id="transcription" id="transcription"
name="transcription" name="transcription"
rows="12" rows="12"
placeholder="Vollständiger Text des Dokuments…" placeholder={m.form_placeholder_transcription()}
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm font-serif" class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm font-serif"
>{doc.transcription || ''}</textarea> >{doc.transcription || ''}</textarea>
</div> </div>
<!-- ── Section 4: Datei ── --> <!-- ── Section 4: Datei ── -->
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6"> <div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Datei</h2> <h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.doc_section_file()}</h2>
<div class="flex items-center gap-3 mb-4 text-sm text-gray-600 bg-brand-sand/20 rounded px-3 py-2"> <div class="flex items-center gap-3 mb-4 text-sm text-gray-600 bg-brand-sand/20 rounded px-3 py-2">
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 flex-shrink-0" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 flex-shrink-0" />
<span>Aktuelle Datei: <strong class="text-brand-navy font-medium">{doc.originalFilename}</strong></span> <span>{m.doc_current_file_label()} <strong class="text-brand-navy font-medium">{doc.originalFilename}</strong></span>
</div> </div>
<label for="file-upload" class="block text-sm font-medium text-gray-700 mb-1"> <label for="file-upload" class="block text-sm font-medium text-gray-700 mb-1">
Neue Datei hochladen <span class="font-normal text-gray-400">(ersetzt die aktuelle Datei)</span> {m.doc_file_replace_label()} <span class="font-normal text-gray-400">({m.doc_file_replace_note()})</span>
</label> </label>
<input <input
id="file-upload" id="file-upload"
@@ -213,13 +214,13 @@
href="/documents/{doc.id}" href="/documents/{doc.id}"
class="text-sm text-gray-500 hover:text-brand-navy transition-colors font-medium" class="text-sm text-gray-500 hover:text-brand-navy transition-colors font-medium"
> >
Abbrechen {m.btn_cancel()}
</a> </a>
<button <button
type="submit" type="submit"
class="px-6 py-2 bg-brand-navy text-white text-sm font-bold uppercase tracking-widest rounded hover:bg-brand-navy/80 transition-colors" class="px-6 py-2 bg-brand-navy text-white text-sm font-bold uppercase tracking-widest rounded hover:bg-brand-navy/80 transition-colors"
> >
Speichern {m.btn_save()}
</button> </button>
</div> </div>

View File

@@ -3,6 +3,7 @@ import { enhance } from '$app/forms';
import TagInput from '$lib/components/TagInput.svelte'; import TagInput from '$lib/components/TagInput.svelte';
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte'; import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte'; import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte';
import { m } from '$lib/paraglide/messages.js';
let { form } = $props(); let { form } = $props();
@@ -61,9 +62,9 @@ function handleDateInput(e: Event) {
d="M10 19l-7-7m0 0l7-7m-7 7h18" d="M10 19l-7-7m0 0l7-7m-7 7h18"
/> />
</svg> </svg>
Zurück zur Übersicht {m.btn_back_to_overview()}
</a> </a>
<h1 class="font-serif text-3xl text-brand-navy">Neues Dokument</h1> <h1 class="font-serif text-3xl text-brand-navy">{m.doc_new_heading()}</h1>
</div> </div>
{#if form?.error} {#if form?.error}
@@ -73,13 +74,13 @@ function handleDateInput(e: Event) {
<form method="POST" enctype="multipart/form-data" use:enhance class="space-y-6 pb-20"> <form method="POST" enctype="multipart/form-data" use:enhance class="space-y-6 pb-20">
<!-- ── Section 1: Wer & Wann ── --> <!-- ── Section 1: Wer & Wann ── -->
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm"> <div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">Wer &amp; Wann</h2> <h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">{m.doc_section_who_when()}</h2>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2"> <div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<!-- Datum --> <!-- Datum -->
<div> <div>
<label for="documentDate" class="mb-1 block text-sm font-medium text-gray-700" <label for="documentDate" class="mb-1 block text-sm font-medium text-gray-700"
>Datum</label >{m.form_label_date()}</label
> >
<input <input
id="documentDate" id="documentDate"
@@ -87,7 +88,7 @@ function handleDateInput(e: Event) {
inputmode="numeric" inputmode="numeric"
value={dateDisplay} value={dateDisplay}
oninput={handleDateInput} oninput={handleDateInput}
placeholder="TT.MM.JJJJ" placeholder={m.form_placeholder_date()}
maxlength="10" maxlength="10"
class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm
{dateInvalid ? 'border-red-400 focus:border-red-500 focus:ring-red-500' : 'focus:border-brand-navy focus:ring-brand-navy'}" {dateInvalid ? 'border-red-400 focus:border-red-500 focus:ring-red-500' : 'focus:border-brand-navy focus:ring-brand-navy'}"
@@ -96,31 +97,31 @@ function handleDateInput(e: Event) {
<input type="hidden" name="documentDate" value={dateIso} /> <input type="hidden" name="documentDate" value={dateIso} />
{#if dateInvalid} {#if dateInvalid}
<p id="date-error" class="mt-1 text-xs text-red-600"> <p id="date-error" class="mt-1 text-xs text-red-600">
Bitte im Format TT.MM.JJJJ eingeben, z.B. 20.12.2026 {m.form_date_error()}
</p> </p>
{/if} {/if}
</div> </div>
<!-- Ort --> <!-- Ort -->
<div> <div>
<label for="location" class="mb-1 block text-sm font-medium text-gray-700">Ort</label> <label for="location" class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_location()}</label>
<input <input
id="location" id="location"
type="text" type="text"
name="location" name="location"
placeholder="z.B. Berlin, Wien…" placeholder={m.form_placeholder_location()}
class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy" class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
/> />
</div> </div>
<!-- Absender --> <!-- Absender -->
<div> <div>
<PersonTypeahead name="senderId" label="Absender" bind:value={senderId} /> <PersonTypeahead name="senderId" label={m.form_label_sender()} bind:value={senderId} />
</div> </div>
<!-- Empfänger --> <!-- Empfänger -->
<div> <div>
<p class="mb-1 block text-sm font-medium text-gray-700">Empfänger</p> <p class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_receivers()}</p>
<PersonMultiSelect bind:selectedPersons={selectedReceivers} /> <PersonMultiSelect bind:selectedPersons={selectedReceivers} />
</div> </div>
</div> </div>
@@ -128,12 +129,12 @@ function handleDateInput(e: Event) {
<!-- ── Section 2: Beschreibung ── --> <!-- ── Section 2: Beschreibung ── -->
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm"> <div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">Beschreibung</h2> <h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">{m.doc_section_description()}</h2>
<div class="space-y-5"> <div class="space-y-5">
<!-- Titel --> <!-- Titel -->
<div> <div>
<label for="title" class="mb-1 block text-sm font-medium text-gray-700">Titel *</label> <label for="title" class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_title()} *</label>
<input <input
id="title" id="title"
type="text" type="text"
@@ -146,33 +147,33 @@ function handleDateInput(e: Event) {
<!-- Aufbewahrungsort --> <!-- Aufbewahrungsort -->
<div> <div>
<label for="documentLocation" class="mb-1 block text-sm font-medium text-gray-700" <label for="documentLocation" class="mb-1 block text-sm font-medium text-gray-700"
>Aufbewahrungsort</label >{m.form_label_archive_location()}</label
> >
<input <input
id="documentLocation" id="documentLocation"
type="text" type="text"
name="documentLocation" name="documentLocation"
placeholder="z.B. Schrank 3, Mappe B" placeholder={m.form_placeholder_archive_location()}
class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy" class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
/> />
<p class="mt-1 text-xs text-gray-400">Wo befindet sich das Originaldokument?</p> <p class="mt-1 text-xs text-gray-400">{m.form_helper_archive_location()}</p>
</div> </div>
<!-- Schlagworte --> <!-- Schlagworte -->
<div> <div>
<p class="mb-1 block text-sm font-medium text-gray-700">Schlagworte</p> <p class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_tags()}</p>
<TagInput bind:tags={tags} /> <TagInput bind:tags={tags} />
<input type="hidden" name="tags" value={tags.join(',')} /> <input type="hidden" name="tags" value={tags.join(',')} />
</div> </div>
<!-- Inhalt --> <!-- Inhalt -->
<div> <div>
<label for="summary" class="mb-1 block text-sm font-medium text-gray-700">Inhalt</label> <label for="summary" class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_content()}</label>
<textarea <textarea
id="summary" id="summary"
name="summary" name="summary"
rows="5" rows="5"
placeholder="Kurze Beschreibung des Inhalts…" placeholder={m.form_placeholder_content()}
class="block w-full rounded border border-gray-300 p-2 font-serif text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy" class="block w-full rounded border border-gray-300 p-2 font-serif text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
></textarea> ></textarea>
</div> </div>
@@ -181,22 +182,22 @@ function handleDateInput(e: Event) {
<!-- ── Section 3: Transkription ── --> <!-- ── Section 3: Transkription ── -->
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm"> <div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">Transkription</h2> <h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">{m.form_label_transcription()}</h2>
<textarea <textarea
id="transcription" id="transcription"
name="transcription" name="transcription"
rows="12" rows="12"
placeholder="Vollständiger Text des Dokuments…" placeholder={m.form_placeholder_transcription()}
class="block w-full rounded border border-gray-300 p-2 font-serif text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy" class="block w-full rounded border border-gray-300 p-2 font-serif text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
></textarea> ></textarea>
</div> </div>
<!-- ── Section 4: Datei ── --> <!-- ── Section 4: Datei ── -->
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm"> <div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">Datei</h2> <h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">{m.doc_section_file()}</h2>
<label for="file-upload" class="mb-1 block text-sm font-medium text-gray-700"> <label for="file-upload" class="mb-1 block text-sm font-medium text-gray-700">
Datei hochladen <span class="font-normal text-gray-400">(optional)</span> {m.doc_file_upload_label()} <span class="font-normal text-gray-400">({m.doc_file_upload_note()})</span>
</label> </label>
<input <input
id="file-upload" id="file-upload"
@@ -216,13 +217,13 @@ function handleDateInput(e: Event) {
class="sticky bottom-0 z-10 -mx-4 flex items-center justify-between border-t border-brand-sand bg-white px-6 py-4 shadow-[0_-2px_8px_rgba(0,0,0,0.06)]" class="sticky bottom-0 z-10 -mx-4 flex items-center justify-between border-t border-brand-sand bg-white px-6 py-4 shadow-[0_-2px_8px_rgba(0,0,0,0.06)]"
> >
<a href="/" class="text-sm font-medium text-gray-500 transition-colors hover:text-brand-navy"> <a href="/" class="text-sm font-medium text-gray-500 transition-colors hover:text-brand-navy">
Abbrechen {m.btn_cancel()}
</a> </a>
<button <button
type="submit" type="submit"
class="rounded bg-brand-navy px-6 py-2 text-sm font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/80" class="rounded bg-brand-navy px-6 py-2 text-sm font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/80"
> >
Speichern {m.btn_save()}
</button> </button>
</div> </div>
</form> </form>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { m } from '$lib/paraglide/messages.js';
let { form }: { form?: { error?: string; success?: boolean } } = $props(); let { form }: { form?: { error?: string; success?: boolean } } = $props();
</script> </script>
@@ -17,17 +18,17 @@
<!-- Card --> <!-- Card -->
<div class="bg-white border border-brand-sand rounded-sm shadow-sm p-8"> <div class="bg-white border border-brand-sand rounded-sm shadow-sm p-8">
<h1 class="font-sans text-sm font-bold uppercase tracking-widest text-brand-navy mb-6">Anmelden</h1> <h1 class="font-sans text-sm font-bold uppercase tracking-widest text-brand-navy mb-6">{m.login_heading()}</h1>
<form method="POST" action="?/login" class="space-y-5"> <form method="POST" action="?/login" class="space-y-5">
<div> <div>
<label for="username" class="block text-xs font-bold font-sans uppercase tracking-widest text-gray-500 mb-1.5">Benutzername</label> <label for="username" class="block text-xs font-bold font-sans uppercase tracking-widest text-gray-500 mb-1.5">{m.login_label_username()}</label>
<input type="text" name="username" id="username" required autocomplete="username" <input type="text" name="username" id="username" required autocomplete="username"
class="block w-full border border-gray-300 py-2.5 px-3 text-sm font-serif text-brand-navy placeholder-gray-400 focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none" /> class="block w-full border border-gray-300 py-2.5 px-3 text-sm font-serif text-brand-navy placeholder-gray-400 focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none" />
</div> </div>
<div> <div>
<label for="password" class="block text-xs font-bold font-sans uppercase tracking-widest text-gray-500 mb-1.5">Passwort</label> <label for="password" class="block text-xs font-bold font-sans uppercase tracking-widest text-gray-500 mb-1.5">{m.login_label_password()}</label>
<input type="password" name="password" id="password" required autocomplete="current-password" <input type="password" name="password" id="password" required autocomplete="current-password"
class="block w-full border border-gray-300 py-2.5 px-3 text-sm font-serif text-brand-navy placeholder-gray-400 focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none" /> class="block w-full border border-gray-300 py-2.5 px-3 text-sm font-serif text-brand-navy placeholder-gray-400 focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none" />
</div> </div>
@@ -38,7 +39,7 @@
<button type="submit" <button type="submit"
class="w-full bg-brand-navy text-white py-2.5 text-xs font-bold uppercase tracking-widest font-sans hover:bg-brand-navy/90 transition-colors mt-2"> class="w-full bg-brand-navy text-white py-2.5 text-xs font-bold uppercase tracking-widest font-sans hover:bg-brand-navy/90 transition-colors mt-2">
Anmelden {m.login_btn_submit()}
</button> </button>
</form> </form>
</div> </div>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { m } from '$lib/paraglide/messages.js';
let { data } = $props(); let { data } = $props();
@@ -20,16 +21,16 @@ function handleSearch(e: Event) {
class="mb-10 flex flex-col justify-between gap-6 border-b border-brand-navy/10 pb-6 md:flex-row md:items-end" class="mb-10 flex flex-col justify-between gap-6 border-b border-brand-navy/10 pb-6 md:flex-row md:items-end"
> >
<div> <div>
<h1 class="font-serif text-3xl font-medium text-brand-navy">Personenverzeichnis</h1> <h1 class="font-serif text-3xl font-medium text-brand-navy">{m.persons_heading()}</h1>
<p class="mt-2 max-w-xl font-sans text-sm text-brand-navy/60"> <p class="mt-2 max-w-xl font-sans text-sm text-brand-navy/60">
Durchsuchen Sie den Index aller erfassten Personen im Familienarchiv. {m.persons_subtitle()}
</p> </p>
<a <a
href="/persons/new" href="/persons/new"
class="mt-3 inline-flex items-center gap-1 text-sm font-medium text-brand-navy/60 transition-colors hover:text-brand-navy" class="mt-3 inline-flex items-center gap-1 text-sm font-medium text-brand-navy/60 transition-colors hover:text-brand-navy"
> >
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Add/Add-General-MD.svg" alt="" aria-hidden="true" class="h-4 w-4" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Add/Add-General-MD.svg" alt="" aria-hidden="true" class="h-4 w-4" />
Neue Person {m.persons_btn_new()}
</a> </a>
</div> </div>
@@ -40,7 +41,7 @@ function handleSearch(e: Event) {
<input <input
id="search" id="search"
type="text" type="text"
placeholder="Namen suchen..." placeholder={m.persons_search_placeholder()}
value={data.q || ''} value={data.q || ''}
oninput={handleSearch} oninput={handleSearch}
class="block w-full rounded-sm border border-gray-300 bg-white py-2.5 pr-10 pl-4 font-sans text-sm text-brand-navy placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none" class="block w-full rounded-sm border border-gray-300 bg-white py-2.5 pr-10 pl-4 font-sans text-sm text-brand-navy placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none"
@@ -63,8 +64,8 @@ function handleSearch(e: Event) {
> >
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Account-MD.svg" alt="" aria-hidden="true" class="h-6 w-6" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Account-MD.svg" alt="" aria-hidden="true" class="h-6 w-6" />
</div> </div>
<p class="font-serif text-lg text-brand-navy">Keine Personen gefunden.</p> <p class="font-serif text-lg text-brand-navy">{m.persons_empty_heading()}</p>
<p class="mt-1 font-sans text-sm text-gray-500">Versuchen Sie einen anderen Suchbegriff.</p> <p class="mt-1 font-sans text-sm text-gray-500">{m.persons_empty_text()}</p>
</div> </div>
{:else} {:else}
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte'; import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
import { m } from '$lib/paraglide/messages.js';
let { data, form } = $props(); let { data, form } = $props();
@@ -29,7 +30,7 @@
<div class="mb-6"> <div class="mb-6">
<a href="/persons" class="inline-flex items-center text-xs font-bold uppercase tracking-widest text-gray-500 hover:text-brand-navy transition-colors group"> <a href="/persons" class="inline-flex items-center text-xs font-bold uppercase tracking-widest text-gray-500 hover:text-brand-navy transition-colors group">
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" /> <img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" />
Zurück zur Übersicht {m.btn_back_to_overview()}
</a> </a>
</div> </div>
@@ -42,7 +43,7 @@
<!-- Edit Form --> <!-- Edit Form -->
<form method="POST" action="?/update" use:enhance> <form method="POST" action="?/update" use:enhance>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<h2 class="text-xl font-serif text-brand-navy border-b border-gray-100 pb-3">Person bearbeiten</h2> <h2 class="text-xl font-serif text-brand-navy border-b border-gray-100 pb-3">{m.person_edit_heading()}</h2>
{#if form?.updateError} {#if form?.updateError}
<p class="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">{form.updateError}</p> <p class="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">{form.updateError}</p>
@@ -50,7 +51,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label for="firstName" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">Vorname *</label> <label for="firstName" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">{m.form_label_first_name()} *</label>
<input <input
id="firstName" id="firstName"
name="firstName" name="firstName"
@@ -61,7 +62,7 @@
/> />
</div> </div>
<div> <div>
<label for="lastName" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">Nachname *</label> <label for="lastName" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">{m.form_label_last_name()} *</label>
<input <input
id="lastName" id="lastName"
name="lastName" name="lastName"
@@ -72,7 +73,7 @@
/> />
</div> </div>
<div class="md:col-span-2"> <div class="md:col-span-2">
<label for="alias" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">Rufname / Alias</label> <label for="alias" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">{m.form_label_alias()}</label>
<input <input
id="alias" id="alias"
name="alias" name="alias"
@@ -85,10 +86,10 @@
<div class="flex gap-3"> <div class="flex gap-3">
<button type="submit" class="px-5 py-2 bg-brand-navy text-white text-sm font-bold uppercase tracking-widest rounded hover:bg-brand-navy/80 transition-colors"> <button type="submit" class="px-5 py-2 bg-brand-navy text-white text-sm font-bold uppercase tracking-widest rounded hover:bg-brand-navy/80 transition-colors">
Speichern {m.btn_save()}
</button> </button>
<button type="button" onclick={() => (editMode = false)} class="px-5 py-2 border border-gray-300 text-gray-600 text-sm font-bold uppercase tracking-widest rounded hover:bg-gray-50 transition-colors"> <button type="button" onclick={() => (editMode = false)} class="px-5 py-2 border border-gray-300 text-gray-600 text-sm font-bold uppercase tracking-widest rounded hover:bg-gray-50 transition-colors">
Abbrechen {m.btn_cancel()}
</button> </button>
</div> </div>
</div> </div>
@@ -109,19 +110,19 @@
</h1> </h1>
<button onclick={() => (editMode = true)} class="ml-4 flex-shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:border-brand-navy hover:text-brand-navy transition-colors"> <button onclick={() => (editMode = true)} class="ml-4 flex-shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:border-brand-navy hover:text-brand-navy transition-colors">
<img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Edit-Content-SM.svg" alt="" aria-hidden="true" class="w-3.5 h-3.5" /> <img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Edit-Content-SM.svg" alt="" aria-hidden="true" class="w-3.5 h-3.5" />
Bearbeiten {m.btn_edit()}
</button> </button>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div> <div>
<span class="block text-xs font-bold font-sans text-gray-400 uppercase tracking-widest mb-1">Voller Name</span> <span class="block text-xs font-bold font-sans text-gray-400 uppercase tracking-widest mb-1">{m.person_label_full_name()}</span>
<span class="block text-lg font-serif text-brand-navy">{person.firstName} {person.lastName}</span> <span class="block text-lg font-serif text-brand-navy">{person.firstName} {person.lastName}</span>
</div> </div>
{#if person.alias} {#if person.alias}
<div> <div>
<span class="block text-xs font-bold font-sans text-gray-400 uppercase tracking-widest mb-1">Rufname / Alias</span> <span class="block text-xs font-bold font-sans text-gray-400 uppercase tracking-widest mb-1">{m.form_label_alias()}</span>
<span class="block text-lg font-serif text-brand-navy italic">"{person.alias}"</span> <span class="block text-lg font-serif text-brand-navy italic">"{person.alias}"</span>
</div> </div>
{/if} {/if}
@@ -136,9 +137,9 @@
{#key person.id} {#key person.id}
<div class="bg-white shadow-sm border border-brand-sand rounded-sm overflow-hidden mb-10"> <div class="bg-white shadow-sm border border-brand-sand rounded-sm overflow-hidden mb-10">
<div class="p-6 md:p-8"> <div class="p-6 md:p-8">
<h2 class="text-lg font-serif text-brand-navy mb-1">Person zusammenführen</h2> <h2 class="text-lg font-serif text-brand-navy mb-1">{m.person_merge_heading()}</h2>
<p class="text-sm text-gray-500 font-sans mb-5"> <p class="text-sm text-gray-500 font-sans mb-5">
Diese Person wird in die gewählte Zielperson überführt. Alle Dokumente und Verknüpfungen werden übertragen, danach wird diese Person gelöscht. {m.person_merge_description()}
</p> </p>
{#if form?.mergeError} {#if form?.mergeError}
@@ -152,7 +153,7 @@
<div class="flex-1"> <div class="flex-1">
<PersonTypeahead <PersonTypeahead
name="_targetPersonDisplay" name="_targetPersonDisplay"
label="Zusammenführen mit" label={m.person_merge_target_label()}
value={mergeTargetId} value={mergeTargetId}
onchange={(value) => { mergeTargetId = value; showMergeConfirm = false; }} onchange={(value) => { mergeTargetId = value; showMergeConfirm = false; }}
/> />
@@ -165,7 +166,7 @@
onclick={() => (showMergeConfirm = true)} onclick={() => (showMergeConfirm = true)}
class="px-4 py-2 text-sm font-bold uppercase tracking-widest border border-red-300 text-red-600 rounded hover:bg-red-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed" class="px-4 py-2 text-sm font-bold uppercase tracking-widest border border-red-300 text-red-600 rounded hover:bg-red-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
> >
Zusammenführen {m.person_btn_merge()}
</button> </button>
{:else} {:else}
<div class="flex gap-2"> <div class="flex gap-2">
@@ -173,14 +174,14 @@
type="submit" type="submit"
class="px-4 py-2 text-sm font-bold uppercase tracking-widest bg-red-600 text-white rounded hover:bg-red-700 transition-colors" class="px-4 py-2 text-sm font-bold uppercase tracking-widest bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
> >
Ja, zusammenführen {m.person_btn_merge_confirm()}
</button> </button>
<button <button
type="button" type="button"
onclick={() => (showMergeConfirm = false)} onclick={() => (showMergeConfirm = false)}
class="px-4 py-2 text-sm font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:bg-gray-50 transition-colors" class="px-4 py-2 text-sm font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:bg-gray-50 transition-colors"
> >
Abbrechen {m.btn_cancel()}
</button> </button>
</div> </div>
{/if} {/if}
@@ -188,7 +189,7 @@
{#if showMergeConfirm} {#if showMergeConfirm}
<p class="mt-3 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2"> <p class="mt-3 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2">
Achtung: Diese Aktion ist nicht rückgängig zu machen. <strong>{person.firstName} {person.lastName}</strong> wird gelöscht. {m.person_merge_warning()} <strong>{person.firstName} {person.lastName}</strong> wird gelöscht.
</p> </p>
{/if} {/if}
</form> </form>
@@ -199,7 +200,7 @@
<!-- Linked Documents Section --> <!-- Linked Documents Section -->
<div> <div>
<div class="flex items-center justify-between mb-6 border-b border-brand-navy/10 pb-2"> <div class="flex items-center justify-between mb-6 border-b border-brand-navy/10 pb-2">
<h2 class="text-xl font-serif text-brand-navy">Gesendete Dokumente</h2> <h2 class="text-xl font-serif text-brand-navy">{m.person_docs_heading()}</h2>
<span class="bg-brand-navy text-white text-xs font-bold px-2 py-1 rounded-full"> <span class="bg-brand-navy text-white text-xs font-bold px-2 py-1 rounded-full">
{documents.length} {documents.length}
</span> </span>
@@ -207,7 +208,7 @@
{#if documents.length === 0} {#if documents.length === 0}
<div class="p-12 text-center bg-white border border-brand-sand border-dashed rounded-sm"> <div class="p-12 text-center bg-white border border-brand-sand border-dashed rounded-sm">
<p class="text-gray-500 font-sans">Diese Person ist noch nicht als Absender verknüpft.</p> <p class="text-gray-500 font-sans">{m.person_no_docs()}</p>
</div> </div>
{:else} {:else}
<ul class="space-y-3"> <ul class="space-y-3">

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { m } from '$lib/paraglide/messages.js';
let { form } = $props(); let { form } = $props();
</script> </script>
@@ -22,9 +23,9 @@ let { form } = $props();
d="M10 19l-7-7m0 0l7-7m-7 7h18" d="M10 19l-7-7m0 0l7-7m-7 7h18"
/> />
</svg> </svg>
Zurück zur Übersicht {m.btn_back_to_overview()}
</a> </a>
<h1 class="font-serif text-3xl text-brand-navy">Neue Person</h1> <h1 class="font-serif text-3xl text-brand-navy">{m.persons_new_heading()}</h1>
</div> </div>
{#if form?.error} {#if form?.error}
@@ -34,13 +35,13 @@ let { form } = $props();
<form method="POST"> <form method="POST">
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm"> <div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase"> <h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">
Angaben zur Person {m.persons_section_details()}
</h2> </h2>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2"> <div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<div> <div>
<label for="firstName" class="mb-1 block text-sm font-medium text-gray-700" <label for="firstName" class="mb-1 block text-sm font-medium text-gray-700"
>Vorname *</label >{m.form_label_first_name()} *</label
> >
<input <input
id="firstName" id="firstName"
@@ -53,7 +54,7 @@ let { form } = $props();
<div> <div>
<label for="lastName" class="mb-1 block text-sm font-medium text-gray-700" <label for="lastName" class="mb-1 block text-sm font-medium text-gray-700"
>Nachname *</label >{m.form_label_last_name()} *</label
> >
<input <input
id="lastName" id="lastName"
@@ -66,13 +67,13 @@ let { form } = $props();
<div class="md:col-span-2"> <div class="md:col-span-2">
<label for="alias" class="mb-1 block text-sm font-medium text-gray-700" <label for="alias" class="mb-1 block text-sm font-medium text-gray-700"
>Rufname / Alias</label >{m.form_label_alias()}</label
> >
<input <input
id="alias" id="alias"
name="alias" name="alias"
type="text" type="text"
placeholder="z.B. Oma Frieda, Onkel Karl…" placeholder={m.form_placeholder_alias()}
class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy" class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
/> />
</div> </div>
@@ -87,13 +88,13 @@ let { form } = $props();
href="/persons" href="/persons"
class="text-sm font-medium text-gray-500 transition-colors hover:text-brand-navy" class="text-sm font-medium text-gray-500 transition-colors hover:text-brand-navy"
> >
Abbrechen {m.btn_cancel()}
</a> </a>
<button <button
type="submit" type="submit"
class="rounded bg-brand-navy px-6 py-2 text-sm font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/80" class="rounded bg-brand-navy px-6 py-2 text-sm font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/80"
> >
Erstellen {m.btn_create()}
</button> </button>
</div> </div>
</form> </form>