mirror of
https://github.com/dergigi/boris.git
synced 2026-02-11 10:04:25 +01:00
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:
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
29
src/components/Toast.tsx
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user