feat: add password management, reading persistence on feed switch, and spin animation
This commit is contained in:
@@ -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' }}>
|
||||
|
||||
@@ -20,6 +20,7 @@ interface ThreePaneViewProps {
|
||||
stats: Stats | null;
|
||||
entries: FeedEntry[];
|
||||
selectedIndex: number;
|
||||
activeArticle?: FeedEntry | null;
|
||||
onSelectEntry: (index: number) => void;
|
||||
onToggleRead: (entry: FeedEntry, e?: React.MouseEvent) => void;
|
||||
onToggleStar: (entry: FeedEntry, e?: React.MouseEvent) => void;
|
||||
@@ -35,6 +36,7 @@ export const ThreePaneView: React.FC<ThreePaneViewProps> = ({
|
||||
stats,
|
||||
entries,
|
||||
selectedIndex,
|
||||
activeArticle,
|
||||
onSelectEntry,
|
||||
onToggleRead,
|
||||
onToggleStar,
|
||||
@@ -44,7 +46,7 @@ export const ThreePaneView: React.FC<ThreePaneViewProps> = ({
|
||||
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
const readerPaneRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const selectedEntry = entries[selectedIndex] || entries[0];
|
||||
const selectedEntry = activeArticle || entries[selectedIndex] || entries[0];
|
||||
|
||||
// Auto-scroll list to active card
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user