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
This commit is contained in:
Gigi
2025-10-05 03:53:02 +01:00
parent 31610af706
commit 61307fc22d
4 changed files with 106 additions and 24 deletions

View File

@@ -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<BookmarksProps> = ({ relayPool, onLogout }) => {
const [selectedHighlightId, setSelectedHighlightId] = useState<string | undefined>(undefined)
const [showSettings, setShowSettings] = useState(false)
const [settings, setSettings] = useState<UserSettings>({})
const [isSavingSettings, setIsSavingSettings] = useState(false)
const [toastMessage, setToastMessage] = useState<string | null>(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<BookmarksProps> = ({ 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<BookmarksProps> = ({ relayPool, onLogout }) => {
settings={settings}
onSave={handleSaveSettings}
onClose={() => setShowSettings(false)}
isSaving={isSavingSettings}
/>
) : (
<ContentPanel
@@ -205,6 +206,13 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
/>
</div>
</div>
{toastMessage && (
<Toast
message={toastMessage}
type={toastType}
onClose={() => setToastMessage(null)}
/>
)}
</>
)
}

View File

@@ -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<void>
onClose: () => void
isSaving: boolean
}
const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose, isSaving }) => {
const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
const [localSettings, setLocalSettings] = useState<UserSettings>(settings)
const isInitialMount = useRef(true)
useEffect(() => {
setLocalSettings(settings)
@@ -32,9 +32,19 @@ const Settings: React.FC<SettingsProps> = ({ 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<SettingsProps> = ({ settings, onSave, onClose, isSaving
</div>
</div>
</div>
<div className="settings-footer">
<button
className="btn-primary"
onClick={handleSave}
disabled={isSaving}
>
<FontAwesomeIcon icon={faSave} />
{isSaving ? 'Saving...' : 'Save Settings'}
</button>
</div>
</div>
)
}

29
src/components/Toast.tsx Normal file
View File

@@ -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<ToastProps> = ({ message, type = 'success', onClose, duration = 3000 }) => {
useEffect(() => {
const timer = setTimeout(() => {
onClose()
}, duration)
return () => clearTimeout(timer)
}, [duration, onClose])
return (
<div className={`toast toast-${type}`}>
<FontAwesomeIcon icon={type === 'success' ? faCheck : faTimes} />
<span>{message}</span>
</div>
)
}
export default Toast

View File

@@ -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;
}
}