fischerX/apps/web/src/app/(dashboard)/profile/page.tsx

255 lines
7.8 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import api from '@/lib/api';
import { useUserStore } from '@/stores/userStore';
const profileSchema = z.object({
firstName: z.string().optional(),
lastName: z.string().optional(),
email: z.string().email('Invalid email format').optional().or(z.literal('')),
phone: z.string().regex(/^1[3-9]\d{9}$/, 'Invalid phone format').optional().or(z.literal('')),
});
type ProfileFormData = z.infer<typeof profileSchema>;
export default function ProfilePage() {
const { user, setUser } = useUserStore();
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<ProfileFormData>({
resolver: zodResolver(profileSchema),
});
useEffect(() => {
if (user) {
reset({
firstName: user.firstName || '',
lastName: user.lastName || '',
email: user.email || '',
phone: user.phone || '',
});
}
}, [user, reset]);
const onSubmit = async (data: ProfileFormData) => {
try {
setError(null);
setMessage(null);
setLoading(true);
const response = await api.put('/users/me', {
firstName: data.firstName || undefined,
lastName: data.lastName || undefined,
email: data.email || undefined,
phone: data.phone || undefined,
});
const updatedUser = response.data.data;
setUser({
...user,
...updatedUser,
});
setMessage('Profile updated successfully');
} catch (err) {
const error = err as { response?: { data?: { message?: string } } };
setError(error.response?.data?.message || 'Failed to update profile');
} finally {
setLoading(false);
}
};
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
setError('Please upload an image file');
return;
}
if (file.size > 5 * 1024 * 1024) {
setError('Image size must be less than 5MB');
return;
}
try {
setError(null);
setLoading(true);
const formData = new FormData();
formData.append('avatar', file);
const response = await api.post('/users/me/avatar', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
const updatedUser = response.data.data;
setUser({
...user,
avatar: updatedUser.avatar,
});
setMessage('Avatar updated successfully');
} catch (err) {
const error = err as { response?: { data?: { message?: string } } };
setError(error.response?.data?.message || 'Failed to upload avatar');
} finally {
setLoading(false);
}
};
const handleLogout = () => {
localStorage.removeItem('token');
setUser(null);
window.location.href = '/auth/login';
};
if (!user) {
return <div className="p-6">Loading...</div>;
}
return (
<div className="max-w-2xl mx-auto p-6 space-y-6">
<h1 className="text-2xl font-bold">Profile Settings</h1>
<div className="space-y-4">
<div className="flex items-center space-x-4">
<div className="w-20 h-20 rounded-full bg-gray-200 flex items-center justify-center overflow-hidden">
{user.avatar ? (
<img src={user.avatar} alt="Avatar" className="w-full h-full object-cover" />
) : (
<span className="text-2xl font-bold text-gray-500">
{user.firstName?.[0] || user.username?.[0] || 'U'}
</span>
)}
</div>
<div>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="px-4 py-2 text-sm border rounded-md hover:bg-gray-50"
disabled={loading}
>
Change Avatar
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleAvatarUpload}
className="hidden"
/>
<p className="text-xs text-gray-500 mt-1">JPG, PNG, GIF or WebP (max 5MB)</p>
</div>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">First Name</label>
<input
type="text"
{...register('firstName')}
className="w-full px-3 py-2 border rounded-md"
placeholder="John"
/>
{errors.firstName && (
<p className="text-sm text-red-500">{errors.firstName.message}</p>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Last Name</label>
<input
type="text"
{...register('lastName')}
className="w-full px-3 py-2 border rounded-md"
placeholder="Doe"
/>
{errors.lastName && (
<p className="text-sm text-red-500">{errors.lastName.message}</p>
)}
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Username</label>
<input
type="text"
value={user.username || ''}
disabled
className="w-full px-3 py-2 border rounded-md bg-gray-50"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Email</label>
<input
type="email"
{...register('email')}
className="w-full px-3 py-2 border rounded-md"
placeholder="your@email.com"
/>
{errors.email && (
<p className="text-sm text-red-500">{errors.email.message}</p>
)}
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Phone</label>
<input
type="tel"
{...register('phone')}
className="w-full px-3 py-2 border rounded-md"
placeholder="13800138000"
/>
{errors.phone && (
<p className="text-sm text-red-500">{errors.phone.message}</p>
)}
</div>
{message && (
<div className="p-3 bg-green-50 border border-green-200 rounded-md text-green-600 text-sm">
{message}
</div>
)}
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md text-red-600 text-sm">
{error}
</div>
)}
<div className="flex space-x-4">
<button
type="submit"
disabled={loading}
className="flex-1 py-2 px-4 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Saving...' : 'Save Changes'}
</button>
<button
type="button"
onClick={handleLogout}
className="py-2 px-4 border border-red-300 text-red-600 rounded-md hover:bg-red-50"
>
Logout
</button>
</div>
</form>
</div>
</div>
);
}