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:
107
frontend/src/components/profile/PasswordChangeForm.jsx
Normal file
107
frontend/src/components/profile/PasswordChangeForm.jsx
Normal 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;
|
||||||
213
frontend/src/components/profile/ProfileForm.jsx
Normal file
213
frontend/src/components/profile/ProfileForm.jsx
Normal 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;
|
||||||
2
frontend/src/components/profile/index.js
Normal file
2
frontend/src/components/profile/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as ProfileForm } from './ProfileForm';
|
||||||
|
export { default as PasswordChangeForm } from './PasswordChangeForm';
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user