Final UI V3
# UI V3 Changelog Major updates and improvements in this release: ## Core Changes - Complete NEW REWRITTEN UI system overhaul (V3) with semantic design tokens - New settings management system with drag-and-drop capabilities - Enhanced provider system supporting multiple AI services - Improved theme system with better dark mode support - New component library with consistent design patterns ## Technical Updates - Reorganized project architecture for better maintainability - Performance optimizations and bundle size improvements - Enhanced security features and access controls - Improved developer experience with better tooling - Comprehensive testing infrastructure ## New Features - Background rays effect for improved visual feedback - Advanced tab management system - Automatic and manual update support - Enhanced error handling and visualization - Improved accessibility across all components For detailed information about all changes and improvements, please see the full changelog.
This commit is contained in:
174
app/components/@settings/tabs/profile/ProfileTab.tsx
Normal file
174
app/components/@settings/tabs/profile/ProfileTab.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useState } from 'react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { profileStore, updateProfile } from '~/lib/stores/profile';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export default function ProfileTab() {
|
||||
const profile = useStore(profileStore);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
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) => {
|
||||
updateProfile({ [field]: value });
|
||||
|
||||
// Only show toast for completed typing (after 1 second of no typing)
|
||||
const debounceToast = setTimeout(() => {
|
||||
toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`);
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(debounceToast);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user