forge/frontend/app/settings/page.tsx

421 lines
15 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
import { Settings, User, Bell, Palette, Key, Save, Shield, LogOut, Loader2 } from 'lucide-react';
import { useStore } from '@/lib/store';
import { usersApi, authApi } from '@/lib/api';
export default function SettingsPage() {
const router = useRouter();
const { user, setUser, logout } = useStore();
const [activeTab, setActiveTab] = useState('profile');
const [loading, setLoading] = useState(false);
// Profile state
const [name, setName] = useState('');
const [email, setEmail] = useState('');
// Security state
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [savingPassword, setSavingPassword] = useState(false);
const [loggingOut, setLoggingOut] = useState(false);
// Notification preferences
const [emailNotifications, setEmailNotifications] = useState(true);
const [jobCompletionAlerts, setJobCompletionAlerts] = useState(true);
// Default settings
const [defaultImageProvider, setDefaultImageProvider] = useState('openai');
const [defaultVideoProvider, setDefaultVideoProvider] = useState('runway');
const [defaultVoice, setDefaultVoice] = useState('');
useEffect(() => {
if (user) {
setName(user.name || '');
setEmail(user.email || '');
}
}, [user]);
const handleSaveProfile = async () => {
setLoading(true);
try {
const response = await usersApi.updateProfile({ name });
setUser(response.data);
toast.success('Profile updated!');
} catch (err) {
toast.error('Failed to update profile');
} finally {
setLoading(false);
}
};
const handleChangePassword = async () => {
if (!currentPassword || !newPassword || !confirmPassword) {
toast.error('Please fill in all password fields');
return;
}
if (newPassword.length < 8) {
toast.error('New password must be at least 8 characters');
return;
}
if (newPassword !== confirmPassword) {
toast.error('New passwords do not match');
return;
}
setSavingPassword(true);
try {
await authApi.changePassword({
current_password: currentPassword,
new_password: newPassword,
});
setCurrentPassword('');
setNewPassword('');
setConfirmPassword('');
toast.success('Password changed successfully');
} catch (err: any) {
toast.error(err.response?.data?.detail || 'Failed to change password');
} finally {
setSavingPassword(false);
}
};
const handleLogout = async () => {
setLoggingOut(true);
try {
await authApi.logout();
} catch (err) {
// Ignore logout errors
}
logout();
toast.success('Logged out successfully');
router.push('/login');
};
const tabs = [
{ id: 'profile', label: 'Profile', icon: User },
{ id: 'security', label: 'Security', icon: Shield },
{ id: 'notifications', label: 'Notifications', icon: Bell },
{ id: 'preferences', label: 'Preferences', icon: Palette },
{ id: 'api-keys', label: 'API Keys', icon: Key },
];
return (
<div className="max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-forge-yellow/10 rounded-lg flex items-center justify-center">
<Settings className="w-6 h-6 text-forge-yellow" />
</div>
<div>
<h1 className="text-2xl font-bold text-white">Settings</h1>
<p className="text-gray-500">Manage your account and preferences</p>
</div>
</div>
{/* Tabs */}
<div className="flex gap-2 border-b border-gray-800 pb-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors ${activeTab === tab.id
? 'bg-forge-yellow/10 text-forge-yellow'
: 'text-gray-400 hover:text-white hover:bg-forge-gray'
}`}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</div>
{/* Content */}
<div className="bg-forge-dark rounded-xl border border-gray-800 p-6">
{activeTab === 'profile' && (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-white">Profile Settings</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Display Name
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="input-field"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Email Address
</label>
<input
type="email"
value={email}
disabled
className="input-field opacity-50 cursor-not-allowed"
/>
<p className="text-xs text-gray-500 mt-1">
Email cannot be changed. Contact support for assistance.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Role
</label>
<input
type="text"
value={user?.role || 'user'}
disabled
className="input-field opacity-50 cursor-not-allowed capitalize"
/>
</div>
</div>
<button
onClick={handleSaveProfile}
disabled={loading}
className="btn-primary flex items-center gap-2"
>
<Save className="w-4 h-4" />
{loading ? 'Saving...' : 'Save Changes'}
</button>
</div>
)}
{activeTab === 'security' && (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-white">Security Settings</h2>
{/* Change Password */}
<div className="space-y-4">
<h3 className="text-white font-medium">Change Password</h3>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Current Password
</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
placeholder="Enter current password"
className="input-field"
autoComplete="current-password"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
New Password
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password"
className="input-field"
autoComplete="new-password"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Confirm New Password
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm new password"
className="input-field"
autoComplete="new-password"
/>
</div>
</div>
<button
onClick={handleChangePassword}
disabled={savingPassword}
className="btn-primary flex items-center gap-2"
>
{savingPassword ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Shield className="w-4 h-4" />
)}
{savingPassword ? 'Changing...' : 'Change Password'}
</button>
</div>
{/* Sign Out */}
<div className="pt-6 border-t border-gray-800">
<div className="flex items-center justify-between">
<div>
<h3 className="text-white font-medium">Sign Out</h3>
<p className="text-sm text-gray-500">
Sign out of your account on this device
</p>
</div>
<button
onClick={handleLogout}
disabled={loggingOut}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors flex items-center gap-2 disabled:opacity-50"
>
{loggingOut ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<LogOut className="w-4 h-4" />
)}
Sign Out
</button>
</div>
</div>
</div>
)}
{activeTab === 'notifications' && (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-white">Notification Settings</h2>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-forge-gray rounded-lg">
<div>
<p className="text-white font-medium">Email Notifications</p>
<p className="text-sm text-gray-500">
Receive email updates about your account
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={emailNotifications}
onChange={(e) => setEmailNotifications(e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-forge-yellow"></div>
</label>
</div>
<div className="flex items-center justify-between p-4 bg-forge-gray rounded-lg">
<div>
<p className="text-white font-medium">Job Completion Alerts</p>
<p className="text-sm text-gray-500">
Get notified when your jobs complete
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={jobCompletionAlerts}
onChange={(e) => setJobCompletionAlerts(e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-600 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-forge-yellow"></div>
</label>
</div>
</div>
</div>
)}
{activeTab === 'preferences' && (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-white">Default Preferences</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Default Image Provider
</label>
<select
value={defaultImageProvider}
onChange={(e) => setDefaultImageProvider(e.target.value)}
className="select-field"
>
<option value="openai">OpenAI DALL-E 3</option>
<option value="flux">Flux Pro</option>
<option value="ideogram">Ideogram</option>
</select>
<p className="text-xs text-gray-500 mt-1">
Default: OpenAI DALL-E 3
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Default Video Provider
</label>
<select
value={defaultVideoProvider}
onChange={(e) => setDefaultVideoProvider(e.target.value)}
className="select-field"
>
<option value="runway">Runway Gen-3 Alpha Turbo</option>
<option value="veo">Google Veo 2</option>
</select>
<p className="text-xs text-gray-500 mt-1">
Default: Runway Gen-3 Alpha Turbo
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Default Voice (Text-to-Speech)
</label>
<select
value={defaultVoice}
onChange={(e) => setDefaultVoice(e.target.value)}
className="select-field"
>
<option value="">Select a default voice...</option>
<option value="21m00Tcm4TlvDq8ikWAM">Rachel (Female, American)</option>
<option value="ErXwobaYiN019PkySvjV">Antoni (Male, American)</option>
<option value="AZnzlk1XvdvUeBnXmlld">Domi (Female, American)</option>
<option value="EXAVITQu4vr4xnSDxMaL">Bella (Female, American)</option>
</select>
<p className="text-xs text-gray-500 mt-1">
Powered by ElevenLabs
</p>
</div>
</div>
<button className="btn-primary flex items-center gap-2">
<Save className="w-4 h-4" />
Save Preferences
</button>
</div>
)}
{activeTab === 'api-keys' && (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-white">API Keys</h2>
<p className="text-gray-400">
Manage your personal API keys for third-party services.
</p>
<div className="bg-forge-gray rounded-lg p-4">
<p className="text-gray-400 text-sm">
API key management is handled at the organization level. Contact your
administrator to update API keys.
</p>
</div>
</div>
)}
</div>
</div>
);
}