Files
tt-booking/components/user/user-profile.tsx
T

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>
);
}