Some checks failed
Automated Container Build / build-and-push (push) Failing after 15s
302 lines
11 KiB
TypeScript
302 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { Settings, Shield, Image as ImageIcon, History, Trash2, Save, Loader2, LogOut } from 'lucide-react';
|
|
import api from '@/lib/api';
|
|
|
|
interface SettingsData {
|
|
theme: string;
|
|
grid_size: string;
|
|
trash_auto_purge_days: string;
|
|
}
|
|
|
|
interface SettingsPageProps {
|
|
onLogout: () => void;
|
|
}
|
|
|
|
export default function SettingsPage({ onLogout }: SettingsPageProps) {
|
|
const [settings, setSettings] = useState<SettingsData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
const [oldPassword, setOldPassword] = useState('');
|
|
const [newPassword, setNewPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [pwdSaving, setPwdSaving] = useState(false);
|
|
const [pwdError, setPwdError] = useState<string | null>(null);
|
|
const [pwdSuccess, setPwdSuccess] = useState(false);
|
|
|
|
useEffect(() => {
|
|
loadSettings();
|
|
}, []);
|
|
|
|
function applyTheme(theme: string) {
|
|
if (theme === 'light') {
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
document.documentElement.classList.remove('dark');
|
|
document.documentElement.style.setProperty('color-scheme', 'light');
|
|
} else {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
document.documentElement.classList.add('dark');
|
|
document.documentElement.style.setProperty('color-scheme', 'dark');
|
|
}
|
|
localStorage.setItem('theme', theme);
|
|
}
|
|
|
|
async function loadSettings() {
|
|
try {
|
|
const data = await api.getSettings();
|
|
const s = data.settings || data;
|
|
const loaded = {
|
|
theme: s.theme || 'dark',
|
|
grid_size: s.grid_size || '200',
|
|
trash_auto_purge_days: s.trash_auto_purge_days || '14',
|
|
};
|
|
setSettings(loaded);
|
|
// Apply the persisted theme on load
|
|
applyTheme(loaded.theme);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to load settings');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleSave() {
|
|
if (!settings) return;
|
|
setSaving(true);
|
|
setError(null);
|
|
setSuccess(false);
|
|
|
|
try {
|
|
const existing = await api.getSettings();
|
|
const merged = {
|
|
...(existing?.settings || existing || {}),
|
|
theme: settings.theme,
|
|
grid_size: settings.grid_size,
|
|
trash_auto_purge_days: settings.trash_auto_purge_days,
|
|
};
|
|
await api.updateSettings(merged);
|
|
setSuccess(true);
|
|
setTimeout(() => setSuccess(false), 3000);
|
|
applyTheme(settings.theme);
|
|
} catch (err: any) {
|
|
setError(err.message || 'Failed to save settings');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
async function handlePasswordChange(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (newPassword !== confirmPassword) {
|
|
setPwdError("New passwords do not match.");
|
|
return;
|
|
}
|
|
if (newPassword.length < 8) {
|
|
setPwdError("Password must be at least 8 characters.");
|
|
return;
|
|
}
|
|
setPwdSaving(true);
|
|
setPwdError(null);
|
|
setPwdSuccess(false);
|
|
try {
|
|
await api.changePassword(oldPassword, newPassword);
|
|
setPwdSuccess(true);
|
|
setOldPassword('');
|
|
setNewPassword('');
|
|
setConfirmPassword('');
|
|
setTimeout(() => setPwdSuccess(false), 3000);
|
|
} catch (err: any) {
|
|
setPwdError(err.message || 'Failed to change password');
|
|
} finally {
|
|
setPwdSaving(false);
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<Loader2 className="w-8 h-8 animate-spin" style={{ color: 'var(--color-text-tertiary)' }} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!settings) return null;
|
|
|
|
const sections = [
|
|
{
|
|
id: 'appearance',
|
|
title: 'Appearance',
|
|
icon: <Settings className="w-5 h-5" />,
|
|
content: (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-primary)' }}>Grid Tile Size ({settings.grid_size}px)</label>
|
|
<input
|
|
type="range"
|
|
min="120"
|
|
max="250"
|
|
step="10"
|
|
value={settings.grid_size}
|
|
onChange={(e) => setSettings({ ...settings, grid_size: e.target.value })}
|
|
className="w-full max-w-xs accent-[var(--color-accent)]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
id: 'security',
|
|
title: 'Security',
|
|
icon: <Shield className="w-5 h-5" />,
|
|
content: (
|
|
<form onSubmit={handlePasswordChange} className="space-y-4 max-w-xs">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-primary)' }}>Current Password</label>
|
|
<input
|
|
type="password"
|
|
value={oldPassword}
|
|
onChange={e => setOldPassword(e.target.value)}
|
|
required
|
|
className="w-full px-3 py-2 rounded-lg text-sm outline-none"
|
|
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-primary)' }}>New Password</label>
|
|
<input
|
|
type="password"
|
|
value={newPassword}
|
|
onChange={e => setNewPassword(e.target.value)}
|
|
required
|
|
minLength={8}
|
|
className="w-full px-3 py-2 rounded-lg text-sm outline-none"
|
|
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1" style={{ color: 'var(--color-text-primary)' }}>Confirm New Password</label>
|
|
<input
|
|
type="password"
|
|
value={confirmPassword}
|
|
onChange={e => setConfirmPassword(e.target.value)}
|
|
required
|
|
minLength={8}
|
|
className="w-full px-3 py-2 rounded-lg text-sm outline-none"
|
|
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
|
|
/>
|
|
</div>
|
|
{pwdError && <p className="text-sm" style={{ color: 'var(--color-error)' }}>{pwdError}</p>}
|
|
{pwdSuccess && <p className="text-sm" style={{ color: 'var(--color-success)' }}>Password changed successfully!</p>}
|
|
<button
|
|
type="submit"
|
|
disabled={pwdSaving}
|
|
className="px-4 py-2 rounded-lg text-sm font-medium text-white transition-opacity disabled:opacity-50"
|
|
style={{ backgroundColor: 'var(--color-accent)' }}
|
|
>
|
|
{pwdSaving ? 'Saving...' : 'Change Password'}
|
|
</button>
|
|
</form>
|
|
)
|
|
},
|
|
|
|
|
|
{
|
|
id: 'trash',
|
|
title: 'Trash Auto-Purge',
|
|
icon: <Trash2 className="w-5 h-5" />,
|
|
content: (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--color-text-primary)' }}>Days to Keep Trashed Files</label>
|
|
<p className="text-xs mb-3" style={{ color: 'var(--color-text-secondary)' }}>
|
|
Files in the trash will be permanently deleted after this many days. Set to 0 to disable auto-purge.
|
|
</p>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
max="365"
|
|
value={settings.trash_auto_purge_days}
|
|
onChange={(e) => setSettings({ ...settings, trash_auto_purge_days: e.target.value })}
|
|
className="w-full max-w-xs px-3 py-2 rounded-lg text-sm outline-none"
|
|
style={{ backgroundColor: 'var(--color-bg-primary)', border: '1px solid var(--color-border)', color: 'var(--color-text-primary)' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div className="flex-1 overflow-y-auto w-full">
|
|
<div className="p-8 max-w-4xl mx-auto w-full">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--color-text-primary)' }}>Settings</h1>
|
|
<p style={{ color: 'var(--color-text-secondary)' }}>Manage your Drive preferences and configurations.</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-6 p-4 rounded-xl flex items-center gap-3" style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', color: 'var(--color-danger)' }}>
|
|
<Shield className="w-5 h-5" />
|
|
<span className="text-sm font-medium">{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
{success && (
|
|
<div className="mb-6 p-4 rounded-xl flex items-center gap-3" style={{ backgroundColor: 'rgba(34, 197, 94, 0.1)', color: 'var(--color-success)' }}>
|
|
<Save className="w-5 h-5" />
|
|
<span className="text-sm font-medium">Settings saved successfully.</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-6">
|
|
{sections.map((section, idx) => (
|
|
<motion.div
|
|
key={section.id}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: idx * 0.05 }}
|
|
className="p-6 rounded-2xl"
|
|
style={{ backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)' }}
|
|
>
|
|
<div className="flex items-center gap-3 mb-6" style={{ color: 'var(--color-text-primary)' }}>
|
|
<div className="p-2 rounded-lg" style={{ backgroundColor: 'var(--color-bg-secondary)' }}>
|
|
{section.icon}
|
|
</div>
|
|
<h2 className="text-xl font-semibold">{section.title}</h2>
|
|
</div>
|
|
{section.content}
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
|
|
|
|
|
|
<div className="mt-8 flex justify-between items-center">
|
|
<button
|
|
onClick={onLogout}
|
|
className="flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-medium transition-all hover:opacity-80"
|
|
style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', color: 'var(--color-danger)' }}
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
Sign Out
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleSave}
|
|
disabled={saving}
|
|
className="flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-medium text-white transition-all hover:opacity-90 disabled:opacity-50"
|
|
style={{ background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)' }}
|
|
>
|
|
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
|
Save Settings
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|