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:
Radosław Gierwiało
2025-11-23 22:13:56 +01:00
parent 185c485ec7
commit 93ff331bfb
4 changed files with 328 additions and 316 deletions

View File

@@ -0,0 +1,107 @@
import { useState } from 'react';
import { authAPI } from '../../services/api';
import Alert from '../common/Alert';
import FormInput from '../common/FormInput';
import LoadingButton from '../common/LoadingButton';
import { Lock } from 'lucide-react';
const PasswordChangeForm = () => {
const [passwordData, setPasswordData] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const handleChange = (e) => {
setPasswordData({ ...passwordData, [e.target.name]: e.target.value });
setMessage('');
setError('');
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setMessage('');
setError('');
if (passwordData.newPassword !== passwordData.confirmPassword) {
setError('New passwords do not match');
setLoading(false);
return;
}
try {
const response = await authAPI.changePassword(
passwordData.currentPassword,
passwordData.newPassword
);
if (response.success) {
setMessage(response.message);
setPasswordData({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
}
} catch (err) {
setError(err.data?.error || 'Failed to change password');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<h2 className="text-xl font-semibold mb-4">Change Password</h2>
<Alert type="success" message={message} />
<Alert type="error" message={error} />
<FormInput
label="Current Password"
name="currentPassword"
type="password"
value={passwordData.currentPassword}
onChange={handleChange}
icon={Lock}
required
/>
<FormInput
label="New Password"
name="newPassword"
type="password"
value={passwordData.newPassword}
onChange={handleChange}
icon={Lock}
required
/>
<FormInput
label="Confirm New Password"
name="confirmPassword"
type="password"
value={passwordData.confirmPassword}
onChange={handleChange}
icon={Lock}
required
/>
<LoadingButton
type="submit"
loading={loading}
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>
);
};
export default PasswordChangeForm;

View File

@@ -0,0 +1,213 @@
import { useState, useEffect } from 'react';
import { authAPI } from '../../services/api';
import Alert from '../common/Alert';
import FormInput from '../common/FormInput';
import FormSelect from '../common/FormSelect';
import LoadingButton from '../common/LoadingButton';
import { Mail, Save, Hash, Youtube, Instagram, Facebook, MapPin, Globe } from 'lucide-react';
import { COUNTRIES } from '../../data/countries';
const TikTokIcon = () => (
<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>
);
const ProfileForm = ({ user, onUserUpdate }) => {
const [profileData, setProfileData] = useState({
firstName: '',
lastName: '',
email: '',
wsdcId: '',
youtubeUrl: '',
instagramUrl: '',
facebookUrl: '',
tiktokUrl: '',
country: '',
city: '',
});
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
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 handleChange = (e) => {
setProfileData({ ...profileData, [e.target.name]: e.target.value });
setMessage('');
setError('');
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setMessage('');
setError('');
try {
const response = await authAPI.updateProfile(profileData);
if (response.success) {
if (response.data.user) {
onUserUpdate(response.data.user);
}
setMessage(response.message);
if (response.data.emailChanged) {
setMessage(
'Profile updated! Please check your new email address to verify it.'
);
}
}
} catch (err) {
setError(err.data?.error || 'Failed to update profile');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<h2 className="text-xl font-semibold mb-4">Edit Profile</h2>
<Alert type="success" message={message} />
<Alert type="error" message={error} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormInput
label="First Name"
name="firstName"
value={profileData.firstName}
onChange={handleChange}
/>
<FormInput
label="Last Name"
name="lastName"
value={profileData.lastName}
onChange={handleChange}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormSelect
label="Country"
name="country"
value={profileData.country}
onChange={handleChange}
options={COUNTRIES}
icon={Globe}
placeholder="Select a country"
/>
<FormInput
label="City"
name="city"
value={profileData.city}
onChange={handleChange}
icon={MapPin}
placeholder="Somewhere"
/>
</div>
<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)"
/>
<FormInput
label="Email Address"
name="email"
type="email"
value={profileData.email}
onChange={handleChange}
icon={Mail}
helperText="Changing your email will require re-verification"
/>
<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={handleChange}
icon={Youtube}
placeholder="https://youtube.com/@yourhandle"
/>
<FormInput
label="Instagram"
name="instagramUrl"
type="url"
value={profileData.instagramUrl}
onChange={handleChange}
icon={Instagram}
placeholder="https://instagram.com/yourhandle"
/>
<FormInput
label="Facebook"
name="facebookUrl"
type="url"
value={profileData.facebookUrl}
onChange={handleChange}
icon={Facebook}
placeholder="https://facebook.com/yourhandle"
/>
<FormInput
label="TikTok"
name="tiktokUrl"
type="url"
value={profileData.tiktokUrl}
onChange={handleChange}
icon={TikTokIcon}
placeholder="https://tiktok.com/@yourhandle"
/>
</div>
</div>
<LoadingButton
type="submit"
loading={loading}
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>
);
};
export default ProfileForm;

View File

@@ -0,0 +1,2 @@
export { default as ProfileForm } from './ProfileForm';
export { default as PasswordChangeForm } from './PasswordChangeForm';

View File

@@ -1,142 +1,14 @@
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { authAPI } from '../services/api';
import Layout from '../components/layout/Layout'; 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 Avatar from '../components/common/Avatar';
import { User, Mail, Lock, Save, Hash, Youtube, Instagram, Facebook, MapPin, Globe } from 'lucide-react'; import { ProfileForm, PasswordChangeForm } from '../components/profile';
import { COUNTRIES } from '../data/countries'; import { User, Lock } from 'lucide-react';
const ProfilePage = () => { const ProfilePage = () => {
const { user, updateUser } = useAuth(); const { user, updateUser } = useAuth();
const [activeTab, setActiveTab] = useState('profile'); 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 ( return (
<Layout> <Layout>
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
@@ -159,7 +31,7 @@ const ProfilePage = () => {
<p className="text-gray-600">@{user?.username}</p> <p className="text-gray-600">@{user?.username}</p>
{!user?.emailVerified && ( {!user?.emailVerified && (
<p className="text-sm text-yellow-600 mt-1"> <p className="text-sm text-yellow-600 mt-1">
Email not verified Email not verified
</p> </p>
)} )}
</div> </div>
@@ -196,193 +68,11 @@ const ProfilePage = () => {
</div> </div>
<div className="p-6"> <div className="p-6">
{/* Profile Tab */}
{activeTab === 'profile' && ( {activeTab === 'profile' && (
<form onSubmit={handleProfileSubmit} className="space-y-4"> <ProfileForm user={user} onUserUpdate={updateUser} />
<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>
)} )}
{/* Password Tab */} {activeTab === 'password' && <PasswordChangeForm />}
{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>
)}
</div> </div>
</div> </div>
</div> </div>