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
212 lines
6.9 KiB
TypeScript
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;
|