From 61307fc22d832ebd6a303154d090ac69549e397b Mon Sep 17 00:00:00 2001 From: Gigi Date: Sun, 5 Oct 2025 03:53:02 +0100 Subject: [PATCH] feat: implement auto-save for settings with toast notifications - Create Toast component for user feedback - Add toast notification styles with slide-in animation - Remove Save Settings button from Settings component - Implement auto-save with 500ms debounce on setting changes - Show success/error toast messages when settings are saved - Settings now save automatically as user makes changes - Improves UX by eliminating manual save step --- src/components/Bookmarks.tsx | 20 +++++++++++----- src/components/Settings.tsx | 35 +++++++++++++-------------- src/components/Toast.tsx | 29 +++++++++++++++++++++++ src/index.css | 46 ++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 24 deletions(-) create mode 100644 src/components/Toast.tsx diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index a209266d..96d3ddbf 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -14,6 +14,7 @@ import { fetchReadableContent, ReadableContent } from '../services/readerService import Settings from './Settings' import { UserSettings, loadSettings, saveSettings, watchSettings } from '../services/settingsService' import { loadFont, getFontFamily } from '../utils/fontLoader' +import Toast from './Toast' export type ViewMode = 'compact' | 'cards' | 'large' interface BookmarksProps { @@ -40,7 +41,8 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { const [selectedHighlightId, setSelectedHighlightId] = useState(undefined) const [showSettings, setShowSettings] = useState(false) const [settings, setSettings] = useState({}) - const [isSavingSettings, setIsSavingSettings] = useState(false) + const [toastMessage, setToastMessage] = useState(null) + const [toastType, setToastType] = useState<'success' | 'error'>('success') const activeAccount = Hooks.useActiveAccount() const accountManager = Hooks.useAccountManager() const eventStore = useEventStore() @@ -114,18 +116,18 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { const handleSaveSettings = async (newSettings: UserSettings) => { if (!relayPool || !activeAccount) return - setIsSavingSettings(true) try { const fullAccount = accountManager.getActive() if (!fullAccount) throw new Error('No active account') const factory = new EventFactory({ signer: fullAccount }) await saveSettings(relayPool, eventStore, factory, newSettings, RELAY_URLS) setSettings(newSettings) - setShowSettings(false) + setToastType('success') + setToastMessage('Settings saved') } catch (err) { console.error('Failed to save settings:', err) - } finally { - setIsSavingSettings(false) + setToastType('error') + setToastMessage('Failed to save settings') } } @@ -171,7 +173,6 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { settings={settings} onSave={handleSaveSettings} onClose={() => setShowSettings(false)} - isSaving={isSavingSettings} /> ) : ( = ({ relayPool, onLogout }) => { /> + {toastMessage && ( + setToastMessage(null)} + /> + )} ) } diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index e1bb9264..3a924fee 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faTimes, faSave, faList, faThLarge, faImage } from '@fortawesome/free-solid-svg-icons' +import { faTimes, faList, faThLarge, faImage } from '@fortawesome/free-solid-svg-icons' import { UserSettings } from '../services/settingsService' import IconButton from './IconButton' import { loadFont, getFontFamily } from '../utils/fontLoader' @@ -9,11 +9,11 @@ interface SettingsProps { settings: UserSettings onSave: (settings: UserSettings) => Promise onClose: () => void - isSaving: boolean } -const Settings: React.FC = ({ settings, onSave, onClose, isSaving }) => { +const Settings: React.FC = ({ settings, onSave, onClose }) => { const [localSettings, setLocalSettings] = useState(settings) + const isInitialMount = useRef(true) useEffect(() => { setLocalSettings(settings) @@ -32,9 +32,19 @@ const Settings: React.FC = ({ settings, onSave, onClose, isSaving } }, [localSettings.readingFont]) - const handleSave = async () => { - await onSave(localSettings) - } + // Auto-save settings whenever they change (except on initial mount) + useEffect(() => { + if (isInitialMount.current) { + isInitialMount.current = false + return + } + + const timer = setTimeout(() => { + onSave(localSettings) + }, 500) // Debounce for 500ms + + return () => clearTimeout(timer) + }, [localSettings, onSave]) const previewFontFamily = getFontFamily(localSettings.readingFont) @@ -200,17 +210,6 @@ const Settings: React.FC = ({ settings, onSave, onClose, isSaving - -
- -
) } diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 00000000..e36745aa --- /dev/null +++ b/src/components/Toast.tsx @@ -0,0 +1,29 @@ +import React, { useEffect } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck, faTimes } from '@fortawesome/free-solid-svg-icons' + +interface ToastProps { + message: string + type?: 'success' | 'error' + onClose: () => void + duration?: number +} + +const Toast: React.FC = ({ message, type = 'success', onClose, duration = 3000 }) => { + useEffect(() => { + const timer = setTimeout(() => { + onClose() + }, duration) + + return () => clearTimeout(timer) + }, [duration, onClose]) + + return ( +
+ + {message} +
+ ) +} + +export default Toast diff --git a/src/index.css b/src/index.css index bd167805..aa777b10 100644 --- a/src/index.css +++ b/src/index.css @@ -1686,3 +1686,49 @@ body { opacity: 0.6; cursor: not-allowed; } + +/* Toast Notification */ +.toast { + position: fixed; + top: 2rem; + right: 2rem; + background: #1a1a1a; + color: #fff; + padding: 1rem 1.5rem; + border-radius: 8px; + border: 1px solid #333; + display: flex; + align-items: center; + gap: 0.75rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + animation: toast-slide-in 0.3s ease-out; + z-index: 9999; + font-size: 0.95rem; +} + +.toast-success { + border-color: #28a745; +} + +.toast-success svg { + color: #28a745; +} + +.toast-error { + border-color: #dc3545; +} + +.toast-error svg { + color: #dc3545; +} + +@keyframes toast-slide-in { + from { + transform: translateX(400px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +}