tududi/app/frontend/components/Profile/ProfileSettings.tsx
Chris Veleris dfcb97a355 Move to React
Add .gitignore

Removed node_modules from previous commit

Fix task modes

Fix task modes

Fix task modes

Remove node_modules

Update basic task modal

Add notes functionality

Improve UI

Setup views

Add scopes

Fix projects layout

Restructure

Fix rest of the UI issues

Cleanup old views

Add .env to .gitignore
2024-10-25 21:03:43 +03:00

212 lines
6.9 KiB
TypeScript

import React, { useState, useEffect, ChangeEvent, FormEvent } from 'react';
interface ProfileSettingsProps {
currentUser: { id: number; email: string };
}
interface Profile {
id: number;
email: string;
appearance: 'light' | 'dark';
language: string;
timezone: string;
avatar_image: string | null;
}
const ProfileSettings: React.FC<ProfileSettingsProps> = ({ currentUser }) => {
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [formData, setFormData] = useState({
appearance: 'light',
language: 'en',
timezone: 'UTC',
avatar_image: '',
});
useEffect(() => {
const fetchProfile = async () => {
try {
const response = await fetch('/api/profile', {
headers: { Accept: 'application/json' },
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to fetch profile.');
}
const data: Profile = await response.json();
setProfile(data);
setFormData({
appearance: data.appearance,
language: data.language,
timezone: data.timezone,
avatar_image: data.avatar_image || '',
});
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
};
fetchProfile();
}, []);
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleAvatarChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onloadend = () => {
setFormData((prev) => ({ ...prev, avatar_image: reader.result as string }));
};
reader.readAsDataURL(e.target.files[0]);
}
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError(null);
setSuccess(null);
try {
const response = await fetch('/api/profile', {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(formData),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to update profile.');
}
const updatedProfile: Profile = await response.json();
setProfile(updatedProfile);
setSuccess('Profile updated successfully.');
} catch (err) {
setError((err as Error).message);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
<div className="text-xl font-semibold text-gray-700 dark:text-gray-200">
Loading profile settings...
</div>
</div>
);
}
if (error) {
return (
<div className="flex items-center justify-center h-screen bg-gray-100 dark:bg-gray-900">
<div className="text-red-500 text-lg">{error}</div>
</div>
);
}
return (
<div className="max-w-4xl mx-auto p-6">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-6">
Profile Settings
</h2>
{success && <div className="mb-4 text-green-500">{success}</div>}
{error && <div className="mb-4 text-red-500">{error}</div>}
<form onSubmit={handleSubmit}>
{/* Appearance Selection */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Appearance
</label>
<select
name="appearance"
value={formData.appearance}
onChange={handleChange}
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
{/* Language Selection */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Language
</label>
<select
name="language"
value={formData.language}
onChange={handleChange}
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value="en">English</option>
<option value="es">Spanish</option>
{/* Add more languages if necessary */}
</select>
</div>
{/* Timezone Selection */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Timezone
</label>
<select
name="timezone"
value={formData.timezone}
onChange={handleChange}
className="mt-1 block w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm px-3 py-2 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value="UTC">UTC</option>
<option value="America/New_York">America/New_York</option>
<option value="Europe/London">Europe/London</option>
<option value="Asia/Tokyo">Asia/Tokyo</option>
</select>
</div>
{/* Avatar Image Upload */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Avatar Image
</label>
<input
type="file"
accept="image/*"
onChange={handleAvatarChange}
className="mt-1 block w-full text-sm text-gray-500 dark:text-gray-300 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 dark:file:bg-gray-700 dark:file:text-gray-200 dark:hover:file:bg-gray-600"
/>
{formData.avatar_image && (
<img
src={formData.avatar_image}
alt="Avatar Preview"
className="mt-2 h-24 w-24 rounded-full object-cover"
/>
)}
</div>
{/* Save Button */}
<div className="flex justify-end">
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
>
Save Changes
</button>
</div>
</form>
</div>
);
};
export default ProfileSettings;