Heavy modifications. INtegrations of APIs

This commit is contained in:
belviskhoremk
2026-03-06 23:17:30 +00:00
commit d5db301bd2
61 changed files with 12630 additions and 0 deletions

344
src/pages/Profile.tsx Normal file
View File

@@ -0,0 +1,344 @@
import React, { useState, useEffect } from 'react';
import { User, Mail, Phone, Lock, Save, Heart, LogOut, Camera } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import Button from '../components/common/Button';
import Card, { CardContent, CardHeader } from '../components/common/Card';
import { useAuth } from '../contexts/AuthContext';
import { api } from '../services/api';
import { usePageTitle } from '../hooks/usePageTitle';
import { useToast } from '../contexts/ToastContext';
import { useI18n } from '../contexts/I18nContext';
const Profile: React.FC = () => {
const { t } = useI18n();
usePageTitle('My Account');
const { user, logout, refreshUser } = useAuth();
const { showToast } = useToast();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<'profile' | 'password'>('profile');
const [isLoading, setIsLoading] = useState(false);
const [success, setSuccess] = useState('');
const [error, setError] = useState('');
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(user?.avatarUrl);
const [uploadingAvatar, setUploadingAvatar] = useState(false);
// Keep avatarUrl in sync when user context refreshes
useEffect(() => {
setAvatarUrl(user?.avatarUrl);
}, [user?.avatarUrl]);
const [profile, setProfile] = useState({
name: user?.name || '',
email: user?.email || '',
phone: user?.phone || '',
});
const [passwords, setPasswords] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
// Fetch latest user data on mount (to get phone and any other fields)
useEffect(() => {
api.users.me()
.then((data: any) => {
setProfile({
name: data.name || '',
email: data.email || '',
phone: data.phone || '',
});
})
.catch(() => {});
}, []);
const handleProfileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setProfile((prev) => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploadingAvatar(true);
try {
const result = await api.uploads.image(file);
const url = result.url || result.urls?.[0];
if (url) {
await api.users.updateMe({ avatar_url: url });
await refreshUser();
setAvatarUrl(url);
showToast(t.profile.avatarUpdated, 'success');
}
} catch (err: any) {
showToast(err.message || 'Failed to upload avatar', 'error');
} finally {
setUploadingAvatar(false);
}
};
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPasswords((prev) => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleProfileSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
setError('');
setSuccess('');
try {
await api.users.updateMe({ name: profile.name, email: profile.email, phone: profile.phone || undefined });
await refreshUser();
setSuccess(t.profile.profileUpdated);
showToast(t.profile.profileUpdated, 'success');
} catch (err: any) {
setError(err.message || 'Failed to update profile');
showToast(err.message || 'Failed to update profile', 'error');
} finally {
setIsLoading(false);
}
};
const handlePasswordSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setSuccess('');
if (passwords.newPassword !== passwords.confirmPassword) {
setError(t.profile.passwordsMismatch);
return;
}
if (passwords.newPassword.length < 8) {
setError(t.profile.passwordTooShort);
return;
}
setIsLoading(true);
try {
await api.auth.changePassword(passwords.currentPassword, passwords.newPassword);
setSuccess(t.profile.passwordChanged);
showToast(t.profile.passwordChanged, 'success');
setPasswords({ currentPassword: '', newPassword: '', confirmPassword: '' });
} catch (err: any) {
setError(err.message || 'Failed to change password');
showToast(err.message || 'Failed to change password', 'error');
} finally {
setIsLoading(false);
}
};
const handleLogout = () => {
logout();
navigate('/login');
};
if (!user) {
navigate('/login');
return null;
}
return (
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 className="text-3xl font-bold text-primary-800 mb-8">{t.profile.title}</h1>
{/* Profile Header */}
<Card className="mb-6">
<CardContent className="p-6">
<div className="flex items-center space-x-4">
<label className="relative cursor-pointer group">
<div className="w-20 h-20 rounded-full bg-primary-100 flex items-center justify-center overflow-hidden">
{avatarUrl ? (
<img src={avatarUrl} alt="Avatar" className="w-full h-full object-cover" />
) : (
<User className="h-10 w-10 text-primary-600" />
)}
</div>
<div className="absolute inset-0 rounded-full bg-black bg-opacity-0 group-hover:bg-opacity-30 flex items-center justify-center transition-all">
<Camera className="h-6 w-6 text-white opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
<input type="file" accept="image/*" className="hidden" onChange={handleAvatarUpload} disabled={uploadingAvatar} />
{uploadingAvatar && (
<div className="absolute inset-0 rounded-full bg-black bg-opacity-40 flex items-center justify-center">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
</div>
)}
</label>
<div>
<h2 className="text-xl font-semibold text-primary-800">{user.name}</h2>
<p className="text-primary-500">{user.email}</p>
<span className="inline-block mt-1 px-2 py-0.5 text-xs font-medium rounded-full bg-accent-100 text-accent-800 capitalize">
{user.role}
</span>
</div>
</div>
</CardContent>
</Card>
{/* Tabs */}
<div className="flex space-x-1 border border-gray-200 rounded-lg p-1 bg-gray-50 w-fit mb-6">
<button
onClick={() => { setActiveTab('profile'); setError(''); setSuccess(''); }}
className={`px-4 py-2 text-sm font-medium rounded-md flex items-center ${
activeTab === 'profile'
? 'bg-white shadow-sm text-primary-800'
: 'text-primary-600 hover:text-primary-800'
}`}
>
<User className="h-4 w-4 mr-2" />
{t.profile.profileTab}
</button>
<button
onClick={() => { setActiveTab('password'); setError(''); setSuccess(''); }}
className={`px-4 py-2 text-sm font-medium rounded-md flex items-center ${
activeTab === 'password'
? 'bg-white shadow-sm text-primary-800'
: 'text-primary-600 hover:text-primary-800'
}`}
>
<Lock className="h-4 w-4 mr-2" />
{t.profile.passwordTab}
</button>
</div>
{/* Feedback */}
{success && (
<div className="mb-4 p-3 rounded-md bg-success-50 text-success-700 text-sm">{success}</div>
)}
{error && (
<div className="mb-4 p-3 rounded-md bg-error-50 text-error-700 text-sm">{error}</div>
)}
{/* Profile Form */}
{activeTab === 'profile' && (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold text-primary-800">{t.profile.editProfile}</h3>
</CardHeader>
<CardContent>
<form onSubmit={handleProfileSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-primary-700 mb-1">{t.profile.fullName}</label>
<div className="relative">
<User className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
<input
name="name"
value={profile.name}
onChange={handleProfileChange}
className="pl-10 w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-2 focus:ring-accent-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-primary-700 mb-1">{t.profile.email}</label>
<div className="relative">
<Mail className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
<input
name="email"
type="email"
value={profile.email}
onChange={handleProfileChange}
className="pl-10 w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-2 focus:ring-accent-500"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-primary-700 mb-1">{t.profile.phone}</label>
<div className="relative">
<Phone className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
<input
name="phone"
type="tel"
value={profile.phone}
onChange={handleProfileChange}
placeholder="+228 90 00 00 00"
className="pl-10 w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-2 focus:ring-accent-500"
/>
</div>
</div>
<Button type="submit" variant="secondary" disabled={isLoading} icon={<Save className="h-4 w-4" />}>
{isLoading ? t.profile.saving : t.profile.saveChanges}
</Button>
</form>
</CardContent>
</Card>
)}
{/* Password Form */}
{activeTab === 'password' && (
<Card>
<CardHeader>
<h3 className="text-lg font-semibold text-primary-800">{t.profile.changePassword}</h3>
</CardHeader>
<CardContent>
<form onSubmit={handlePasswordSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-primary-700 mb-1">{t.profile.currentPassword}</label>
<input
name="currentPassword"
type="password"
value={passwords.currentPassword}
onChange={handlePasswordChange}
required
className="w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-2 focus:ring-accent-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-primary-700 mb-1">{t.profile.newPassword}</label>
<input
name="newPassword"
type="password"
value={passwords.newPassword}
onChange={handlePasswordChange}
required
minLength={8}
className="w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-2 focus:ring-accent-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-primary-700 mb-1">{t.profile.confirmPassword}</label>
<input
name="confirmPassword"
type="password"
value={passwords.confirmPassword}
onChange={handlePasswordChange}
required
minLength={8}
className="w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-2 focus:ring-accent-500"
/>
</div>
<Button type="submit" variant="secondary" disabled={isLoading} icon={<Lock className="h-4 w-4" />}>
{isLoading ? t.profile.changing : t.profile.changePassword}
</Button>
</form>
</CardContent>
</Card>
)}
{/* Quick Links */}
<div className="mt-6 grid grid-cols-1 sm:grid-cols-2 gap-4">
<button
onClick={() => navigate('/favorites')}
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition"
>
<Heart className="h-5 w-5 text-accent-600 mr-3" />
<span className="text-primary-700 font-medium">{t.profile.myFavorites}</span>
</button>
<button
onClick={handleLogout}
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-error-50 transition"
>
<LogOut className="h-5 w-5 text-error-500 mr-3" />
<span className="text-error-600 font-medium">{t.profile.signOut}</span>
</button>
</div>
</div>
);
};
export default Profile;