Files
family-hub/Settings_with_birthday.tsx

492 lines
18 KiB
TypeScript

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;
birthday?: string; // ISO date string
is_admin: boolean;
is_active: boolean;
}
interface UpdateProfileData {
email?: string;
full_name?: string;
discord_id?: string;
profile_picture?: string;
birthday?: string;
password?: string;
}
interface AdminUpdateData extends UpdateProfileData {
is_admin?: boolean;
is_active?: boolean;
}
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 [selectedUser, setSelectedUser] = useState<UserProfile | null>(null);
const [editFormData, setEditFormData] = useState<AdminUpdateData>({});
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 || '',
birthday: response.data.birthday || '',
});
} 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('');
if (formData.password && formData.password !== confirmPassword) {
setError('Passwords do not match');
return;
}
setIsLoading(true);
try {
const updateData: UpdateProfileData = {};
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.birthday !== profile?.birthday) updateData.birthday = formData.birthday;
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: AdminUpdateData) => {
try {
await api.put(`/api/v1/auth/users/${userId}`, updateData);
setSuccess('User updated successfully!');
setSelectedUser(null);
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 }));
};
const openEditModal = (u: UserProfile) => {
setSelectedUser(u);
setEditFormData({
email: u.email,
full_name: u.full_name,
discord_id: u.discord_id || '',
profile_picture: u.profile_picture || '',
birthday: u.birthday || '',
is_admin: u.is_admin,
is_active: u.is_active,
});
};
const handleEditFormChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setEditFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const submitUserEdit = async (e: React.FormEvent) => {
e.preventDefault();
if (selectedUser) {
await handleAdminUpdateUser(selectedUser.id, editFormData);
}
};
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="birthday" className="block text-sm font-medium text-gray-700 mb-2">
Birthday <span className="text-gray-500">(for chore scheduling)</span>
</label>
<input
id="birthday"
name="birthday"
type="date"
value={formData.birthday || ''}
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"
/>
<p className="text-sm text-gray-500 mt-1">Get a break from chores on your special day!</p>
</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} {u.is_admin && <span className="text-xs bg-purple-100 text-purple-800 px-2 py-1 rounded ml-2">Admin</span>}
</h3>
<p className="text-sm text-gray-500">@{u.username}</p>
<p className="text-sm text-gray-500">{u.email}</p>
{u.birthday && (
<p className="text-sm text-gray-500">🎂 {new Date(u.birthday).toLocaleDateString()}</p>
)}
{u.discord_id && (
<p className="text-sm text-gray-500">Discord: {u.discord_id}</p>
)}
{!u.is_active && (
<span className="text-xs bg-red-100 text-red-800 px-2 py-1 rounded">Locked</span>
)}
</div>
<div className="flex gap-2">
{u.id !== user.id && (
<>
<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>
<button
onClick={() => openEditModal(u)}
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>
)}
{/* Edit User Modal */}
{selectedUser && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 w-full max-w-md max-h-[90vh] overflow-y-auto">
<h2 className="text-2xl font-semibold text-gray-900 mb-4">
Edit User: {selectedUser.full_name}
</h2>
<form onSubmit={submitUserEdit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name
</label>
<input
name="full_name"
type="text"
value={editFormData.full_name || ''}
onChange={handleEditFormChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email
</label>
<input
name="email"
type="email"
value={editFormData.email || ''}
onChange={handleEditFormChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Birthday
</label>
<input
name="birthday"
type="date"
value={editFormData.birthday || ''}
onChange={handleEditFormChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Discord ID
</label>
<input
name="discord_id"
type="text"
value={editFormData.discord_id || ''}
onChange={handleEditFormChange}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 text-gray-900"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Profile Picture URL
</label>
<input
name="profile_picture"
type="url"
value={editFormData.profile_picture || ''}
onChange={handleEditFormChange}
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 text-gray-900"
/>
</div>
<div className="flex items-center space-x-4">
<label className="flex items-center">
<input
name="is_admin"
type="checkbox"
checked={editFormData.is_admin || false}
onChange={handleEditFormChange}
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Admin</span>
</label>
<label className="flex items-center">
<input
name="is_active"
type="checkbox"
checked={editFormData.is_active || false}
onChange={handleEditFormChange}
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
/>
<span className="ml-2 text-sm text-gray-700">Active</span>
</label>
</div>
<div className="flex gap-2 pt-4">
<button
type="submit"
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Save Changes
</button>
<button
type="button"
onClick={() => setSelectedUser(null)}
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"
>
Cancel
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
};
export default Settings;