refactor(frontend): extract ProfileForm and PasswordChangeForm from ProfilePage
- Create components/profile/ProfileForm.jsx (192 lines) - Create components/profile/PasswordChangeForm.jsx (99 lines) - Create components/profile/index.js barrel export - Reduce ProfilePage.jsx from 394 → 84 lines (-79%)
This commit is contained in:
@@ -1,142 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { authAPI } from '../services/api';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import Alert from '../components/common/Alert';
|
||||
import FormInput from '../components/common/FormInput';
|
||||
import FormSelect from '../components/common/FormSelect';
|
||||
import LoadingButton from '../components/common/LoadingButton';
|
||||
import Avatar from '../components/common/Avatar';
|
||||
import { User, Mail, Lock, Save, Hash, Youtube, Instagram, Facebook, MapPin, Globe } from 'lucide-react';
|
||||
import { COUNTRIES } from '../data/countries';
|
||||
import { ProfileForm, PasswordChangeForm } from '../components/profile';
|
||||
import { User, Lock } from 'lucide-react';
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { user, updateUser } = useAuth();
|
||||
const [activeTab, setActiveTab] = useState('profile');
|
||||
|
||||
// Profile edit state
|
||||
const [profileData, setProfileData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
wsdcId: '',
|
||||
youtubeUrl: '',
|
||||
instagramUrl: '',
|
||||
facebookUrl: '',
|
||||
tiktokUrl: '',
|
||||
country: '',
|
||||
city: '',
|
||||
});
|
||||
|
||||
// Load user data when component mounts or user changes
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setProfileData({
|
||||
firstName: user.firstName || '',
|
||||
lastName: user.lastName || '',
|
||||
email: user.email || '',
|
||||
wsdcId: user.wsdcId || '',
|
||||
youtubeUrl: user.youtubeUrl || '',
|
||||
instagramUrl: user.instagramUrl || '',
|
||||
facebookUrl: user.facebookUrl || '',
|
||||
tiktokUrl: user.tiktokUrl || '',
|
||||
country: user.country || '',
|
||||
city: user.city || '',
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const [profileLoading, setProfileLoading] = useState(false);
|
||||
const [profileMessage, setProfileMessage] = useState('');
|
||||
const [profileError, setProfileError] = useState('');
|
||||
|
||||
// Password change state
|
||||
const [passwordData, setPasswordData] = useState({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
const [passwordLoading, setPasswordLoading] = useState(false);
|
||||
const [passwordMessage, setPasswordMessage] = useState('');
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
|
||||
const handleProfileChange = (e) => {
|
||||
setProfileData({ ...profileData, [e.target.name]: e.target.value });
|
||||
setProfileMessage('');
|
||||
setProfileError('');
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e) => {
|
||||
setPasswordData({ ...passwordData, [e.target.name]: e.target.value });
|
||||
setPasswordMessage('');
|
||||
setPasswordError('');
|
||||
};
|
||||
|
||||
const handleProfileSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setProfileLoading(true);
|
||||
setProfileMessage('');
|
||||
setProfileError('');
|
||||
|
||||
try {
|
||||
const response = await authAPI.updateProfile(profileData);
|
||||
|
||||
if (response.success) {
|
||||
// Update context with new user data
|
||||
if (response.data.user) {
|
||||
updateUser(response.data.user);
|
||||
}
|
||||
|
||||
setProfileMessage(response.message);
|
||||
|
||||
if (response.data.emailChanged) {
|
||||
setProfileMessage(
|
||||
'Profile updated! Please check your new email address to verify it.'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setProfileError(error.data?.error || 'Failed to update profile');
|
||||
} finally {
|
||||
setProfileLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setPasswordLoading(true);
|
||||
setPasswordMessage('');
|
||||
setPasswordError('');
|
||||
|
||||
// Validate passwords match
|
||||
if (passwordData.newPassword !== passwordData.confirmPassword) {
|
||||
setPasswordError('New passwords do not match');
|
||||
setPasswordLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await authAPI.changePassword(
|
||||
passwordData.currentPassword,
|
||||
passwordData.newPassword
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
setPasswordMessage(response.message);
|
||||
// Clear form
|
||||
setPasswordData({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setPasswordError(error.data?.error || 'Failed to change password');
|
||||
} finally {
|
||||
setPasswordLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
@@ -159,7 +31,7 @@ const ProfilePage = () => {
|
||||
<p className="text-gray-600">@{user?.username}</p>
|
||||
{!user?.emailVerified && (
|
||||
<p className="text-sm text-yellow-600 mt-1">
|
||||
⚠️ Email not verified
|
||||
Email not verified
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -196,193 +68,11 @@ const ProfilePage = () => {
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{/* Profile Tab */}
|
||||
{activeTab === 'profile' && (
|
||||
<form onSubmit={handleProfileSubmit} className="space-y-4">
|
||||
<h2 className="text-xl font-semibold mb-4">Edit Profile</h2>
|
||||
|
||||
<Alert type="success" message={profileMessage} />
|
||||
<Alert type="error" message={profileError} />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormInput
|
||||
label="First Name"
|
||||
name="firstName"
|
||||
value={profileData.firstName}
|
||||
onChange={handleProfileChange}
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="Last Name"
|
||||
name="lastName"
|
||||
value={profileData.lastName}
|
||||
onChange={handleProfileChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Location Section */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormSelect
|
||||
label="Country"
|
||||
name="country"
|
||||
value={profileData.country}
|
||||
onChange={handleProfileChange}
|
||||
options={COUNTRIES}
|
||||
icon={Globe}
|
||||
placeholder="Select a country"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="City"
|
||||
name="city"
|
||||
value={profileData.city}
|
||||
onChange={handleProfileChange}
|
||||
icon={MapPin}
|
||||
placeholder="Somewhere"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* WSDC ID */}
|
||||
<FormInput
|
||||
label="WSDC ID"
|
||||
name="wsdcId"
|
||||
type="text"
|
||||
value={profileData.wsdcId}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
setProfileData({ ...profileData, wsdcId: value });
|
||||
}}
|
||||
icon={Hash}
|
||||
placeholder="12345"
|
||||
maxLength={10}
|
||||
helperText="Your World Swing Dance Council ID (optional)"
|
||||
/>
|
||||
|
||||
{/* Email */}
|
||||
<FormInput
|
||||
label="Email Address"
|
||||
name="email"
|
||||
type="email"
|
||||
value={profileData.email}
|
||||
onChange={handleProfileChange}
|
||||
icon={Mail}
|
||||
helperText="Changing your email will require re-verification"
|
||||
/>
|
||||
|
||||
{/* Social Media Links Section */}
|
||||
<div className="pt-4 border-t border-gray-200">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Social Media Links</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<FormInput
|
||||
label="YouTube"
|
||||
name="youtubeUrl"
|
||||
type="url"
|
||||
value={profileData.youtubeUrl}
|
||||
onChange={handleProfileChange}
|
||||
icon={Youtube}
|
||||
placeholder="https://youtube.com/@yourhandle"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="Instagram"
|
||||
name="instagramUrl"
|
||||
type="url"
|
||||
value={profileData.instagramUrl}
|
||||
onChange={handleProfileChange}
|
||||
icon={Instagram}
|
||||
placeholder="https://instagram.com/yourhandle"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="Facebook"
|
||||
name="facebookUrl"
|
||||
type="url"
|
||||
value={profileData.facebookUrl}
|
||||
onChange={handleProfileChange}
|
||||
icon={Facebook}
|
||||
placeholder="https://facebook.com/yourhandle"
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="TikTok"
|
||||
name="tiktokUrl"
|
||||
type="url"
|
||||
value={profileData.tiktokUrl}
|
||||
onChange={handleProfileChange}
|
||||
icon={() => (
|
||||
<svg className="h-5 w-5 text-gray-400" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z"/>
|
||||
</svg>
|
||||
)}
|
||||
placeholder="https://tiktok.com/@yourhandle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
loading={profileLoading}
|
||||
loadingText="Saving..."
|
||||
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
|
||||
>
|
||||
<Save className="w-5 h-5" />
|
||||
Save Changes
|
||||
</LoadingButton>
|
||||
</form>
|
||||
<ProfileForm user={user} onUserUpdate={updateUser} />
|
||||
)}
|
||||
|
||||
{/* Password Tab */}
|
||||
{activeTab === 'password' && (
|
||||
<form onSubmit={handlePasswordSubmit} className="space-y-4">
|
||||
<h2 className="text-xl font-semibold mb-4">Change Password</h2>
|
||||
|
||||
<Alert type="success" message={passwordMessage} />
|
||||
<Alert type="error" message={passwordError} />
|
||||
|
||||
<FormInput
|
||||
label="Current Password"
|
||||
name="currentPassword"
|
||||
type="password"
|
||||
value={passwordData.currentPassword}
|
||||
onChange={handlePasswordChange}
|
||||
icon={Lock}
|
||||
required
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="New Password"
|
||||
name="newPassword"
|
||||
type="password"
|
||||
value={passwordData.newPassword}
|
||||
onChange={handlePasswordChange}
|
||||
icon={Lock}
|
||||
required
|
||||
/>
|
||||
|
||||
<FormInput
|
||||
label="Confirm New Password"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
value={passwordData.confirmPassword}
|
||||
onChange={handlePasswordChange}
|
||||
icon={Lock}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Submit Button */}
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
loading={passwordLoading}
|
||||
loadingText="Changing..."
|
||||
className="w-full flex items-center justify-center gap-2 py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50"
|
||||
>
|
||||
<Lock className="w-5 h-5" />
|
||||
Change Password
|
||||
</LoadingButton>
|
||||
</form>
|
||||
)}
|
||||
{activeTab === 'password' && <PasswordChangeForm />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user