2025-02-18 13:13:13 +00:00
|
|
|
import { useState, useCallback } from 'react';
|
2025-02-02 00:42:30 +00:00
|
|
|
import { useStore } from '@nanostores/react';
|
|
|
|
import { classNames } from '~/utils/classNames';
|
|
|
|
import { profileStore, updateProfile } from '~/lib/stores/profile';
|
|
|
|
import { toast } from 'react-toastify';
|
2025-02-18 13:13:13 +00:00
|
|
|
import { debounce } from '~/utils/debounce';
|
2025-02-02 00:42:30 +00:00
|
|
|
|
|
|
|
export default function ProfileTab() {
|
|
|
|
const profile = useStore(profileStore);
|
|
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
|
|
|
2025-02-18 13:13:13 +00:00
|
|
|
// Create debounced update functions
|
|
|
|
const debouncedUpdate = useCallback(
|
|
|
|
debounce((field: 'username' | 'bio', value: string) => {
|
|
|
|
updateProfile({ [field]: value });
|
|
|
|
toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`);
|
|
|
|
}, 1000),
|
|
|
|
[],
|
|
|
|
);
|
|
|
|
|
2025-02-02 00:42:30 +00:00
|
|
|
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
const file = e.target.files?.[0];
|
|
|
|
|
|
|
|
if (!file) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
setIsUploading(true);
|
|
|
|
|
|
|
|
// Convert the file to base64
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
|
|
reader.onloadend = () => {
|
|
|
|
const base64String = reader.result as string;
|
|
|
|
updateProfile({ avatar: base64String });
|
|
|
|
setIsUploading(false);
|
|
|
|
toast.success('Profile picture updated');
|
|
|
|
};
|
|
|
|
|
|
|
|
reader.onerror = () => {
|
|
|
|
console.error('Error reading file:', reader.error);
|
|
|
|
setIsUploading(false);
|
|
|
|
toast.error('Failed to update profile picture');
|
|
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error uploading avatar:', error);
|
|
|
|
setIsUploading(false);
|
|
|
|
toast.error('Failed to update profile picture');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleProfileUpdate = (field: 'username' | 'bio', value: string) => {
|
2025-02-18 13:13:13 +00:00
|
|
|
// Update the store immediately for UI responsiveness
|
2025-02-02 00:42:30 +00:00
|
|
|
updateProfile({ [field]: value });
|
|
|
|
|
2025-02-18 13:13:13 +00:00
|
|
|
// Debounce the toast notification
|
|
|
|
debouncedUpdate(field, value);
|
2025-02-02 00:42:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="max-w-2xl mx-auto">
|
|
|
|
<div className="space-y-6">
|
|
|
|
{/* Personal Information Section */}
|
|
|
|
<div>
|
|
|
|
{/* Avatar Upload */}
|
|
|
|
<div className="flex items-start gap-6 mb-8">
|
|
|
|
<div
|
|
|
|
className={classNames(
|
|
|
|
'w-24 h-24 rounded-full overflow-hidden',
|
|
|
|
'bg-gray-100 dark:bg-gray-800/50',
|
|
|
|
'flex items-center justify-center',
|
|
|
|
'ring-1 ring-gray-200 dark:ring-gray-700',
|
|
|
|
'relative group',
|
|
|
|
'transition-all duration-300 ease-out',
|
|
|
|
'hover:ring-purple-500/30 dark:hover:ring-purple-500/30',
|
|
|
|
'hover:shadow-lg hover:shadow-purple-500/10',
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
{profile.avatar ? (
|
|
|
|
<img
|
|
|
|
src={profile.avatar}
|
|
|
|
alt="Profile"
|
|
|
|
className={classNames(
|
|
|
|
'w-full h-full object-cover',
|
|
|
|
'transition-all duration-300 ease-out',
|
|
|
|
'group-hover:scale-105 group-hover:brightness-90',
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<div className="i-ph:robot-fill w-16 h-16 text-gray-400 dark:text-gray-500 transition-colors group-hover:text-purple-500/70 transform -translate-y-1" />
|
|
|
|
)}
|
|
|
|
|
|
|
|
<label
|
|
|
|
className={classNames(
|
|
|
|
'absolute inset-0',
|
|
|
|
'flex items-center justify-center',
|
|
|
|
'bg-black/0 group-hover:bg-black/40',
|
|
|
|
'cursor-pointer transition-all duration-300 ease-out',
|
|
|
|
isUploading ? 'cursor-wait' : '',
|
|
|
|
)}
|
|
|
|
>
|
|
|
|
<input
|
|
|
|
type="file"
|
|
|
|
accept="image/*"
|
|
|
|
className="hidden"
|
|
|
|
onChange={handleAvatarUpload}
|
|
|
|
disabled={isUploading}
|
|
|
|
/>
|
|
|
|
{isUploading ? (
|
|
|
|
<div className="i-ph:spinner-gap w-6 h-6 text-white animate-spin" />
|
|
|
|
) : (
|
|
|
|
<div className="i-ph:camera-plus w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-all duration-300 ease-out transform group-hover:scale-110" />
|
|
|
|
)}
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="flex-1 pt-1">
|
|
|
|
<label className="block text-base font-medium text-gray-900 dark:text-gray-100 mb-1">
|
|
|
|
Profile Picture
|
|
|
|
</label>
|
|
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Upload a profile picture or avatar</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* Username Input */}
|
|
|
|
<div className="mb-6">
|
|
|
|
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Username</label>
|
|
|
|
<div className="relative group">
|
|
|
|
<div className="absolute left-3.5 top-1/2 -translate-y-1/2">
|
|
|
|
<div className="i-ph:user-circle-fill w-5 h-5 text-gray-400 dark:text-gray-500 transition-colors group-focus-within:text-purple-500" />
|
|
|
|
</div>
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
value={profile.username}
|
|
|
|
onChange={(e) => handleProfileUpdate('username', e.target.value)}
|
|
|
|
className={classNames(
|
|
|
|
'w-full pl-11 pr-4 py-2.5 rounded-xl',
|
|
|
|
'bg-white dark:bg-gray-800/50',
|
|
|
|
'border border-gray-200 dark:border-gray-700/50',
|
|
|
|
'text-gray-900 dark:text-white',
|
|
|
|
'placeholder-gray-400 dark:placeholder-gray-500',
|
|
|
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50',
|
|
|
|
'transition-all duration-300 ease-out',
|
|
|
|
)}
|
|
|
|
placeholder="Enter your username"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/* Bio Input */}
|
|
|
|
<div className="mb-8">
|
|
|
|
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Bio</label>
|
|
|
|
<div className="relative group">
|
|
|
|
<div className="absolute left-3.5 top-3">
|
|
|
|
<div className="i-ph:text-aa w-5 h-5 text-gray-400 dark:text-gray-500 transition-colors group-focus-within:text-purple-500" />
|
|
|
|
</div>
|
|
|
|
<textarea
|
|
|
|
value={profile.bio}
|
|
|
|
onChange={(e) => handleProfileUpdate('bio', e.target.value)}
|
|
|
|
className={classNames(
|
|
|
|
'w-full pl-11 pr-4 py-2.5 rounded-xl',
|
|
|
|
'bg-white dark:bg-gray-800/50',
|
|
|
|
'border border-gray-200 dark:border-gray-700/50',
|
|
|
|
'text-gray-900 dark:text-white',
|
|
|
|
'placeholder-gray-400 dark:placeholder-gray-500',
|
|
|
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50',
|
|
|
|
'transition-all duration-300 ease-out',
|
|
|
|
'resize-none',
|
|
|
|
'h-32',
|
|
|
|
)}
|
|
|
|
placeholder="Tell us about yourself"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|