Migración desde workspace-v2/projects/template-saas/apps/frontend Este repositorio es parte del estándar multi-repo v2 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
199 lines
6.4 KiB
TypeScript
199 lines
6.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { useAuthStore, useUIStore } from '@/stores';
|
|
import toast from 'react-hot-toast';
|
|
import { Loader2, User, Building, Moon, Sun, Monitor } from 'lucide-react';
|
|
|
|
interface ProfileFormData {
|
|
first_name: string;
|
|
last_name: string;
|
|
email: string;
|
|
}
|
|
|
|
export function SettingsPage() {
|
|
const { user } = useAuthStore();
|
|
const { theme, setTheme } = useUIStore();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
} = useForm<ProfileFormData>({
|
|
defaultValues: {
|
|
first_name: user?.first_name || '',
|
|
last_name: user?.last_name || '',
|
|
email: user?.email || '',
|
|
},
|
|
});
|
|
|
|
const onSubmit = async (_data: ProfileFormData) => {
|
|
try {
|
|
setIsLoading(true);
|
|
// API call would go here
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
toast.success('Profile updated successfully!');
|
|
} catch {
|
|
toast.error('Failed to update profile');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const themes = [
|
|
{ value: 'light', label: 'Light', icon: Sun },
|
|
{ value: 'dark', label: 'Dark', icon: Moon },
|
|
{ value: 'system', label: 'System', icon: Monitor },
|
|
] as const;
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-3xl">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-secondary-900 dark:text-white">
|
|
Settings
|
|
</h1>
|
|
<p className="text-secondary-600 dark:text-secondary-400 mt-1">
|
|
Manage your account settings and preferences
|
|
</p>
|
|
</div>
|
|
|
|
{/* Profile Settings */}
|
|
<div className="card">
|
|
<div className="card-header flex items-center gap-3">
|
|
<User className="w-5 h-5 text-secondary-500" />
|
|
<h2 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
|
Profile Information
|
|
</h2>
|
|
</div>
|
|
<div className="card-body">
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label htmlFor="first_name" className="label">
|
|
First name
|
|
</label>
|
|
<input
|
|
id="first_name"
|
|
type="text"
|
|
className="input"
|
|
{...register('first_name', { required: 'First name is required' })}
|
|
/>
|
|
{errors.first_name && (
|
|
<p className="mt-1 text-sm text-red-500">{errors.first_name.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="last_name" className="label">
|
|
Last name
|
|
</label>
|
|
<input
|
|
id="last_name"
|
|
type="text"
|
|
className="input"
|
|
{...register('last_name', { required: 'Last name is required' })}
|
|
/>
|
|
{errors.last_name && (
|
|
<p className="mt-1 text-sm text-red-500">{errors.last_name.message}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="email" className="label">
|
|
Email address
|
|
</label>
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
className="input"
|
|
disabled
|
|
{...register('email')}
|
|
/>
|
|
<p className="mt-1 text-sm text-secondary-500">
|
|
Contact support to change your email address
|
|
</p>
|
|
</div>
|
|
|
|
<div className="pt-4">
|
|
<button type="submit" disabled={isLoading} className="btn-primary">
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
Saving...
|
|
</>
|
|
) : (
|
|
'Save changes'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Organization Settings */}
|
|
<div className="card">
|
|
<div className="card-header flex items-center gap-3">
|
|
<Building className="w-5 h-5 text-secondary-500" />
|
|
<h2 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
|
Organization
|
|
</h2>
|
|
</div>
|
|
<div className="card-body">
|
|
<div className="flex items-center justify-between p-4 bg-secondary-50 dark:bg-secondary-700/50 rounded-lg">
|
|
<div>
|
|
<p className="font-medium text-secondary-900 dark:text-white">
|
|
Tenant ID
|
|
</p>
|
|
<p className="text-sm text-secondary-500 dark:text-secondary-400 font-mono">
|
|
{user?.tenant_id || 'N/A'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Appearance Settings */}
|
|
<div className="card">
|
|
<div className="card-header">
|
|
<h2 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
|
Appearance
|
|
</h2>
|
|
</div>
|
|
<div className="card-body">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
{themes.map((t) => (
|
|
<button
|
|
key={t.value}
|
|
onClick={() => setTheme(t.value)}
|
|
className={`flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors ${
|
|
theme === t.value
|
|
? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
|
|
: 'border-secondary-200 dark:border-secondary-700 hover:border-secondary-300 dark:hover:border-secondary-600'
|
|
}`}
|
|
>
|
|
<t.icon
|
|
className={`w-6 h-6 ${
|
|
theme === t.value
|
|
? 'text-primary-600 dark:text-primary-400'
|
|
: 'text-secondary-500'
|
|
}`}
|
|
/>
|
|
<span
|
|
className={`text-sm font-medium ${
|
|
theme === t.value
|
|
? 'text-primary-600 dark:text-primary-400'
|
|
: 'text-secondary-700 dark:text-secondary-300'
|
|
}`}
|
|
>
|
|
{t.label}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|