feat: Introduce dedicated settings page with API URL configuration and data management hooks, and update Dockerfile for runtime env injection.
This commit is contained in:
@@ -19,9 +19,15 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|||||||
|
|
||||||
# Create script to generate env-config.js from environment variables
|
# Create script to generate env-config.js from environment variables
|
||||||
RUN echo '#!/bin/sh' > /docker-entrypoint.d/40-env-config.sh && \
|
RUN echo '#!/bin/sh' > /docker-entrypoint.d/40-env-config.sh && \
|
||||||
echo 'echo "window.env = {" > /usr/share/nginx/html/env-config.js' >> /docker-entrypoint.d/40-env-config.sh && \
|
echo 'cat > /usr/share/nginx/html/env-config.js << EOF' >> /docker-entrypoint.d/40-env-config.sh && \
|
||||||
echo 'if [ -n "$API_URL" ]; then echo " API_URL: \"$API_URL\"," >> /usr/share/nginx/html/env-config.js; fi' >> /docker-entrypoint.d/40-env-config.sh && \
|
echo '// Runtime environment configuration' >> /docker-entrypoint.d/40-env-config.sh && \
|
||||||
echo 'echo "};" >> /usr/share/nginx/html/env-config.js' >> /docker-entrypoint.d/40-env-config.sh && \
|
echo '// Generated at container startup' >> /docker-entrypoint.d/40-env-config.sh && \
|
||||||
|
echo 'window.env = {' >> /docker-entrypoint.d/40-env-config.sh && \
|
||||||
|
echo 'EOF' >> /docker-entrypoint.d/40-env-config.sh && \
|
||||||
|
echo 'if [ -n "$API_URL" ]; then' >> /docker-entrypoint.d/40-env-config.sh && \
|
||||||
|
echo ' echo " API_URL: \"$API_URL\"," >> /usr/share/nginx/html/env-config.js' >> /docker-entrypoint.d/40-env-config.sh && \
|
||||||
|
echo 'fi' >> /docker-entrypoint.d/40-env-config.sh && \
|
||||||
|
echo 'echo "};\" >> /usr/share/nginx/html/env-config.js' >> /docker-entrypoint.d/40-env-config.sh && \
|
||||||
chmod +x /docker-entrypoint.d/40-env-config.sh
|
chmod +x /docker-entrypoint.d/40-env-config.sh
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||||
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
|
||||||
<script src="/env-config.js"></script>
|
<script src="/env-config.js" onerror="window.env = {}"></script>
|
||||||
<title>K-Notes</title>
|
<title>K-Notes</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
5
k-notes-frontend/public/env-config.js
Normal file
5
k-notes-frontend/public/env-config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Default environment configuration for development
|
||||||
|
// This file is replaced at Docker container startup with runtime values
|
||||||
|
window.env = {
|
||||||
|
// API_URL will be injected by Docker, or can be overridden via localStorage
|
||||||
|
};
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
"{{count}} selected_one": "{{count}} ausgewählt",
|
"{{count}} selected_one": "{{count}} ausgewählt",
|
||||||
"{{count}} selected_other": "{{count}} ausgewählt",
|
"{{count}} selected_other": "{{count}} ausgewählt",
|
||||||
"Add a new note to your collection.": "Füge eine neue Notiz zu deiner Sammlung hinzu.",
|
"Add a new note to your collection.": "Füge eine neue Notiz zu deiner Sammlung hinzu.",
|
||||||
|
"API Configuration": "API-Konfiguration",
|
||||||
|
"API URL reset to default. Please reload the page.": "API-URL auf Standard zurückgesetzt. Bitte Seite neu laden.",
|
||||||
|
"API URL updated successfully. Please reload the page.": "API-URL erfolgreich aktualisiert. Bitte Seite neu laden.",
|
||||||
"Archive": "Archiv",
|
"Archive": "Archiv",
|
||||||
"Archived {{count}} note_one": "{{count}} Notiz archiviert",
|
"Archived {{count}} note_one": "{{count}} Notiz archiviert",
|
||||||
"Archived {{count}} note_other": "{{count}} Notizen archiviert",
|
"Archived {{count}} note_other": "{{count}} Notizen archiviert",
|
||||||
@@ -9,11 +12,15 @@
|
|||||||
"Are you sure you want to delete {{count}} note?_other": "Möchtest du wirklich {{count}} Notizen löschen?",
|
"Are you sure you want to delete {{count}} note?_other": "Möchtest du wirklich {{count}} Notizen löschen?",
|
||||||
"Are you sure?": "Bist du sicher?",
|
"Are you sure?": "Bist du sicher?",
|
||||||
"Backend URL": "Backend-URL",
|
"Backend URL": "Backend-URL",
|
||||||
|
"Choose your preferred language": "Wähle deine bevorzugte Sprache",
|
||||||
"Color": "Farbe",
|
"Color": "Farbe",
|
||||||
"Configure the application settings.": "Konfiguriere die Anwendungseinstellungen.",
|
"Configure the application settings.": "Konfiguriere die Anwendungseinstellungen.",
|
||||||
|
"Configure the backend API URL for this application": "Konfiguriere die Backend-API-URL für diese Anwendung",
|
||||||
"Content": "Inhalt",
|
"Content": "Inhalt",
|
||||||
"Create": "Erstellen",
|
"Create": "Erstellen",
|
||||||
"Create Note": "Notiz erstellen",
|
"Create Note": "Notiz erstellen",
|
||||||
|
"Current API URL": "Aktuelle API-URL",
|
||||||
|
"Custom API URL": "Benutzerdefinierte API-URL",
|
||||||
"Data Management": "Datenverwaltung",
|
"Data Management": "Datenverwaltung",
|
||||||
"Delete": "Löschen",
|
"Delete": "Löschen",
|
||||||
"Delete tag \"{{name}}\"? Notes will keep their content.": "Tag \"{{name}}\" löschen? Notizen behalten ihren Inhalt.",
|
"Delete tag \"{{name}}\"? Notes will keep their content.": "Tag \"{{name}}\" löschen? Notizen behalten ihren Inhalt.",
|
||||||
@@ -29,9 +36,10 @@
|
|||||||
"Import Data": "Daten importieren",
|
"Import Data": "Daten importieren",
|
||||||
"Import failed": "Import fehlgeschlagen",
|
"Import failed": "Import fehlgeschlagen",
|
||||||
"Import successful. Reloading...": "Import erfolgreich. Wird neu geladen...",
|
"Import successful. Reloading...": "Import erfolgreich. Wird neu geladen...",
|
||||||
"Invalid URL": "Ungültige URL",
|
"Invalid URL format. Please enter a valid URL.": "Ungültiges URL-Format. Bitte gib eine gültige URL ein.",
|
||||||
"K-Notes": "K-Notes",
|
"K-Notes": "K-Notes",
|
||||||
"Language": "Sprache",
|
"Language": "Sprache",
|
||||||
|
"Leave empty to use the default or Docker-injected URL": "Leer lassen, um die Standard- oder Docker-injizierte URL zu verwenden",
|
||||||
"List View": "Listenansicht",
|
"List View": "Listenansicht",
|
||||||
"New Note": "Neue Notiz",
|
"New Note": "Neue Notiz",
|
||||||
"No archived notes yet": "Noch keine archivierten Notizen",
|
"No archived notes yet": "Noch keine archivierten Notizen",
|
||||||
@@ -42,16 +50,24 @@
|
|||||||
"Note created": "Notiz erstellt",
|
"Note created": "Notiz erstellt",
|
||||||
"Note title": "Notiztitel",
|
"Note title": "Notiztitel",
|
||||||
"Note updated": "Notiz aktualisiert",
|
"Note updated": "Notiz aktualisiert",
|
||||||
"Notes": "Notizen",
|
|
||||||
"Others": "Andere",
|
"Others": "Andere",
|
||||||
"Pin this note": "Diese Notiz anheften",
|
"Pin this note": "Diese Notiz anheften",
|
||||||
"Pinned": "Angeheftet",
|
"Pinned": "Angeheftet",
|
||||||
|
"Please enter a URL": "Bitte gib eine URL ein",
|
||||||
|
"Reload": "Neu laden",
|
||||||
"Rename": "Umbenennen",
|
"Rename": "Umbenennen",
|
||||||
|
"Reset to Default": "Auf Standard zurücksetzen",
|
||||||
|
"Save": "Speichern",
|
||||||
"Save changes": "Änderungen speichern",
|
"Save changes": "Änderungen speichern",
|
||||||
"Saving...": "Speichern...",
|
"Saving": {
|
||||||
|
"": {
|
||||||
|
"": {
|
||||||
|
"": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Search your notes...": "Durchsuche deine Notizen...",
|
"Search your notes...": "Durchsuche deine Notizen...",
|
||||||
"Settings": "Einstellungen",
|
"Settings": "Einstellungen",
|
||||||
"Settings saved. Please refresh the page.": "Einstellungen gespeichert. Bitte aktualisiere die Seite.",
|
|
||||||
"Tag deleted": "Tag gelöscht",
|
"Tag deleted": "Tag gelöscht",
|
||||||
"Tag renamed": "Tag umbenannt",
|
"Tag renamed": "Tag umbenannt",
|
||||||
"Tags": "Tags",
|
"Tags": "Tags",
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
"{{count}} selected_one": "{{count}} selected",
|
"{{count}} selected_one": "{{count}} selected",
|
||||||
"{{count}} selected_other": "{{count}} selected",
|
"{{count}} selected_other": "{{count}} selected",
|
||||||
"Add a new note to your collection.": "Add a new note to your collection.",
|
"Add a new note to your collection.": "Add a new note to your collection.",
|
||||||
|
"API Configuration": "API Configuration",
|
||||||
|
"API URL reset to default. Please reload the page.": "API URL reset to default. Please reload the page.",
|
||||||
|
"API URL updated successfully. Please reload the page.": "API URL updated successfully. Please reload the page.",
|
||||||
"Archive": "Archive",
|
"Archive": "Archive",
|
||||||
"Archived {{count}} note_one": "Archived {{count}} notes",
|
"Archived {{count}} note_one": "Archived {{count}} notes",
|
||||||
"Archived {{count}} note_other": "Archived {{count}} notes",
|
"Archived {{count}} note_other": "Archived {{count}} notes",
|
||||||
@@ -9,11 +12,15 @@
|
|||||||
"Are you sure you want to delete {{count}} note?_other": "Are you sure you want to delete {{count}} notes?",
|
"Are you sure you want to delete {{count}} note?_other": "Are you sure you want to delete {{count}} notes?",
|
||||||
"Are you sure?": "Are you sure?",
|
"Are you sure?": "Are you sure?",
|
||||||
"Backend URL": "Backend URL",
|
"Backend URL": "Backend URL",
|
||||||
|
"Choose your preferred language": "Choose your preferred language",
|
||||||
"Color": "Color",
|
"Color": "Color",
|
||||||
"Configure the application settings.": "Configure the application settings.",
|
"Configure the application settings.": "Configure the application settings.",
|
||||||
|
"Configure the backend API URL for this application": "Configure the backend API URL for this application",
|
||||||
"Content": "Content",
|
"Content": "Content",
|
||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
"Create Note": "Create Note",
|
"Create Note": "Create Note",
|
||||||
|
"Current API URL": "Current API URL",
|
||||||
|
"Custom API URL": "Custom API URL",
|
||||||
"Data Management": "Data Management",
|
"Data Management": "Data Management",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
"Delete tag \"{{name}}\"? Notes will keep their content.": "Delete tag \"{{name}}\"? Notes will keep their content.",
|
"Delete tag \"{{name}}\"? Notes will keep their content.": "Delete tag \"{{name}}\"? Notes will keep their content.",
|
||||||
@@ -29,9 +36,10 @@
|
|||||||
"Import Data": "Import Data",
|
"Import Data": "Import Data",
|
||||||
"Import failed": "Import failed",
|
"Import failed": "Import failed",
|
||||||
"Import successful. Reloading...": "Import successful. Reloading...",
|
"Import successful. Reloading...": "Import successful. Reloading...",
|
||||||
"Invalid URL": "Invalid URL",
|
"Invalid URL format. Please enter a valid URL.": "Invalid URL format. Please enter a valid URL.",
|
||||||
"K-Notes": "K-Notes",
|
"K-Notes": "K-Notes",
|
||||||
"Language": "Language",
|
"Language": "Language",
|
||||||
|
"Leave empty to use the default or Docker-injected URL": "Leave empty to use the default or Docker-injected URL",
|
||||||
"List View": "List View",
|
"List View": "List View",
|
||||||
"New Note": "New Note",
|
"New Note": "New Note",
|
||||||
"No archived notes yet": "No archived notes yet",
|
"No archived notes yet": "No archived notes yet",
|
||||||
@@ -45,7 +53,11 @@
|
|||||||
"Others": "Others",
|
"Others": "Others",
|
||||||
"Pin this note": "Pin this note",
|
"Pin this note": "Pin this note",
|
||||||
"Pinned": "Pinned",
|
"Pinned": "Pinned",
|
||||||
|
"Please enter a URL": "Please enter a URL",
|
||||||
|
"Reload": "Reload",
|
||||||
"Rename": "Rename",
|
"Rename": "Rename",
|
||||||
|
"Reset to Default": "Reset to Default",
|
||||||
|
"Save": "Save",
|
||||||
"Save changes": "Save changes",
|
"Save changes": "Save changes",
|
||||||
"Saving": {
|
"Saving": {
|
||||||
"": {
|
"": {
|
||||||
@@ -56,7 +68,6 @@
|
|||||||
},
|
},
|
||||||
"Search your notes...": "Search your notes...",
|
"Search your notes...": "Search your notes...",
|
||||||
"Settings": "Settings",
|
"Settings": "Settings",
|
||||||
"Settings saved. Please refresh the page.": "Settings saved. Please refresh the page.",
|
|
||||||
"Tag deleted": "Tag deleted",
|
"Tag deleted": "Tag deleted",
|
||||||
"Tag renamed": "Tag renamed",
|
"Tag renamed": "Tag renamed",
|
||||||
"Tags": "Tags",
|
"Tags": "Tags",
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
{
|
{
|
||||||
"{{count}} selected_one": "{{count}} seleccionado",
|
"{{count}} selected_one": "{{count}} seleccionado",
|
||||||
|
"{{count}} selected_many": "",
|
||||||
"{{count}} selected_other": "{{count}} seleccionados",
|
"{{count}} selected_other": "{{count}} seleccionados",
|
||||||
"Add a new note to your collection.": "Añade una nueva nota a tu colección.",
|
"Add a new note to your collection.": "Añade una nueva nota a tu colección.",
|
||||||
|
"API Configuration": "Configuración de API",
|
||||||
|
"API URL reset to default. Please reload the page.": "URL de API restablecido a predeterminado. Recarga la página.",
|
||||||
|
"API URL updated successfully. Please reload the page.": "URL de API actualizado correctamente. Recarga la página.",
|
||||||
"Archive": "Archivar",
|
"Archive": "Archivar",
|
||||||
"Archived {{count}} note_one": "{{count}} nota archivada",
|
"Archived {{count}} note_one": "{{count}} nota archivada",
|
||||||
|
"Archived {{count}} note_many": "",
|
||||||
"Archived {{count}} note_other": "{{count}} notas archivadas",
|
"Archived {{count}} note_other": "{{count}} notas archivadas",
|
||||||
"Are you sure you want to delete {{count}} note?_one": "¿Estás seguro de que quieres eliminar {{count}} nota?",
|
"Are you sure you want to delete {{count}} note?_one": "¿Estás seguro de que quieres eliminar {{count}} nota?",
|
||||||
|
"Are you sure you want to delete {{count}} note?_many": "",
|
||||||
"Are you sure you want to delete {{count}} note?_other": "¿Estás seguro de que quieres eliminar {{count}} notas?",
|
"Are you sure you want to delete {{count}} note?_other": "¿Estás seguro de que quieres eliminar {{count}} notas?",
|
||||||
"Are you sure?": "¿Estás seguro?",
|
"Are you sure?": "¿Estás seguro?",
|
||||||
"Backend URL": "URL del backend",
|
"Backend URL": "URL del backend",
|
||||||
|
"Choose your preferred language": "Elige tu idioma preferido",
|
||||||
"Color": "Color",
|
"Color": "Color",
|
||||||
"Configure the application settings.": "Configura los ajustes de la aplicación.",
|
"Configure the application settings.": "Configura los ajustes de la aplicación.",
|
||||||
|
"Configure the backend API URL for this application": "Configura la URL de API del backend para esta aplicación",
|
||||||
"Content": "Contenido",
|
"Content": "Contenido",
|
||||||
"Create": "Crear",
|
"Create": "Crear",
|
||||||
"Create Note": "Crear nota",
|
"Create Note": "Crear nota",
|
||||||
|
"Current API URL": "URL de API actual",
|
||||||
|
"Custom API URL": "URL de API personalizado",
|
||||||
"Data Management": "Gestión de datos",
|
"Data Management": "Gestión de datos",
|
||||||
"Delete": "Eliminar",
|
"Delete": "Eliminar",
|
||||||
"Delete tag \"{{name}}\"? Notes will keep their content.": "¿Eliminar etiqueta \"{{name}}\"? Las notas conservarán su contenido.",
|
"Delete tag \"{{name}}\"? Notes will keep their content.": "¿Eliminar etiqueta \"{{name}}\"? Las notas conservarán su contenido.",
|
||||||
"Deleted {{count}} note_one": "{{count}} nota eliminada",
|
"Deleted {{count}} note_one": "{{count}} nota eliminada",
|
||||||
|
"Deleted {{count}} note_many": "",
|
||||||
"Deleted {{count}} note_other": "{{count}} notas eliminadas",
|
"Deleted {{count}} note_other": "{{count}} notas eliminadas",
|
||||||
"Edit Note": "Editar nota",
|
"Edit Note": "Editar nota",
|
||||||
"Export Data": "Exportar datos",
|
"Export Data": "Exportar datos",
|
||||||
@@ -29,9 +40,10 @@
|
|||||||
"Import Data": "Importar datos",
|
"Import Data": "Importar datos",
|
||||||
"Import failed": "Importación fallida",
|
"Import failed": "Importación fallida",
|
||||||
"Import successful. Reloading...": "Importación exitosa. Recargando...",
|
"Import successful. Reloading...": "Importación exitosa. Recargando...",
|
||||||
"Invalid URL": "URL inválida",
|
"Invalid URL format. Please enter a valid URL.": "Formato de URL inválido. Por favor, introduce una URL válida.",
|
||||||
"K-Notes": "K-Notes",
|
"K-Notes": "K-Notes",
|
||||||
"Language": "Idioma",
|
"Language": "Idioma",
|
||||||
|
"Leave empty to use the default or Docker-injected URL": "Dejar vacío para usar la URL predeterminada o inyectada por Docker",
|
||||||
"List View": "Vista de lista",
|
"List View": "Vista de lista",
|
||||||
"New Note": "Nueva nota",
|
"New Note": "Nueva nota",
|
||||||
"No archived notes yet": "Aún no hay notas archivadas",
|
"No archived notes yet": "Aún no hay notas archivadas",
|
||||||
@@ -42,16 +54,24 @@
|
|||||||
"Note created": "Nota creada",
|
"Note created": "Nota creada",
|
||||||
"Note title": "Título de la nota",
|
"Note title": "Título de la nota",
|
||||||
"Note updated": "Nota actualizada",
|
"Note updated": "Nota actualizada",
|
||||||
"Notes": "Notas",
|
|
||||||
"Others": "Otros",
|
"Others": "Otros",
|
||||||
"Pin this note": "Fijar esta nota",
|
"Pin this note": "Fijar esta nota",
|
||||||
"Pinned": "Fijadas",
|
"Pinned": "Fijadas",
|
||||||
|
"Please enter a URL": "Por favor, introduce una URL",
|
||||||
|
"Reload": "Recargar",
|
||||||
"Rename": "Renombrar",
|
"Rename": "Renombrar",
|
||||||
|
"Reset to Default": "Restablecer a predeterminado",
|
||||||
|
"Save": "Guardar",
|
||||||
"Save changes": "Guardar cambios",
|
"Save changes": "Guardar cambios",
|
||||||
"Saving...": "Guardando...",
|
"Saving": {
|
||||||
|
"": {
|
||||||
|
"": {
|
||||||
|
"": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Search your notes...": "Busca tus notas...",
|
"Search your notes...": "Busca tus notas...",
|
||||||
"Settings": "Configuración",
|
"Settings": "Configuración",
|
||||||
"Settings saved. Please refresh the page.": "Configuración guardada. Por favor, actualiza la página.",
|
|
||||||
"Tag deleted": "Etiqueta eliminada",
|
"Tag deleted": "Etiqueta eliminada",
|
||||||
"Tag renamed": "Etiqueta renombrada",
|
"Tag renamed": "Etiqueta renombrada",
|
||||||
"Tags": "Etiquetas",
|
"Tags": "Etiquetas",
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
{
|
{
|
||||||
"{{count}} selected_one": "{{count}} sélectionné",
|
"{{count}} selected_one": "{{count}} sélectionné",
|
||||||
|
"{{count}} selected_many": "",
|
||||||
"{{count}} selected_other": "{{count}} sélectionnés",
|
"{{count}} selected_other": "{{count}} sélectionnés",
|
||||||
"Add a new note to your collection.": "Ajoute une nouvelle note à ta collection.",
|
"Add a new note to your collection.": "Ajoute une nouvelle note à ta collection.",
|
||||||
|
"API Configuration": "Configuration de l'API",
|
||||||
|
"API URL reset to default. Please reload the page.": "URL de l'API réinitialisée par défaut. Veuillez recharger la page.",
|
||||||
|
"API URL updated successfully. Please reload the page.": "URL de l'API mise à jour avec succès. Veuillez recharger la page.",
|
||||||
"Archive": "Archive",
|
"Archive": "Archive",
|
||||||
"Archived {{count}} note_one": "{{count}} note archivée",
|
"Archived {{count}} note_one": "{{count}} note archivée",
|
||||||
|
"Archived {{count}} note_many": "",
|
||||||
"Archived {{count}} note_other": "{{count}} notes archivées",
|
"Archived {{count}} note_other": "{{count}} notes archivées",
|
||||||
"Are you sure you want to delete {{count}} note?_one": "Es-tu sûr de vouloir supprimer {{count}} note ?",
|
"Are you sure you want to delete {{count}} note?_one": "Es-tu sûr de vouloir supprimer {{count}} note ?",
|
||||||
|
"Are you sure you want to delete {{count}} note?_many": "",
|
||||||
"Are you sure you want to delete {{count}} note?_other": "Es-tu sûr de vouloir supprimer {{count}} notes ?",
|
"Are you sure you want to delete {{count}} note?_other": "Es-tu sûr de vouloir supprimer {{count}} notes ?",
|
||||||
"Are you sure?": "Es-tu sûr ?",
|
"Are you sure?": "Es-tu sûr ?",
|
||||||
"Backend URL": "URL du backend",
|
"Backend URL": "URL du backend",
|
||||||
|
"Choose your preferred language": "Choisis ta langue préférée",
|
||||||
"Color": "Couleur",
|
"Color": "Couleur",
|
||||||
"Configure the application settings.": "Configure les paramètres de l'application.",
|
"Configure the application settings.": "Configure les paramètres de l'application.",
|
||||||
|
"Configure the backend API URL for this application": "Configure l'URL de l'API backend pour cette application",
|
||||||
"Content": "Contenu",
|
"Content": "Contenu",
|
||||||
"Create": "Créer",
|
"Create": "Créer",
|
||||||
"Create Note": "Créer une note",
|
"Create Note": "Créer une note",
|
||||||
|
"Current API URL": "URL de l'API actuelle",
|
||||||
|
"Custom API URL": "URL de l'API personnalisée",
|
||||||
"Data Management": "Gestion des données",
|
"Data Management": "Gestion des données",
|
||||||
"Delete": "Supprimer",
|
"Delete": "Supprimer",
|
||||||
"Delete tag \"{{name}}\"? Notes will keep their content.": "Supprimer l'étiquette \"{{name}}\" ? Les notes conserveront leur contenu.",
|
"Delete tag \"{{name}}\"? Notes will keep their content.": "Supprimer l'étiquette \"{{name}}\" ? Les notes conserveront leur contenu.",
|
||||||
"Deleted {{count}} note_one": "{{count}} note supprimée",
|
"Deleted {{count}} note_one": "{{count}} note supprimée",
|
||||||
|
"Deleted {{count}} note_many": "",
|
||||||
"Deleted {{count}} note_other": "{{count}} notes supprimées",
|
"Deleted {{count}} note_other": "{{count}} notes supprimées",
|
||||||
"Edit Note": "Modifier la note",
|
"Edit Note": "Modifier la note",
|
||||||
"Export Data": "Exporter les données",
|
"Export Data": "Exporter les données",
|
||||||
@@ -29,9 +40,10 @@
|
|||||||
"Import Data": "Importer les données",
|
"Import Data": "Importer les données",
|
||||||
"Import failed": "Échec de l'importation",
|
"Import failed": "Échec de l'importation",
|
||||||
"Import successful. Reloading...": "Importation réussie. Rechargement...",
|
"Import successful. Reloading...": "Importation réussie. Rechargement...",
|
||||||
"Invalid URL": "URL invalide",
|
"Invalid URL format. Please enter a valid URL.": "Format d'URL invalide. Veuillez entrer une URL valide.",
|
||||||
"K-Notes": "K-Notes",
|
"K-Notes": "K-Notes",
|
||||||
"Language": "Langue",
|
"Language": "Langue",
|
||||||
|
"Leave empty to use the default or Docker-injected URL": "Laisser vide pour utiliser l'URL par défaut ou injectée par Docker",
|
||||||
"List View": "Vue en liste",
|
"List View": "Vue en liste",
|
||||||
"New Note": "Nouvelle note",
|
"New Note": "Nouvelle note",
|
||||||
"No archived notes yet": "Pas encore de notes archivées",
|
"No archived notes yet": "Pas encore de notes archivées",
|
||||||
@@ -42,16 +54,24 @@
|
|||||||
"Note created": "Note créée",
|
"Note created": "Note créée",
|
||||||
"Note title": "Titre de la note",
|
"Note title": "Titre de la note",
|
||||||
"Note updated": "Note mise à jour",
|
"Note updated": "Note mise à jour",
|
||||||
"Notes": "Notes",
|
|
||||||
"Others": "Autres",
|
"Others": "Autres",
|
||||||
"Pin this note": "Épingler cette note",
|
"Pin this note": "Épingler cette note",
|
||||||
"Pinned": "Épinglées",
|
"Pinned": "Épinglées",
|
||||||
|
"Please enter a URL": "Veuillez entrer une URL",
|
||||||
|
"Reload": "Recharger",
|
||||||
"Rename": "Renommer",
|
"Rename": "Renommer",
|
||||||
|
"Reset to Default": "Réinitialiser par défaut",
|
||||||
|
"Save": "Enregistrer",
|
||||||
"Save changes": "Enregistrer les modifications",
|
"Save changes": "Enregistrer les modifications",
|
||||||
"Saving...": "Enregistrement...",
|
"Saving": {
|
||||||
|
"": {
|
||||||
|
"": {
|
||||||
|
"": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Search your notes...": "Recherche dans tes notes...",
|
"Search your notes...": "Recherche dans tes notes...",
|
||||||
"Settings": "Paramètres",
|
"Settings": "Paramètres",
|
||||||
"Settings saved. Please refresh the page.": "Paramètres enregistrés. Veuillez actualiser la page.",
|
|
||||||
"Tag deleted": "Étiquette supprimée",
|
"Tag deleted": "Étiquette supprimée",
|
||||||
"Tag renamed": "Étiquette renommée",
|
"Tag renamed": "Étiquette renommée",
|
||||||
"Tags": "Étiquettes",
|
"Tags": "Étiquettes",
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"{{count}} selected_many": "{{count}} zaznaczonych",
|
"{{count}} selected_many": "{{count}} zaznaczonych",
|
||||||
"{{count}} selected_other": "{{count}} zaznaczonych",
|
"{{count}} selected_other": "{{count}} zaznaczonych",
|
||||||
"Add a new note to your collection.": "Dodaj nową notatkę do swojej kolekcji.",
|
"Add a new note to your collection.": "Dodaj nową notatkę do swojej kolekcji.",
|
||||||
|
"API Configuration": "Konfiguracja API",
|
||||||
|
"API URL reset to default. Please reload the page.": "URL API zresetowano do domyślnego. Przeładuj stronę.",
|
||||||
|
"API URL updated successfully. Please reload the page.": "URL API zaktualizowano pomyślnie. Przeładuj stronę.",
|
||||||
"Archive": "Archiwum",
|
"Archive": "Archiwum",
|
||||||
"Archived {{count}} note_one": "Zarchiwizowano {{count}} notatkę",
|
"Archived {{count}} note_one": "Zarchiwizowano {{count}} notatkę",
|
||||||
"Archived {{count}} note_few": "Zarchiwizowano {{count}} notatki",
|
"Archived {{count}} note_few": "Zarchiwizowano {{count}} notatki",
|
||||||
@@ -15,11 +18,15 @@
|
|||||||
"Are you sure you want to delete {{count}} note?_other": "Czy na pewno chcesz usunąć {{count}} notatek?",
|
"Are you sure you want to delete {{count}} note?_other": "Czy na pewno chcesz usunąć {{count}} notatek?",
|
||||||
"Are you sure?": "Czy jesteś pewien?",
|
"Are you sure?": "Czy jesteś pewien?",
|
||||||
"Backend URL": "Adres URL backendu",
|
"Backend URL": "Adres URL backendu",
|
||||||
|
"Choose your preferred language": "Wybierz preferowany język",
|
||||||
"Color": "Kolor",
|
"Color": "Kolor",
|
||||||
"Configure the application settings.": "Skonfiguruj ustawienia aplikacji.",
|
"Configure the application settings.": "Skonfiguruj ustawienia aplikacji.",
|
||||||
|
"Configure the backend API URL for this application": "Skonfiguruj URL API backendu dla tej aplikacji",
|
||||||
"Content": "Treść",
|
"Content": "Treść",
|
||||||
"Create": "Utwórz",
|
"Create": "Utwórz",
|
||||||
"Create Note": "Utwórz notatkę",
|
"Create Note": "Utwórz notatkę",
|
||||||
|
"Current API URL": "Aktualny URL API",
|
||||||
|
"Custom API URL": "Własny URL API",
|
||||||
"Data Management": "Zarządzanie danymi",
|
"Data Management": "Zarządzanie danymi",
|
||||||
"Delete": "Usuń",
|
"Delete": "Usuń",
|
||||||
"Delete tag \"{{name}}\"? Notes will keep their content.": "Usunąć tag \"{{name}}\"? Notatki zachowają swoją treść.",
|
"Delete tag \"{{name}}\"? Notes will keep their content.": "Usunąć tag \"{{name}}\"? Notatki zachowają swoją treść.",
|
||||||
@@ -37,9 +44,10 @@
|
|||||||
"Import Data": "Importuj dane",
|
"Import Data": "Importuj dane",
|
||||||
"Import failed": "Import nie powiódł się",
|
"Import failed": "Import nie powiódł się",
|
||||||
"Import successful. Reloading...": "Import zakończony sukcesem. Przeładowywanie...",
|
"Import successful. Reloading...": "Import zakończony sukcesem. Przeładowywanie...",
|
||||||
"Invalid URL": "Nieprawidłowy adres URL",
|
"Invalid URL format. Please enter a valid URL.": "Nieprawidłowy format URL. Podaj prawidłowy URL.",
|
||||||
"K-Notes": "K-Notes",
|
"K-Notes": "K-Notes",
|
||||||
"Language": "Język",
|
"Language": "Język",
|
||||||
|
"Leave empty to use the default or Docker-injected URL": "Pozostaw puste, aby użyć domyślnego lub wstrzykniętego przez Docker URL",
|
||||||
"List View": "Widok listy",
|
"List View": "Widok listy",
|
||||||
"New Note": "Nowa notatka",
|
"New Note": "Nowa notatka",
|
||||||
"No archived notes yet": "Jeszcze nie ma zarchiwizowanych notatek",
|
"No archived notes yet": "Jeszcze nie ma zarchiwizowanych notatek",
|
||||||
@@ -53,7 +61,11 @@
|
|||||||
"Others": "Inne",
|
"Others": "Inne",
|
||||||
"Pin this note": "Przypnij tę notatkę",
|
"Pin this note": "Przypnij tę notatkę",
|
||||||
"Pinned": "Przypięte",
|
"Pinned": "Przypięte",
|
||||||
|
"Please enter a URL": "Proszę podać URL",
|
||||||
|
"Reload": "Przeładuj",
|
||||||
"Rename": "Zmień nazwę",
|
"Rename": "Zmień nazwę",
|
||||||
|
"Reset to Default": "Przywróć domyślne",
|
||||||
|
"Save": "Zapisz",
|
||||||
"Save changes": "Zapisz zmiany",
|
"Save changes": "Zapisz zmiany",
|
||||||
"Saving": {
|
"Saving": {
|
||||||
"": {
|
"": {
|
||||||
@@ -64,7 +76,6 @@
|
|||||||
},
|
},
|
||||||
"Search your notes...": "Szukaj swoich notatek...",
|
"Search your notes...": "Szukaj swoich notatek...",
|
||||||
"Settings": "Ustawienia",
|
"Settings": "Ustawienia",
|
||||||
"Settings saved. Please refresh the page.": "Ustawienia zapisane. Odśwież stronę.",
|
|
||||||
"Tag deleted": "Tag usunięty",
|
"Tag deleted": "Tag usunięty",
|
||||||
"Tag renamed": "Nazwę tagu zmieniono",
|
"Tag renamed": "Nazwę tagu zmieniono",
|
||||||
"Tags": "Tagi",
|
"Tags": "Tagi",
|
||||||
@@ -75,4 +86,4 @@
|
|||||||
"Update": "Aktualizuj",
|
"Update": "Aktualizuj",
|
||||||
"work, todo, ideas": "praca, zadania, pomysły",
|
"work, todo, ideas": "praca, zadania, pomysły",
|
||||||
"Your notes will appear here. Click + to create one.": "Twoje notatki pojawią się tutaj. Kliknij +, aby utworzyć notatkę."
|
"Your notes will appear here. Click + to create one.": "Twoje notatki pojawią się tutaj. Kliknij +, aby utworzyć notatkę."
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Routes, Route, Navigate } from "react-router-dom";
|
import { Routes, Route, Navigate } from "react-router-dom";
|
||||||
import { ProtectedRoute, PublicRoute } from "@/components/auth-guard";
|
import { ProtectedRoute, PublicRoute } from "@/components/auth-guard";
|
||||||
|
import SettingsPage from "@/pages/settings";
|
||||||
import LoginPage from "@/pages/login";
|
import LoginPage from "@/pages/login";
|
||||||
import RegisterPage from "@/pages/register";
|
import RegisterPage from "@/pages/register";
|
||||||
import DashboardPage from "@/pages/dashboard";
|
import DashboardPage from "@/pages/dashboard";
|
||||||
@@ -22,6 +23,7 @@ function App() {
|
|||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
<Route path="/" element={<DashboardPage />} />
|
<Route path="/" element={<DashboardPage />} />
|
||||||
<Route path="/archive" element={<DashboardPage />} />
|
<Route path="/archive" element={<DashboardPage />} />
|
||||||
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/components/ui/sidebar"
|
} from "@/components/ui/sidebar"
|
||||||
import { Link, useLocation, useSearchParams, useNavigate } from "react-router-dom"
|
import { Link, useLocation, useSearchParams, useNavigate } from "react-router-dom"
|
||||||
import { SettingsDialog } from "@/components/settings-dialog"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useTags, useDeleteTag, useRenameTag } from "@/hooks/use-notes"
|
import { useTags, useDeleteTag, useRenameTag } from "@/hooks/use-notes"
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||||
@@ -149,7 +148,6 @@ function TagItem({ tag, isActive }: TagItemProps) {
|
|||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
||||||
const [tagsOpen, setTagsOpen] = useState(true);
|
const [tagsOpen, setTagsOpen] = useState(true);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -176,9 +174,11 @@ export function AppSidebar() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton onClick={() => setSettingsOpen(true)} tooltip={t("Settings")}>
|
<SidebarMenuButton asChild isActive={location.pathname === "/settings"} tooltip={t("Settings")}>
|
||||||
<Settings />
|
<Link to="/settings">
|
||||||
<span>{t("Settings")}</span>
|
<Settings />
|
||||||
|
<span>{t("Settings")}</span>
|
||||||
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
@@ -222,7 +222,6 @@ export function AppSidebar() {
|
|||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} dataManagementEnabled />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { useRef, useState, useEffect } from "react";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { api } from "@/lib/api";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
|
import { useApiUrl } from "@/hooks/use-api-url";
|
||||||
|
import { useDataManagement } from "@/hooks/use-data-management";
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -16,64 +15,13 @@ interface SettingsDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: SettingsDialogProps) {
|
export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: SettingsDialogProps) {
|
||||||
const [url, setUrl] = useState("http://localhost:3000");
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { apiUrl, setApiUrl, saveApiUrl } = useApiUrl();
|
||||||
useEffect(() => {
|
const { fileInputRef, exportData, importData, triggerImport } = useDataManagement();
|
||||||
const stored = localStorage.getItem("k_notes_api_url");
|
|
||||||
if (stored) {
|
|
||||||
setUrl(stored);
|
|
||||||
}
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
try {
|
if (saveApiUrl(apiUrl)) {
|
||||||
// Basic validation
|
|
||||||
new URL(url);
|
|
||||||
// Remove trailing slash if present
|
|
||||||
const cleanUrl = url.replace(/\/$/, "");
|
|
||||||
localStorage.setItem("k_notes_api_url", cleanUrl);
|
|
||||||
toast.success(t("Settings saved. Please refresh the page."));
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
window.location.reload();
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(t("Invalid URL"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleExport = async () => {
|
|
||||||
try {
|
|
||||||
const blob = await api.exportData();
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `k-notes-backup-${new Date().toISOString().split('T')[0]}.json`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
document.body.removeChild(a);
|
|
||||||
toast.success(t("Export successful"));
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(t("Export failed"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const text = await file.text();
|
|
||||||
const data = JSON.parse(text);
|
|
||||||
await api.importData(data);
|
|
||||||
toast.success(t("Import successful. Reloading..."));
|
|
||||||
onOpenChange(false);
|
|
||||||
window.location.reload();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
toast.error(t("Import failed"));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,8 +41,8 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se
|
|||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="url"
|
id="url"
|
||||||
value={url}
|
value={apiUrl}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setApiUrl(e.target.value)}
|
||||||
className="col-span-3"
|
className="col-span-3"
|
||||||
placeholder="http://localhost:3000"
|
placeholder="http://localhost:3000"
|
||||||
/>
|
/>
|
||||||
@@ -115,10 +63,10 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button variant="outline" onClick={handleExport}>
|
<Button variant="outline" onClick={exportData}>
|
||||||
{t("Export Data")}
|
{t("Export Data")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={() => fileInputRef.current?.click()}>
|
<Button variant="outline" onClick={triggerImport}>
|
||||||
{t("Import Data")}
|
{t("Import Data")}
|
||||||
</Button>
|
</Button>
|
||||||
<input
|
<input
|
||||||
@@ -126,7 +74,7 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se
|
|||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
accept=".json"
|
accept=".json"
|
||||||
onChange={handleImport}
|
onChange={importData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
63
k-notes-frontend/src/hooks/use-api-url.ts
Normal file
63
k-notes-frontend/src/hooks/use-api-url.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { getBaseUrl } from "@/lib/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export function useApiUrl() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [apiUrl, setApiUrl] = useState("");
|
||||||
|
const [currentApiUrl, setCurrentApiUrl] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const url = getBaseUrl();
|
||||||
|
setCurrentApiUrl(url);
|
||||||
|
setApiUrl(localStorage.getItem("k_notes_api_url") || "");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveApiUrl = (url: string) => {
|
||||||
|
const trimmedUrl = url.trim();
|
||||||
|
|
||||||
|
if (!trimmedUrl) {
|
||||||
|
toast.error(t("Please enter a URL"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(trimmedUrl);
|
||||||
|
const cleanUrl = trimmedUrl.replace(/\/$/, "");
|
||||||
|
localStorage.setItem("k_notes_api_url", cleanUrl);
|
||||||
|
toast.success(t("API URL updated successfully. Please reload the page."), {
|
||||||
|
action: {
|
||||||
|
label: t("Reload"),
|
||||||
|
onClick: () => window.location.reload(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setCurrentApiUrl(cleanUrl);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
toast.error(t("Invalid URL format. Please enter a valid URL."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetApiUrl = () => {
|
||||||
|
localStorage.removeItem("k_notes_api_url");
|
||||||
|
setApiUrl("");
|
||||||
|
const defaultUrl = window.env?.API_URL || "http://localhost:3000";
|
||||||
|
setCurrentApiUrl(defaultUrl);
|
||||||
|
toast.success(t("API URL reset to default. Please reload the page."), {
|
||||||
|
action: {
|
||||||
|
label: t("Reload"),
|
||||||
|
onClick: () => window.location.reload(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiUrl,
|
||||||
|
setApiUrl,
|
||||||
|
currentApiUrl,
|
||||||
|
saveApiUrl,
|
||||||
|
resetApiUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
53
k-notes-frontend/src/hooks/use-data-management.ts
Normal file
53
k-notes-frontend/src/hooks/use-data-management.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export function useDataManagement() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const exportData = async () => {
|
||||||
|
try {
|
||||||
|
const blob = await api.exportData();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `k-notes-backup-${new Date().toISOString().split('T')[0]}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
toast.success(t("Export successful"));
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(t("Export failed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const importData = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
|
await api.importData(data);
|
||||||
|
toast.success(t("Import successful. Reloading..."));
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast.error(t("Import failed"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerImport = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileInputRef,
|
||||||
|
exportData,
|
||||||
|
importData,
|
||||||
|
triggerImport,
|
||||||
|
};
|
||||||
|
}
|
||||||
125
k-notes-frontend/src/pages/settings.tsx
Normal file
125
k-notes-frontend/src/pages/settings.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useApiUrl } from "@/hooks/use-api-url";
|
||||||
|
import { useDataManagement } from "@/hooks/use-data-management";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||||
|
import { Settings as SettingsIcon, RotateCcw, Save, Download, Upload } from "lucide-react";
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { apiUrl, setApiUrl, currentApiUrl, saveApiUrl, resetApiUrl } = useApiUrl();
|
||||||
|
const { fileInputRef, exportData, importData, triggerImport } = useDataManagement();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto space-y-6">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<SettingsIcon className="h-6 w-6" />
|
||||||
|
<h1 className="text-3xl font-bold">{t("Settings")}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* API Configuration */}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t("API Configuration")}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t("Configure the backend API URL for this application")}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm text-muted-foreground">
|
||||||
|
{t("Current API URL")}
|
||||||
|
</Label>
|
||||||
|
<div className="p-3 bg-muted rounded-md font-mono text-sm break-all">
|
||||||
|
{currentApiUrl}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="api-url">{t("Custom API URL")}</Label>
|
||||||
|
<Input
|
||||||
|
id="api-url"
|
||||||
|
type="url"
|
||||||
|
placeholder="http://localhost:3000"
|
||||||
|
value={apiUrl}
|
||||||
|
onChange={(e) => setApiUrl(e.target.value)}
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t("Leave empty to use the default or Docker-injected URL")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-4">
|
||||||
|
<Button onClick={() => saveApiUrl(apiUrl)} className="flex items-center gap-2">
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
{t("Save")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={resetApiUrl}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4" />
|
||||||
|
{t("Reset to Default")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Language Settings */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t("Language")}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t("Choose your preferred language")}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Data Management */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t("Data Management")}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{t("Export your notes for backup or import from a JSON file.")}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={exportData}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
{t("Export Data")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={triggerImport}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Upload className="h-4 w-4" />
|
||||||
|
{t("Import Data")}
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
className="hidden"
|
||||||
|
accept=".json"
|
||||||
|
onChange={importData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user