421 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|