329 lines
8.4 KiB
TypeScript
329 lines
8.4 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { User, Edit, Mail, Calendar, Save, X, Palette } from 'lucide-react';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { ModeToggle } from '@/components/ui/mode-toggle';
|
|
import { useTheme } from 'next-themes';
|
|
|
|
interface User {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
surname: string;
|
|
role: string;
|
|
createdAt: string;
|
|
themePreference: 'light' | 'dark' | 'system';
|
|
}
|
|
|
|
interface ProfileFormData {
|
|
name: string;
|
|
surname: string;
|
|
}
|
|
|
|
export function UserProfile() {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [formData, setFormData] = useState<ProfileFormData>({
|
|
name: '',
|
|
surname: '',
|
|
});
|
|
const { toast } = useToast();
|
|
const { theme, setTheme } = useTheme();
|
|
|
|
const updateFormData = (field: keyof ProfileFormData, value: string) => {
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchUserProfile();
|
|
}, []);
|
|
|
|
const fetchUserProfile = async () => {
|
|
try {
|
|
const response = await fetch('/api/users/profile');
|
|
if (response.ok) {
|
|
const userData = await response.json();
|
|
setUser(userData.user);
|
|
setFormData({
|
|
name: userData.user.name,
|
|
surname: userData.user.surname,
|
|
});
|
|
// Sync theme with user preference if available
|
|
if (userData.user.themePreference && userData.user.themePreference !== theme) {
|
|
setTheme(userData.user.themePreference);
|
|
}
|
|
} else {
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to fetch user profile',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching user profile:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to fetch user profile',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!formData.name.trim() || !formData.surname.trim()) {
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Name and surname are required',
|
|
variant: 'destructive',
|
|
});
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
const response = await fetch('/api/users/profile', {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: formData.name.trim(),
|
|
surname: formData.surname.trim(),
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
toast({
|
|
title: 'Success',
|
|
description: 'Profile updated successfully!',
|
|
});
|
|
setIsEditing(false);
|
|
await fetchUserProfile(); // Refresh user data
|
|
} else {
|
|
toast({
|
|
title: 'Error',
|
|
description: data.error || 'Failed to update profile',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating profile:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Failed to update profile',
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
if (user) {
|
|
setFormData({
|
|
name: user.name,
|
|
surname: user.surname,
|
|
});
|
|
}
|
|
setIsEditing(false);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardContent className='p-6'>
|
|
<div className='flex items-center justify-center'>
|
|
<div className='animate-spin rounded-full h-8 w-8 border-b-2 border-primary'></div>
|
|
<p className='ml-2 text-muted-foreground'>Loading profile...</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
if (!user) {
|
|
return (
|
|
<Card>
|
|
<CardContent className='p-6'>
|
|
<div className='text-center text-muted-foreground'>
|
|
<p>Unable to load user profile</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className='space-y-6'>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className='flex items-center gap-2'>
|
|
<User className='h-5 w-5' />
|
|
User Profile
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className='space-y-6'>
|
|
{/* Profile Information */}
|
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
|
|
{/* Name */}
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='name'>First Name</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
id='name'
|
|
value={formData.name}
|
|
onChange={(e) => updateFormData('name', e.target.value)}
|
|
placeholder='Enter your first name'
|
|
/>
|
|
) : (
|
|
<div className='flex items-center gap-2 p-2 bg-muted rounded'>
|
|
<span>{user.name}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Surname */}
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='surname'>Last Name</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
id='surname'
|
|
value={formData.surname}
|
|
onChange={(e) => updateFormData('surname', e.target.value)}
|
|
placeholder='Enter your last name'
|
|
/>
|
|
) : (
|
|
<div className='flex items-center gap-2 p-2 bg-muted rounded'>
|
|
<span>{user.surname}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Email (Read-only) */}
|
|
<div className='space-y-2'>
|
|
<Label htmlFor='email'>Email Address</Label>
|
|
<div className='flex items-center gap-2 p-2 bg-muted rounded text-muted-foreground'>
|
|
<Mail className='h-4 w-4' />
|
|
<span>{user.email}</span>
|
|
<span className='text-xs text-muted-foreground/60 ml-auto'>(Read-only)</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Member Since */}
|
|
<div className='space-y-2'>
|
|
<Label>Member Since</Label>
|
|
<div className='flex items-center gap-2 p-2 bg-muted rounded'>
|
|
<Calendar className='h-4 w-4' />
|
|
<span>
|
|
{new Date(user.createdAt).toLocaleDateString('en-IE', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
})}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className='flex gap-2 pt-4 border-t'>
|
|
{isEditing ? (
|
|
<>
|
|
<Button onClick={handleSave} disabled={saving} className='flex items-center gap-2'>
|
|
<Save className='h-4 w-4' />
|
|
{saving ? 'Saving...' : 'Save Changes'}
|
|
</Button>
|
|
<Button
|
|
variant='outline'
|
|
onClick={handleCancel}
|
|
disabled={saving}
|
|
className='flex items-center gap-2'
|
|
>
|
|
<X className='h-4 w-4' />
|
|
Cancel
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<Button onClick={() => setIsEditing(true)} className='flex items-center gap-2'>
|
|
<Edit className='h-4 w-4' />
|
|
Edit Profile
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Account Information Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Account Information</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
|
<div className='space-y-2'>
|
|
<Label>Account Type</Label>
|
|
<div className='p-2 bg-blue-50 dark:bg-blue-900/20 rounded text-blue-800 dark:text-blue-200 capitalize font-medium'>
|
|
{user.role}
|
|
</div>
|
|
</div>
|
|
<div className='space-y-2'>
|
|
<Label>User ID</Label>
|
|
<div className='p-2 bg-muted rounded text-muted-foreground font-mono text-sm'>
|
|
{user.id}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Theme Preferences Card */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className='flex items-center gap-2'>
|
|
<Palette className='h-5 w-5' />
|
|
Theme Preferences
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className='space-y-4'>
|
|
<div className='flex items-center justify-between'>
|
|
<div className='space-y-1'>
|
|
<Label>App Theme</Label>
|
|
<p className='text-sm text-muted-foreground'>
|
|
Choose how the app appears to you. System will use your device's theme setting.
|
|
</p>
|
|
</div>
|
|
<ModeToggle />
|
|
</div>
|
|
|
|
<div className='p-3 bg-muted/50 rounded-lg'>
|
|
<p className='text-sm'>
|
|
<strong>Current theme:</strong>{' '}
|
|
<span className='capitalize'>{theme === 'system' ? 'System preference' : theme}</span>
|
|
</p>
|
|
<p className='text-xs text-muted-foreground mt-1'>
|
|
Your theme preference is automatically saved and will be applied across all your
|
|
sessions.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|