Create Settings page with profile editing and admin user management
This commit is contained in:
309
frontend/src/pages/Settings.tsx
Normal file
309
frontend/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import api from '../api/axios';
|
||||
|
||||
interface UserProfile {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
discord_id?: string;
|
||||
profile_picture?: string;
|
||||
is_admin: boolean;
|
||||
}
|
||||
|
||||
interface UpdateProfileData {
|
||||
email?: string;
|
||||
full_name?: string;
|
||||
discord_id?: string;
|
||||
profile_picture?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [formData, setFormData] = useState<UpdateProfileData>({});
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [allUsers, setAllUsers] = useState<UserProfile[]>([]);
|
||||
const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile();
|
||||
if (user?.is_admin) {
|
||||
loadAllUsers();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const loadProfile = async () => {
|
||||
try {
|
||||
const response = await api.get<UserProfile>('/api/v1/auth/me');
|
||||
setProfile(response.data);
|
||||
setFormData({
|
||||
email: response.data.email,
|
||||
full_name: response.data.full_name,
|
||||
discord_id: response.data.discord_id || '',
|
||||
profile_picture: response.data.profile_picture || '',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to load profile:', err);
|
||||
setError('Failed to load profile');
|
||||
}
|
||||
};
|
||||
|
||||
const loadAllUsers = async () => {
|
||||
try {
|
||||
const response = await api.get<UserProfile[]>('/api/v1/users');
|
||||
setAllUsers(response.data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load users:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setSuccess('');
|
||||
|
||||
// Validate passwords match if changing password
|
||||
if (formData.password && formData.password !== confirmPassword) {
|
||||
setError('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const updateData: UpdateProfileData = {};
|
||||
|
||||
// Only include changed fields
|
||||
if (formData.email !== profile?.email) updateData.email = formData.email;
|
||||
if (formData.full_name !== profile?.full_name) updateData.full_name = formData.full_name;
|
||||
if (formData.discord_id !== profile?.discord_id) updateData.discord_id = formData.discord_id;
|
||||
if (formData.profile_picture !== profile?.profile_picture) updateData.profile_picture = formData.profile_picture;
|
||||
if (formData.password) updateData.password = formData.password;
|
||||
|
||||
await api.put('/api/v1/auth/me', updateData);
|
||||
setSuccess('Profile updated successfully!');
|
||||
setFormData({ ...formData, password: '' });
|
||||
setConfirmPassword('');
|
||||
loadProfile();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to update profile');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdminUpdateUser = async (userId: number, updateData: Partial<UpdateProfileData & { is_active: boolean; is_admin: boolean }>) => {
|
||||
try {
|
||||
await api.put(`/api/v1/auth/users/${userId}`, updateData);
|
||||
setSuccess('User updated successfully!');
|
||||
loadAllUsers();
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.detail || 'Failed to update user');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
if (!profile) {
|
||||
return <div className="text-center py-8">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-8">Settings</h1>
|
||||
|
||||
{/* Personal Profile Section */}
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-900 mb-4">My Profile</h2>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded mb-4">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={profile.username}
|
||||
disabled
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500 cursor-not-allowed"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 mt-1">Username cannot be changed</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email Address
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="full_name" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
id="full_name"
|
||||
name="full_name"
|
||||
type="text"
|
||||
value={formData.full_name || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="discord_id" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Discord ID <span className="text-gray-500">(for notifications)</span>
|
||||
</label>
|
||||
<input
|
||||
id="discord_id"
|
||||
name="discord_id"
|
||||
type="text"
|
||||
value={formData.discord_id || ''}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g., YourDiscordName#1234"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="profile_picture" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Profile Picture URL
|
||||
</label>
|
||||
<input
|
||||
id="profile_picture"
|
||||
name="profile_picture"
|
||||
type="url"
|
||||
value={formData.profile_picture || ''}
|
||||
onChange={handleChange}
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-4 mt-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Change Password</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={formData.password || ''}
|
||||
onChange={handleChange}
|
||||
placeholder="Leave blank to keep current password"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="Confirm new password"
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Admin Section */}
|
||||
{user?.is_admin && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6">
|
||||
<h2 className="text-2xl font-semibold text-gray-900 mb-4">
|
||||
User Management <span className="text-sm font-normal text-gray-500">(Admin)</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{allUsers.map((u) => (
|
||||
<div key={u.id} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">{u.full_name}</h3>
|
||||
<p className="text-sm text-gray-500">@{u.username}</p>
|
||||
<p className="text-sm text-gray-500">{u.email}</p>
|
||||
{u.discord_id && (
|
||||
<p className="text-sm text-gray-500">Discord: {u.discord_id}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleAdminUpdateUser(u.id, { is_active: !u.is_active })}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
u.is_active
|
||||
? 'bg-yellow-100 text-yellow-800 hover:bg-yellow-200'
|
||||
: 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||
}`}
|
||||
>
|
||||
{u.is_active ? 'Lock' : 'Unlock'}
|
||||
</button>
|
||||
{u.id !== user.id && (
|
||||
<button
|
||||
onClick={() => setSelectedUserId(u.id)}
|
||||
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 hover:bg-blue-200 rounded"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
Reference in New Issue
Block a user