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';