feat: add password management, reading persistence on feed switch, and spin animation

This commit is contained in:
2026-08-24 21:53:54 -07:00
parent d7ed8876dc
commit 5ea176d5ad
12 changed files with 424 additions and 76 deletions
+120
View File
@@ -20,6 +20,8 @@ import {
Clock,
Timer,
Eye,
Lock,
Key,
} from 'lucide-react';
interface SettingsViewProps {
@@ -71,6 +73,43 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
const [opmlLoading, setOpmlLoading] = useState(false);
const [opmlResult, setOpmlResult] = useState<string | null>(null);
// Change Password state
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordLoading, setPasswordLoading] = useState(false);
const [passwordError, setPasswordError] = useState<string | null>(null);
const [passwordSuccess, setPasswordSuccess] = useState<string | null>(null);
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault();
setPasswordError(null);
setPasswordSuccess(null);
if (newPassword.length < 6) {
setPasswordError('New password must be at least 6 characters long');
return;
}
if (newPassword !== confirmPassword) {
setPasswordError('New passwords do not match');
return;
}
setPasswordLoading(true);
try {
await api.changePassword(oldPassword, newPassword);
setPasswordSuccess('Password updated successfully! Please use your new password next time you log in.');
setOldPassword('');
setNewPassword('');
setConfirmPassword('');
} catch (err: any) {
setPasswordError(err.message || 'Failed to update password');
} finally {
setPasswordLoading(false);
}
};
// Filter feeds
const filteredFeeds = feeds.filter(
(f) =>
@@ -633,6 +672,87 @@ export const SettingsView: React.FC<SettingsViewProps> = ({
</div>
</div>
<div className="settings-card">
<h3 className="settings-card-title" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Lock size={15} style={{ color: 'var(--accent)' }} />
<span>Change Password</span>
</h3>
<p className="settings-card-subtitle">
Update your single-user administrator password. Minimum 6 characters required.
</p>
{passwordSuccess && (
<div style={{ background: '#f0fdf4', border: '1px solid #bbf7d0', color: 'var(--success)', padding: '0.75rem 1rem', borderRadius: 6, marginBottom: '1rem', fontSize: '0.85rem', display: 'flex', alignItems: 'center', gap: 8 }}>
<CheckCircle2 size={16} />
<span>{passwordSuccess}</span>
</div>
)}
{passwordError && (
<div style={{ background: '#fef2f2', border: '1px solid #fecaca', color: 'var(--danger)', padding: '0.75rem 1rem', borderRadius: 6, marginBottom: '1rem', fontSize: '0.85rem', display: 'flex', alignItems: 'center', gap: 8 }}>
<AlertCircle size={16} />
<span>{passwordError}</span>
</div>
)}
<form onSubmit={handleChangePassword} style={{ maxWidth: 420, marginTop: '0.75rem' }}>
<div className="form-group">
<label className="form-label">Current Password</label>
<input
type="password"
className="form-input"
placeholder="Enter current password"
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
required
/>
</div>
<div className="form-group">
<label className="form-label">New Password</label>
<input
type="password"
className="form-input"
placeholder="Enter new password (min. 6 characters)"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
/>
</div>
<div className="form-group">
<label className="form-label">Confirm New Password</label>
<input
type="password"
className="form-input"
placeholder="Re-enter new password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
</div>
<button
type="submit"
className="btn-primary"
disabled={passwordLoading}
style={{ marginTop: '0.5rem' }}
>
{passwordLoading ? (
<>
<Loader2 size={14} className="animate-spin" />
<span>Updating Password...</span>
</>
) : (
<>
<Key size={14} />
<span>Update Password</span>
</>
)}
</button>
</form>
</div>
<div className="settings-card">
<h3 className="settings-card-title">Storage & Engine Stats</h3>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: '0.5rem' }}>