try to do better
This commit is contained in:
@@ -1,44 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { familyApi, userApi, authApi } from '../api/client';
|
||||
import { User as UserIcon } from 'lucide-react';
|
||||
import { familyApi, authApi } from '../api/client';
|
||||
import { useStore } from '../store/useStore';
|
||||
import type { Theme } from '../types';
|
||||
import {
|
||||
User as UserIcon,
|
||||
Users,
|
||||
Settings,
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
Check,
|
||||
Palette,
|
||||
Languages,
|
||||
LogOut,
|
||||
Edit3,
|
||||
Save,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
|
||||
const THEMES: { id: Theme; gradient: string }[] = [
|
||||
{ id: 'light', gradient: 'bg-gradient-to-r from-gray-100 to-gray-200' },
|
||||
{ id: 'dark', gradient: 'bg-gradient-to-r from-black to-gray-900' },
|
||||
{ id: 'sunset', gradient: 'bg-gradient-to-r from-orange-400 to-pink-500' },
|
||||
{ id: 'ocean', gradient: 'bg-gradient-to-r from-blue-400 to-cyan-500' },
|
||||
{ id: 'forest', gradient: 'bg-gradient-to-r from-green-400 to-teal-500' },
|
||||
{ id: 'purple', gradient: 'bg-gradient-to-r from-purple-500 to-pink-500' },
|
||||
];
|
||||
import { useFamilyMembers, useConfirm } from '../hooks';
|
||||
import { Theme } from '../types';
|
||||
import { ProfileHeader } from '../components/profile/ProfileHeader';
|
||||
import { UserInfo } from '../components/profile/UserInfo';
|
||||
import { FamilySection } from '../components/profile/FamilySection';
|
||||
import { MembersSection } from '../components/profile/MembersSection';
|
||||
import { SettingsSection } from '../components/profile/SettingsSection';
|
||||
import { ConfirmModal, Card } from '../components/ui';
|
||||
import { showToast } from '../utils/toast';
|
||||
import { showErrorToast } from '../utils/errorHandler';
|
||||
|
||||
export default function Profile() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { user, selectedFamily, setSelectedFamily, setUser, preferences, setPreferences, familyMembers, setFamilyMembers } = useStore();
|
||||
const { user, selectedFamily, setSelectedFamily, setUser, preferences, setPreferences } = useStore();
|
||||
const { members, loading: membersLoading, loadMembers } = useFamilyMembers(user?.family_id || null);
|
||||
const { confirmState, confirm, cancel } = useConfirm();
|
||||
|
||||
const [membersLoading, setMembersLoading] = useState(false);
|
||||
const [leavingFamily, setLeavingFamily] = useState(false);
|
||||
const [editingName, setEditingName] = useState(false);
|
||||
const [newFamilyName, setNewFamilyName] = useState('');
|
||||
const [savingName, setSavingName] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.family_id) {
|
||||
@@ -46,12 +30,6 @@ export default function Profile() {
|
||||
}
|
||||
}, [user?.family_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.family_id && selectedFamily) {
|
||||
loadMembers();
|
||||
}
|
||||
}, [user?.family_id, selectedFamily]);
|
||||
|
||||
const loadFamily = async () => {
|
||||
if (!user?.family_id) return;
|
||||
try {
|
||||
@@ -62,90 +40,53 @@ export default function Profile() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadMembers = async () => {
|
||||
if (!user?.family_id) return;
|
||||
try {
|
||||
setMembersLoading(true);
|
||||
const response = await familyApi.getMembers(user.family_id);
|
||||
console.log('Loaded members:', response.data);
|
||||
setFamilyMembers(response.data);
|
||||
} catch (err) {
|
||||
console.error('Error loading members:', err);
|
||||
} finally {
|
||||
setMembersLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLeaveFamily = async () => {
|
||||
if (!confirm(t('profile.leaveConfirm'))) return;
|
||||
await confirm(t('profile.leaveConfirm'), t('profile.leaveMessage'));
|
||||
|
||||
try {
|
||||
setLeavingFamily(true);
|
||||
const { userApi } = await import('../api/client');
|
||||
await userApi.leaveFamily();
|
||||
|
||||
const meResponse = await authApi.me();
|
||||
setUser(meResponse.data);
|
||||
setSelectedFamily(null);
|
||||
setFamilyMembers([]);
|
||||
|
||||
showToast.success(t('profile.leftFamily'));
|
||||
navigate('/');
|
||||
} catch (err) {
|
||||
console.error('Error leaving family:', err);
|
||||
alert(t('profile.leaveError'));
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
} finally {
|
||||
setLeavingFamily(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleThemeChange = (theme: Theme) => {
|
||||
setPreferences({ theme });
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
};
|
||||
|
||||
const handleLocaleChange = (locale: 'ru' | 'en') => {
|
||||
setPreferences({ locale });
|
||||
i18n.changeLanguage(locale);
|
||||
};
|
||||
|
||||
const handleStartEditName = () => {
|
||||
setNewFamilyName(selectedFamily?.name || '');
|
||||
setEditingName(true);
|
||||
};
|
||||
|
||||
const handleSaveName = async () => {
|
||||
if (!selectedFamily || !newFamilyName.trim()) return;
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
setSavingName(true);
|
||||
const response = await familyApi.update(selectedFamily.id, { name: newFamilyName.trim() });
|
||||
setSelectedFamily(response.data);
|
||||
setEditingName(false);
|
||||
} catch (err) {
|
||||
console.error('Error updating family name:', err);
|
||||
alert(t('profile.renameError'));
|
||||
} finally {
|
||||
setSavingName(false);
|
||||
await authApi.logout();
|
||||
setUser(null);
|
||||
setSelectedFamily(null);
|
||||
navigate('/login');
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (user?.family_id) {
|
||||
navigate(`/family/${user.family_id}`);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
const handleThemeChange = (theme: Theme) => {
|
||||
setPreferences({ ...preferences, theme });
|
||||
showToast.success(t('profile.themeChanged'));
|
||||
};
|
||||
|
||||
const handleLocaleChange = (locale: string) => {
|
||||
i18n.changeLanguage(locale);
|
||||
setPreferences({ ...preferences, locale: locale as 'ru' | 'en' });
|
||||
showToast.success(t('profile.languageChanged'));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen gradient-bg py-8 sm:py-12 px-4">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-white/20 hover:bg-white/30 text-white rounded-2xl backdrop-blur-md mb-6 transition-all duration-300"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span className="font-medium">{t('common.back')}</span>
|
||||
</button>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<ProfileHeader onBack={() => navigate(user?.family_id ? `/family/${user.family_id}` : '/')} />
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex p-4 bg-white/20 backdrop-blur-md rounded-2xl mb-4">
|
||||
@@ -155,221 +96,63 @@ export default function Profile() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="glass-effect rounded-2xl shadow-lg p-6">
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 category-icon text-white rounded-xl">
|
||||
<div className="p-2 bg-gradient-to-br from-blue-500 to-blue-600 text-white rounded-xl">
|
||||
<UserIcon className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-800">{t('profile.info')}</h2>
|
||||
<h2 className="text-xl font-bold text-gray-800 dark:text-white">{t('profile.info')}</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-200">
|
||||
<span className="text-gray-600">{t('profile.username')}</span>
|
||||
<span className="font-medium text-gray-900">{user?.username || '-'}</span>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<span className="text-gray-600 dark:text-gray-400">{t('profile.username')}</span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{user?.username || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-200">
|
||||
<span className="text-gray-600">{t('profile.email')}</span>
|
||||
<span className="font-medium text-gray-900">{user?.email || '-'}</span>
|
||||
<div className="flex justify-between items-center py-2">
|
||||
<span className="text-gray-600 dark:text-gray-400">{t('profile.email')}</span>
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{user?.email || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{selectedFamily && (
|
||||
<div className="glass-effect rounded-2xl shadow-lg p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 btn-success text-white rounded-xl">
|
||||
<Users className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-800">{t('profile.family')}</h2>
|
||||
<Card>
|
||||
<FamilySection
|
||||
family={selectedFamily}
|
||||
onLeaveFamily={handleLeaveFamily}
|
||||
onFamilyUpdate={loadFamily}
|
||||
leavingFamily={leavingFamily}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<MembersSection
|
||||
members={members}
|
||||
currentUser={user}
|
||||
loading={membersLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-200">
|
||||
<span className="text-gray-600">{t('profile.familyName')}</span>
|
||||
{editingName ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newFamilyName}
|
||||
onChange={(e) => setNewFamilyName(e.target.value)}
|
||||
className="px-3 py-1 border border-gray-300 rounded-lg focus:border-purple-500 focus:ring-1 focus:ring-purple-200"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={handleSaveName}
|
||||
disabled={savingName}
|
||||
className="p-1.5 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"
|
||||
>
|
||||
{savingName ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingName(false)}
|
||||
className="p-1.5 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">{selectedFamily.name}</span>
|
||||
<button
|
||||
onClick={handleStartEditName}
|
||||
className="p-1.5 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2">
|
||||
<h3 className="text-sm font-medium text-gray-600 mb-3">{t('profile.members')}</h3>
|
||||
{membersLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-gray-400" />
|
||||
</div>
|
||||
) : familyMembers.length === 0 ? (
|
||||
<div className="text-center py-4 text-gray-500 text-sm">
|
||||
{t('profile.noMembers') || 'Нет участников'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{familyMembers.map((member) => (
|
||||
<div
|
||||
key={member.id}
|
||||
className={`flex items-center justify-between p-3 rounded-xl ${member.id === user?.id ? 'member-current border' : 'bg-gray-50'}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 category-icon rounded-full flex items-center justify-center text-white text-sm font-medium">
|
||||
{(member.username || member.email || '?')[0].toUpperCase()}
|
||||
</div>
|
||||
<span className="font-medium text-gray-800">
|
||||
{member.username || member.email || t('profile.unknownUser')}
|
||||
</span>
|
||||
{member.id === user?.id && (
|
||||
<span className="text-xs bg-purple-200 text-purple-700 px-2 py-0.5 rounded-full">
|
||||
{t('profile.you')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{member.is_admin && (
|
||||
<span className="text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full">
|
||||
Admin
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="glass-effect rounded-2xl shadow-lg p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 btn-primary text-white rounded-xl">
|
||||
<Settings className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-800">{t('profile.settings')}</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Languages className="w-4 h-4 text-gray-600" />
|
||||
<h3 className="text-sm font-medium text-gray-600">{t('profile.language')}</h3>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => handleLocaleChange('ru')}
|
||||
className={`flex-1 py-3 px-4 rounded-xl font-medium transition-all ${
|
||||
preferences.locale === 'ru'
|
||||
? 'btn-primary text-white shadow-lg'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<span className="mr-2">🇷🇺</span>
|
||||
Русский
|
||||
{preferences.locale === 'ru' && <Check className="w-4 h-4 inline ml-2" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleLocaleChange('en')}
|
||||
className={`flex-1 py-3 px-4 rounded-xl font-medium transition-all ${
|
||||
preferences.locale === 'en'
|
||||
? 'btn-primary text-white shadow-lg'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<span className="mr-2">🇬🇧</span>
|
||||
English
|
||||
{preferences.locale === 'en' && <Check className="w-4 h-4 inline ml-2" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Palette className="w-4 h-4 text-gray-600" />
|
||||
<h3 className="text-sm font-medium text-gray-600">{t('profile.theme')}</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{THEMES.map((theme) => (
|
||||
<button
|
||||
key={theme.id}
|
||||
onClick={() => handleThemeChange(theme.id)}
|
||||
className={`relative p-1 rounded-xl transition-all ${
|
||||
preferences.theme === theme.id
|
||||
? 'ring-2 ring-purple-500 ring-offset-2'
|
||||
: 'hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<div className={`h-12 rounded-lg ${theme.gradient}`} />
|
||||
<span className="text-xs text-gray-600 mt-1 block">{t(`profile.themes.${theme.id}`)}</span>
|
||||
{preferences.theme === theme.id && (
|
||||
<div className="absolute top-2 right-2 w-5 h-5 bg-white rounded-full flex items-center justify-center shadow">
|
||||
<Check className="w-3 h-3 text-purple-600" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedFamily && (
|
||||
<div className="glass-effect rounded-2xl shadow-lg p-6 border-2 border-red-200">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="p-2 btn-danger text-white rounded-xl">
|
||||
<AlertTriangle className="w-6 h-6" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-800">{t('profile.dangerZone')}</h2>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-600 mb-4">{t('profile.leaveDescription')}</p>
|
||||
|
||||
<button
|
||||
onClick={handleLeaveFamily}
|
||||
disabled={leavingFamily}
|
||||
className="w-full flex items-center justify-center gap-2 px-6 py-3 btn-danger text-white rounded-xl transition-all font-semibold disabled:opacity-50"
|
||||
>
|
||||
{leavingFamily ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
{t('profile.leaving')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LogOut className="w-5 h-5" />
|
||||
{t('profile.leaveFamily')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<SettingsSection
|
||||
currentTheme={preferences.theme}
|
||||
currentLanguage={preferences.locale}
|
||||
onThemeChange={handleThemeChange}
|
||||
onLanguageChange={handleLocaleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={confirmState.isOpen}
|
||||
title={confirmState.title}
|
||||
message={confirmState.message}
|
||||
onConfirm={confirmState.onConfirm}
|
||||
onCancel={cancel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user