diff --git a/k-notes-frontend/Dockerfile b/k-notes-frontend/Dockerfile index 0ad66b1..5a4d512 100644 --- a/k-notes-frontend/Dockerfile +++ b/k-notes-frontend/Dockerfile @@ -19,9 +19,15 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf # Create script to generate env-config.js from environment variables 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 '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 'echo "};" >> /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 '// Runtime environment configuration' >> /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 EXPOSE 80 diff --git a/k-notes-frontend/index.html b/k-notes-frontend/index.html index 1fc05a9..f7162a7 100644 --- a/k-notes-frontend/index.html +++ b/k-notes-frontend/index.html @@ -7,7 +7,7 @@ - + K-Notes diff --git a/k-notes-frontend/public/env-config.js b/k-notes-frontend/public/env-config.js new file mode 100644 index 0000000..682cfcb --- /dev/null +++ b/k-notes-frontend/public/env-config.js @@ -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 +}; diff --git a/k-notes-frontend/public/locales/de/translation.json b/k-notes-frontend/public/locales/de/translation.json index 503103a..5321e3d 100644 --- a/k-notes-frontend/public/locales/de/translation.json +++ b/k-notes-frontend/public/locales/de/translation.json @@ -2,6 +2,9 @@ "{{count}} selected_one": "{{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.", + "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", "Archived {{count}} note_one": "{{count}} Notiz 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?": "Bist du sicher?", "Backend URL": "Backend-URL", + "Choose your preferred language": "Wähle deine bevorzugte Sprache", "Color": "Farbe", "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", "Create": "Erstellen", "Create Note": "Notiz erstellen", + "Current API URL": "Aktuelle API-URL", + "Custom API URL": "Benutzerdefinierte API-URL", "Data Management": "Datenverwaltung", "Delete": "Löschen", "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 failed": "Import fehlgeschlagen", "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", "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", "New Note": "Neue Notiz", "No archived notes yet": "Noch keine archivierten Notizen", @@ -42,16 +50,24 @@ "Note created": "Notiz erstellt", "Note title": "Notiztitel", "Note updated": "Notiz aktualisiert", - "Notes": "Notizen", "Others": "Andere", "Pin this note": "Diese Notiz anheften", "Pinned": "Angeheftet", + "Please enter a URL": "Bitte gib eine URL ein", + "Reload": "Neu laden", "Rename": "Umbenennen", + "Reset to Default": "Auf Standard zurücksetzen", + "Save": "Speichern", "Save changes": "Änderungen speichern", - "Saving...": "Speichern...", + "Saving": { + "": { + "": { + "": "" + } + } + }, "Search your notes...": "Durchsuche deine Notizen...", "Settings": "Einstellungen", - "Settings saved. Please refresh the page.": "Einstellungen gespeichert. Bitte aktualisiere die Seite.", "Tag deleted": "Tag gelöscht", "Tag renamed": "Tag umbenannt", "Tags": "Tags", diff --git a/k-notes-frontend/public/locales/en/translation.json b/k-notes-frontend/public/locales/en/translation.json index 9697ac1..58311e9 100644 --- a/k-notes-frontend/public/locales/en/translation.json +++ b/k-notes-frontend/public/locales/en/translation.json @@ -2,6 +2,9 @@ "{{count}} selected_one": "{{count}} selected", "{{count}} selected_other": "{{count}} selected", "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", "Archived {{count}} note_one": "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?": "Are you sure?", "Backend URL": "Backend URL", + "Choose your preferred language": "Choose your preferred language", "Color": "Color", "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", "Create": "Create", "Create Note": "Create Note", + "Current API URL": "Current API URL", + "Custom API URL": "Custom API URL", "Data Management": "Data Management", "Delete": "Delete", "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 failed": "Import failed", "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", "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", "New Note": "New Note", "No archived notes yet": "No archived notes yet", @@ -45,7 +53,11 @@ "Others": "Others", "Pin this note": "Pin this note", "Pinned": "Pinned", + "Please enter a URL": "Please enter a URL", + "Reload": "Reload", "Rename": "Rename", + "Reset to Default": "Reset to Default", + "Save": "Save", "Save changes": "Save changes", "Saving": { "": { @@ -56,7 +68,6 @@ }, "Search your notes...": "Search your notes...", "Settings": "Settings", - "Settings saved. Please refresh the page.": "Settings saved. Please refresh the page.", "Tag deleted": "Tag deleted", "Tag renamed": "Tag renamed", "Tags": "Tags", diff --git a/k-notes-frontend/public/locales/es/translation.json b/k-notes-frontend/public/locales/es/translation.json index 42aee1b..44a9a97 100644 --- a/k-notes-frontend/public/locales/es/translation.json +++ b/k-notes-frontend/public/locales/es/translation.json @@ -1,23 +1,34 @@ { "{{count}} selected_one": "{{count}} seleccionado", + "{{count}} selected_many": "", "{{count}} selected_other": "{{count}} seleccionados", "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", "Archived {{count}} note_one": "{{count}} nota archivada", + "Archived {{count}} note_many": "", "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?_many": "", "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?", "Backend URL": "URL del backend", + "Choose your preferred language": "Elige tu idioma preferido", "Color": "Color", "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", "Create": "Crear", "Create Note": "Crear nota", + "Current API URL": "URL de API actual", + "Custom API URL": "URL de API personalizado", "Data Management": "Gestión de datos", "Delete": "Eliminar", "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_many": "", "Deleted {{count}} note_other": "{{count}} notas eliminadas", "Edit Note": "Editar nota", "Export Data": "Exportar datos", @@ -29,9 +40,10 @@ "Import Data": "Importar datos", "Import failed": "Importación fallida", "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", "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", "New Note": "Nueva nota", "No archived notes yet": "Aún no hay notas archivadas", @@ -42,16 +54,24 @@ "Note created": "Nota creada", "Note title": "Título de la nota", "Note updated": "Nota actualizada", - "Notes": "Notas", "Others": "Otros", "Pin this note": "Fijar esta nota", "Pinned": "Fijadas", + "Please enter a URL": "Por favor, introduce una URL", + "Reload": "Recargar", "Rename": "Renombrar", + "Reset to Default": "Restablecer a predeterminado", + "Save": "Guardar", "Save changes": "Guardar cambios", - "Saving...": "Guardando...", + "Saving": { + "": { + "": { + "": "" + } + } + }, "Search your notes...": "Busca tus notas...", "Settings": "Configuración", - "Settings saved. Please refresh the page.": "Configuración guardada. Por favor, actualiza la página.", "Tag deleted": "Etiqueta eliminada", "Tag renamed": "Etiqueta renombrada", "Tags": "Etiquetas", diff --git a/k-notes-frontend/public/locales/fr/translation.json b/k-notes-frontend/public/locales/fr/translation.json index 9ee2788..2347601 100644 --- a/k-notes-frontend/public/locales/fr/translation.json +++ b/k-notes-frontend/public/locales/fr/translation.json @@ -1,23 +1,34 @@ { "{{count}} selected_one": "{{count}} sélectionné", + "{{count}} selected_many": "", "{{count}} selected_other": "{{count}} sélectionnés", "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", "Archived {{count}} note_one": "{{count}} note archivée", + "Archived {{count}} note_many": "", "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?_many": "", "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 ?", "Backend URL": "URL du backend", + "Choose your preferred language": "Choisis ta langue préférée", "Color": "Couleur", "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", "Create": "Créer", "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", "Delete": "Supprimer", "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_many": "", "Deleted {{count}} note_other": "{{count}} notes supprimées", "Edit Note": "Modifier la note", "Export Data": "Exporter les données", @@ -29,9 +40,10 @@ "Import Data": "Importer les données", "Import failed": "Échec de l'importation", "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", "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", "New Note": "Nouvelle note", "No archived notes yet": "Pas encore de notes archivées", @@ -42,16 +54,24 @@ "Note created": "Note créée", "Note title": "Titre de la note", "Note updated": "Note mise à jour", - "Notes": "Notes", "Others": "Autres", "Pin this note": "Épingler cette note", "Pinned": "Épinglées", + "Please enter a URL": "Veuillez entrer une URL", + "Reload": "Recharger", "Rename": "Renommer", + "Reset to Default": "Réinitialiser par défaut", + "Save": "Enregistrer", "Save changes": "Enregistrer les modifications", - "Saving...": "Enregistrement...", + "Saving": { + "": { + "": { + "": "" + } + } + }, "Search your notes...": "Recherche dans tes notes...", "Settings": "Paramètres", - "Settings saved. Please refresh the page.": "Paramètres enregistrés. Veuillez actualiser la page.", "Tag deleted": "Étiquette supprimée", "Tag renamed": "Étiquette renommée", "Tags": "Étiquettes", diff --git a/k-notes-frontend/public/locales/pl/translation.json b/k-notes-frontend/public/locales/pl/translation.json index 3f83ddc..db9f3b3 100644 --- a/k-notes-frontend/public/locales/pl/translation.json +++ b/k-notes-frontend/public/locales/pl/translation.json @@ -4,6 +4,9 @@ "{{count}} selected_many": "{{count}} zaznaczonych", "{{count}} selected_other": "{{count}} zaznaczonych", "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", "Archived {{count}} note_one": "Zarchiwizowano {{count}} notatkę", "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?": "Czy jesteś pewien?", "Backend URL": "Adres URL backendu", + "Choose your preferred language": "Wybierz preferowany język", "Color": "Kolor", "Configure the application settings.": "Skonfiguruj ustawienia aplikacji.", + "Configure the backend API URL for this application": "Skonfiguruj URL API backendu dla tej aplikacji", "Content": "Treść", "Create": "Utwórz", "Create Note": "Utwórz notatkę", + "Current API URL": "Aktualny URL API", + "Custom API URL": "Własny URL API", "Data Management": "Zarządzanie danymi", "Delete": "Usuń", "Delete tag \"{{name}}\"? Notes will keep their content.": "Usunąć tag \"{{name}}\"? Notatki zachowają swoją treść.", @@ -37,9 +44,10 @@ "Import Data": "Importuj dane", "Import failed": "Import nie powiódł się", "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", "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", "New Note": "Nowa notatka", "No archived notes yet": "Jeszcze nie ma zarchiwizowanych notatek", @@ -53,7 +61,11 @@ "Others": "Inne", "Pin this note": "Przypnij tę notatkę", "Pinned": "Przypięte", + "Please enter a URL": "Proszę podać URL", + "Reload": "Przeładuj", "Rename": "Zmień nazwę", + "Reset to Default": "Przywróć domyślne", + "Save": "Zapisz", "Save changes": "Zapisz zmiany", "Saving": { "": { @@ -64,7 +76,6 @@ }, "Search your notes...": "Szukaj swoich notatek...", "Settings": "Ustawienia", - "Settings saved. Please refresh the page.": "Ustawienia zapisane. Odśwież stronę.", "Tag deleted": "Tag usunięty", "Tag renamed": "Nazwę tagu zmieniono", "Tags": "Tagi", @@ -75,4 +86,4 @@ "Update": "Aktualizuj", "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ę." -} +} \ No newline at end of file diff --git a/k-notes-frontend/src/App.tsx b/k-notes-frontend/src/App.tsx index 9006712..c482d5f 100644 --- a/k-notes-frontend/src/App.tsx +++ b/k-notes-frontend/src/App.tsx @@ -1,5 +1,6 @@ import { Routes, Route, Navigate } from "react-router-dom"; import { ProtectedRoute, PublicRoute } from "@/components/auth-guard"; +import SettingsPage from "@/pages/settings"; import LoginPage from "@/pages/login"; import RegisterPage from "@/pages/register"; import DashboardPage from "@/pages/dashboard"; @@ -22,6 +23,7 @@ function App() { }> } /> } /> + } /> diff --git a/k-notes-frontend/src/components/app-sidebar.tsx b/k-notes-frontend/src/components/app-sidebar.tsx index 0815bde..32b8a6b 100644 --- a/k-notes-frontend/src/components/app-sidebar.tsx +++ b/k-notes-frontend/src/components/app-sidebar.tsx @@ -10,7 +10,6 @@ import { SidebarMenuItem, } from "@/components/ui/sidebar" import { Link, useLocation, useSearchParams, useNavigate } from "react-router-dom" -import { SettingsDialog } from "@/components/settings-dialog" import { useState } from "react" import { useTags, useDeleteTag, useRenameTag } from "@/hooks/use-notes" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" @@ -149,7 +148,6 @@ function TagItem({ tag, isActive }: TagItemProps) { export function AppSidebar() { const location = useLocation(); const [searchParams] = useSearchParams(); - const [settingsOpen, setSettingsOpen] = useState(false); const [tagsOpen, setTagsOpen] = useState(true); const { t } = useTranslation(); @@ -176,9 +174,11 @@ export function AppSidebar() { ))} - setSettingsOpen(true)} tooltip={t("Settings")}> - - {t("Settings")} + + + + {t("Settings")} + @@ -222,7 +222,6 @@ export function AppSidebar() { - ) } diff --git a/k-notes-frontend/src/components/settings-dialog.tsx b/k-notes-frontend/src/components/settings-dialog.tsx index 23ad4f7..12cdddf 100644 --- a/k-notes-frontend/src/components/settings-dialog.tsx +++ b/k-notes-frontend/src/components/settings-dialog.tsx @@ -1,13 +1,12 @@ -import { useRef, useState, useEffect } from "react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { toast } from "sonner"; -import { api } from "@/lib/api"; import { Separator } from "@/components/ui/separator"; import { useTranslation } from "react-i18next"; import { LanguageSwitcher } from "@/components/language-switcher"; +import { useApiUrl } from "@/hooks/use-api-url"; +import { useDataManagement } from "@/hooks/use-data-management"; interface SettingsDialogProps { open: boolean; @@ -16,64 +15,13 @@ interface SettingsDialogProps { } export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: SettingsDialogProps) { - const [url, setUrl] = useState("http://localhost:3000"); const { t } = useTranslation(); - - useEffect(() => { - const stored = localStorage.getItem("k_notes_api_url"); - if (stored) { - setUrl(stored); - } - }, [open]); + const { apiUrl, setApiUrl, saveApiUrl } = useApiUrl(); + const { fileInputRef, exportData, importData, triggerImport } = useDataManagement(); const handleSave = () => { - try { - // 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.")); + if (saveApiUrl(apiUrl)) { onOpenChange(false); - window.location.reload(); - } catch (e) { - toast.error(t("Invalid URL")); - } - }; - - const fileInputRef = useRef(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) => { - 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 setUrl(e.target.value)} + value={apiUrl} + onChange={(e) => setApiUrl(e.target.value)} className="col-span-3" placeholder="http://localhost:3000" /> @@ -115,10 +63,10 @@ export function SettingsDialog({ open, onOpenChange, dataManagementEnabled }: Se

- -
diff --git a/k-notes-frontend/src/hooks/use-api-url.ts b/k-notes-frontend/src/hooks/use-api-url.ts new file mode 100644 index 0000000..6217875 --- /dev/null +++ b/k-notes-frontend/src/hooks/use-api-url.ts @@ -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, + }; +} diff --git a/k-notes-frontend/src/hooks/use-data-management.ts b/k-notes-frontend/src/hooks/use-data-management.ts new file mode 100644 index 0000000..4e304a1 --- /dev/null +++ b/k-notes-frontend/src/hooks/use-data-management.ts @@ -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(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) => { + 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, + }; +} diff --git a/k-notes-frontend/src/pages/settings.tsx b/k-notes-frontend/src/pages/settings.tsx new file mode 100644 index 0000000..e4afdee --- /dev/null +++ b/k-notes-frontend/src/pages/settings.tsx @@ -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 ( +
+
+ +

{t("Settings")}

+
+ + {/* API Configuration */} + + + + {t("API Configuration")} + + {t("Configure the backend API URL for this application")} + + + +
+ +
+ {currentApiUrl} +
+
+ +
+ + setApiUrl(e.target.value)} + className="font-mono" + /> +

+ {t("Leave empty to use the default or Docker-injected URL")} +

+
+ +
+ + +
+
+
+ + {/* Language Settings */} + + + {t("Language")} + + {t("Choose your preferred language")} + + + + + + + + {/* Data Management */} + + + {t("Data Management")} + + {t("Export your notes for backup or import from a JSON file.")} + + + +
+ + + +
+
+
+
+ ); +}