uchill/front_material/app/(protected)/profile/page.tsx

1029 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect, useCallback, Suspense, useRef } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import { useRouter, useSearchParams } from 'next/navigation';
import {
getProfileSettings,
updateProfileSettings,
updateProfile,
searchCitiesFromCSV,
loadTelegramAvatar,
type ProfileSettings,
type CityOption,
} from '@/api/profile';
import {
getNotificationPreferences,
updateNotificationPreferences,
type NotificationPreference,
} from '@/api/notifications';
import { getReferralStats } from '@/api/referrals';
import { formatPhoneForDisplay, parsePhoneFromInput } from '@/lib/phone-utils';
import { ProfilePaymentTab } from '@/components/profile/ProfilePaymentTab';
import { NotificationSettingsSection } from '@/components/profile/NotificationSettingsSection';
import { ParentChildNotificationSettings } from '@/components/profile/ParentChildNotificationSettings';
import { TelegramSection } from '@/components/profile/TelegramSection';
import { Switch } from '@/components/common/Switch';
function getAvatarUrl(user: { avatar_url?: string | null; avatar?: string | null } | null): string | null {
if (!user) return null;
const url = user.avatar_url || user.avatar;
if (!url) return null;
if (url.startsWith('http')) return url;
const base = typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8123` : '';
return url.startsWith('/') ? `${base}${url}` : `${base}/${url}`;
}
const ROLE_LABELS: Record<string, string> = {
mentor: 'Ментор',
client: 'Студент',
parent: 'Родитель',
};
/** Имя/фамилия: только латиница ИЛИ только кириллица, без смешивания. Алфавит задаётся первым символом. */
function filterNameInput(value: string): string {
if (!value.trim()) return value.replace(/[^a-zA-ZА-Яа-яЁё]/g, '');
const first = value[0];
if (/[a-zA-Z]/.test(first)) return value.replace(/[^a-zA-Z]/g, '');
if (/[А-Яа-яЁё]/.test(first)) return value.replace(/[^А-Яа-яЁё]/g, '');
return value.replace(/[^a-zA-ZА-Яа-яЁё]/g, '');
}
type TabId = 'profile' | 'payment';
function ProfilePage() {
const { user, loading: authLoading, logout, refreshUser } = useAuth();
const router = useRouter();
const searchParams = useSearchParams();
const tabParam = searchParams?.get('tab');
const activeTab: TabId = tabParam === 'payment' ? 'payment' : 'profile';
const avatarUrl = getAvatarUrl(user);
const [settings, setSettings] = useState<ProfileSettings | null>(null);
const [notificationPrefs, setNotificationPrefs] = useState<NotificationPreference | null>(null);
const [formData, setFormData] = useState({ first_name: '', last_name: '', phone: '', email: '' });
const [avatarFile, setAvatarFile] = useState<File | null>(null);
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
const [uploadingAvatar, setUploadingAvatar] = useState(false);
const [saving, setSaving] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [saveSuccess, setSaveSuccess] = useState(false);
const [citySearchQuery, setCitySearchQuery] = useState('');
const [citySearchResults, setCitySearchResults] = useState<CityOption[]>([]);
const [isCityInputFocused, setIsCityInputFocused] = useState(false);
const [isSearchingCities, setIsSearchingCities] = useState(false);
const [avatarHovered, setAvatarHovered] = useState(false);
const [loadingTelegramAvatar, setLoadingTelegramAvatar] = useState(false);
const [referralPoints, setReferralPoints] = useState<number | null>(null);
const cityInputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const saveSettingsAndNotificationsRef = useRef<() => Promise<void>>(() => Promise.resolve());
const settingsSaveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const loadProfile = useCallback(async () => {
if (!user) return;
setFormData({
first_name: user.first_name || '',
last_name: user.last_name || '',
phone: user.phone || '',
email: user.email || '',
});
try {
const [settingsRes, notifRes, refStats] = await Promise.all([
getProfileSettings().catch(() => null),
getNotificationPreferences(),
getReferralStats().catch(() => null),
]);
if (refStats?.total_points != null) setReferralPoints(refStats.total_points);
setSettings(settingsRes || null);
const defaultPrefs = {
enabled: true,
email_enabled: true,
telegram_enabled: false,
in_app_enabled: true,
};
setNotificationPrefs((prev) => notifRes ?? prev ?? defaultPrefs);
if (settingsRes) {
if (!settingsRes.preferences?.timezone) {
settingsRes.preferences = {
...settingsRes.preferences,
timezone: typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : 'Europe/Moscow',
};
}
if (settingsRes.preferences?.city) {
setCitySearchQuery(settingsRes.preferences.city);
}
}
} catch (e) {
setLoadError('Не удалось загрузить настройки');
}
}, [user]);
useEffect(() => {
if (tabParam === 'analytics') router.replace('/analytics');
if (tabParam === 'referrals') router.replace('/referrals');
}, [tabParam, router]);
const handleChangePreference = useCallback((field: 'city' | 'timezone', value: string) => {
setSettings((prev) => {
if (!prev) return prev;
return {
...prev,
preferences: { ...prev.preferences, [field]: value },
};
});
}, []);
const handleCitySearch = useCallback(async (query: string) => {
if (query.trim().length < 2) {
setCitySearchResults([]);
return;
}
setIsSearchingCities(true);
try {
const results = await searchCitiesFromCSV(query.trim(), 20);
setCitySearchResults(results);
} catch {
setCitySearchResults([]);
} finally {
setIsSearchingCities(false);
}
}, []);
useEffect(() => {
const t = setTimeout(() => {
if (citySearchQuery.trim().length >= 2) handleCitySearch(citySearchQuery);
else setCitySearchResults([]);
}, 300);
return () => clearTimeout(t);
}, [citySearchQuery, handleCitySearch]);
useEffect(() => {
loadProfile();
}, [loadProfile]);
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !user) return;
if (file.size > 5 * 1024 * 1024) {
setLoadError('Размер файла не должен превышать 5 МБ');
return;
}
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
setLoadError('Поддерживаются только форматы: JPEG, PNG, WebP, GIF');
return;
}
const objectUrl = URL.createObjectURL(file);
setAvatarPreview(objectUrl);
setAvatarFile(file);
setLoadError(null);
try {
setUploadingAvatar(true);
const updatedUser = await updateProfile({ avatar: file });
await refreshUser();
const newAvatarUrl = getAvatarUrl(updatedUser);
if (newAvatarUrl) {
URL.revokeObjectURL(objectUrl);
setAvatarPreview(newAvatarUrl);
setAvatarFile(null);
} else {
setAvatarPreview(objectUrl);
}
} catch (err) {
console.error(err);
setLoadError('Не удалось загрузить аватар. Нажмите «Сохранить» для повторной попытки.');
// objectUrl не отзываем — оставляем preview, avatarFile сохранён для кнопки «Сохранить»
} finally {
setUploadingAvatar(false);
e.target.value = '';
}
};
const handleAvatarDelete = async () => {
if (!user) return;
try {
await updateProfile({ avatar: null });
await refreshUser();
setAvatarFile(null);
setAvatarPreview(null);
} catch (err) {
console.error(err);
}
};
const handleLoadTelegramAvatar = async () => {
if (!user?.telegram_id) return;
try {
setLoadingTelegramAvatar(true);
setLoadError(null);
const updatedUser = await loadTelegramAvatar();
await refreshUser();
const newUrl = getAvatarUrl(updatedUser);
if (newUrl) {
setAvatarPreview(newUrl);
setAvatarFile(null);
}
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
} catch (err: unknown) {
const msg =
(err as { response?: { data?: { error?: string } } })?.response?.data?.error ||
(err instanceof Error ? err.message : 'Не удалось загрузить аватар из Telegram');
setLoadError(typeof msg === 'string' ? msg : 'Не удалось загрузить аватар из Telegram');
} finally {
setLoadingTelegramAvatar(false);
}
};
const saveProfileFields = useCallback(async () => {
if (!user) return;
try {
const updatedUser = await updateProfile({
first_name: formData.first_name,
last_name: formData.last_name,
phone: formData.phone,
email: formData.email,
});
if (updatedUser) {
setFormData({
first_name: updatedUser.first_name ?? formData.first_name,
last_name: updatedUser.last_name ?? formData.last_name,
phone: updatedUser.phone ?? formData.phone,
email: updatedUser.email ?? formData.email,
});
}
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
} catch (err) {
console.error(err);
setLoadError('Не удалось сохранить имя или телефон.');
}
}, [user, formData.first_name, formData.last_name, formData.phone]);
const saveSettingsAndNotifications = useCallback(async () => {
if (!user || !settings) return;
try {
setSaving(true);
setLoadError(null);
const prefsToSave = {
...settings,
preferences: {
...settings?.preferences,
city: citySearchQuery.trim() || settings?.preferences?.city,
},
...(user.role === 'mentor' && settings?.mentor_homework_ai !== undefined && {
mentor_homework_ai: settings.mentor_homework_ai,
}),
};
await updateProfileSettings(prefsToSave);
if (notificationPrefs) {
const updated = await updateNotificationPreferences(notificationPrefs);
if (updated) setNotificationPrefs(updated);
}
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
} catch (err) {
console.error(err);
setLoadError('Не удалось сохранить настройки.');
} finally {
setSaving(false);
}
}, [user, settings, citySearchQuery, notificationPrefs]);
saveSettingsAndNotificationsRef.current = saveSettingsAndNotifications;
const scheduleSettingsSave = useCallback(() => {
if (settingsSaveTimeoutRef.current) clearTimeout(settingsSaveTimeoutRef.current);
settingsSaveTimeoutRef.current = setTimeout(() => {
settingsSaveTimeoutRef.current = null;
saveSettingsAndNotificationsRef.current();
}, 800);
}, []);
useEffect(() => {
return () => {
if (settingsSaveTimeoutRef.current) clearTimeout(settingsSaveTimeoutRef.current);
};
}, []);
const handleProfileFieldBlur = useCallback(() => {
saveProfileFields();
}, [saveProfileFields]);
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
if (!user) return;
setSaving(true);
setLoadError(null);
setSaveSuccess(false);
try {
const updatedUser = await updateProfile({
first_name: formData.first_name,
last_name: formData.last_name,
phone: formData.phone,
email: formData.email,
...(avatarFile instanceof File && { avatar: avatarFile }),
});
if (updatedUser) {
setFormData({
first_name: updatedUser.first_name ?? formData.first_name,
last_name: updatedUser.last_name ?? formData.last_name,
phone: updatedUser.phone ?? formData.phone,
email: updatedUser.email ?? formData.email,
});
}
const prefsToSave = {
...settings,
preferences: {
...settings?.preferences,
city: citySearchQuery.trim() || settings?.preferences?.city,
},
...(user.role === 'mentor' && settings?.mentor_homework_ai !== undefined && {
mentor_homework_ai: settings.mentor_homework_ai,
}),
};
if (prefsToSave) await updateProfileSettings(prefsToSave);
if (notificationPrefs) {
const updatedPrefs = await updateNotificationPreferences(notificationPrefs);
if (updatedPrefs) setNotificationPrefs(updatedPrefs);
}
setAvatarFile(null);
setAvatarPreview(null);
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
} catch (err) {
console.error(err);
if (avatarFile instanceof File) {
setLoadError('Не удалось сохранить аватар. Проверьте формат и размер (до 5 МБ).');
}
} finally {
setSaving(false);
}
};
const handleLogout = () => {
logout();
router.push('/login');
};
if (authLoading || !user) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
<LoadingSpinner size="large" />
</div>
);
}
return (
<div
className="page-profile"
style={{
padding: 24,
position: 'relative',
overflow: 'hidden',
}}
>
<div
className="page-profile-grid"
style={{
display: 'grid',
gridTemplateColumns: 'minmax(0, 440px) 1fr',
gap: 24,
alignItems: 'start',
position: 'relative',
zIndex: 1,
}}
>
{/* Левая карточка — Мой профиль */}
<div
style={{
background: '#fff',
borderRadius: 20,
boxShadow: '0 4px 24px rgba(0,0,0,0.06)',
overflow: 'hidden',
}}
>
<form id="profile-form" onSubmit={handleSave}>
{/* Аватар — квадрат, заполняет пространство */}
<div
style={{
padding: 0,
aspectRatio: '1',
width: '100%',
overflow: 'hidden',
position: 'relative',
}}
>
<div
className="profile-avatar-container"
onMouseEnter={() => setAvatarHovered(true)}
onMouseLeave={() => setAvatarHovered(false)}
style={{
position: 'relative',
width: '100%',
aspectRatio: '1',
overflow: 'hidden',
background: 'var(--md-sys-color-surface-variant)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
fontSize: 'clamp(32px, 10vw, 64px)',
fontWeight: 600,
cursor: 'pointer',
}}
>
{avatarPreview || avatarUrl ? (
<>
<img
src={avatarPreview || avatarUrl || ''}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover', pointerEvents: 'none', opacity: uploadingAvatar ? 0.6 : 1 }}
/>
{uploadingAvatar && (
<div
style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(0,0,0,0.3)',
zIndex: 2,
}}
>
<div
style={{
width: 40,
height: 40,
border: '3px solid rgba(255,255,255,0.3)',
borderTopColor: '#fff',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
}}
/>
</div>
)}
</>
) : (
user.first_name?.charAt(0) || user.email?.charAt(0) || 'У'
)}
<div
className="profile-avatar-overlay"
style={{
position: 'absolute',
inset: 0,
zIndex: 1,
background: 'rgba(0,0,0,0.5)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
padding: 8,
opacity: avatarHovered ? 1 : 0,
transition: 'opacity 0.2s',
pointerEvents: avatarHovered ? 'auto' : 'none',
}}
>
<label
className="profile-avatar-overlay-btn"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
height: 50,
minHeight: 50,
padding: '0 10px',
borderRadius: 8,
background: 'rgba(255,255,255,0.95)',
color: '#282C32',
cursor: 'pointer',
fontSize: 12,
fontWeight: 500,
}}
title="Обновить фото"
>
<input
type="file"
accept="image/*"
onChange={handleAvatarChange}
style={{ display: 'none' }}
/>
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>edit</span>
Обновить
</label>
{user?.telegram_id && (
<button
type="button"
onClick={handleLoadTelegramAvatar}
disabled={loadingTelegramAvatar || uploadingAvatar}
className="profile-avatar-overlay-btn"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
height: 50,
minHeight: 50,
padding: '0 10px',
borderRadius: 8,
background: 'rgba(255,255,255,0.95)',
color: '#1976d2',
border: 'none',
cursor: loadingTelegramAvatar || uploadingAvatar ? 'not-allowed' : 'pointer',
fontSize: 12,
fontWeight: 500,
opacity: loadingTelegramAvatar || uploadingAvatar ? 0.7 : 1,
}}
title="Подгрузить из Telegram"
>
{loadingTelegramAvatar ? (
<span
style={{
width: 14,
height: 14,
border: '2px solid rgba(25,118,210,0.3)',
borderTopColor: '#1976d2',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
}}
/>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style={{ flexShrink: 0 }} aria-hidden>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.63-.2-1.12-.31-1.08-.66.02-.18.27-.36.74-.55 2.92-1.27 4.86-2.11 5.83-2.51 2.78-1.16 3.35-1.36 3.73-1.36.08 0 .27.02.39.12.1.08.13.19.14.27-.01.06.01.24 0 .38z" />
</svg>
)}
Telegram
</button>
)}
{(avatarPreview || avatarUrl) && (
<button
type="button"
onClick={handleAvatarDelete}
className="profile-avatar-overlay-btn"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
height: 50,
minHeight: 50,
padding: '0 10px',
borderRadius: 8,
background: 'rgba(255,255,255,0.95)',
color: '#BA1A1A',
border: 'none',
cursor: 'pointer',
fontSize: 12,
fontWeight: 500,
}}
title="Удалить фото"
>
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>delete</span>
Удалить
</button>
)}
</div>
</div>
</div>
<div style={{ padding: '16px 24px 0', display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
<p style={{ fontSize: 13, color: '#858585', margin: 0 }}>
{ROLE_LABELS[user.role] || user.role}
</p>
{referralPoints != null && (
<a
href="/referrals"
onClick={(e) => { e.preventDefault(); router.push('/referrals'); }}
style={{
fontSize: 13,
color: 'var(--md-sys-color-primary, #6750a4)',
textDecoration: 'none',
display: 'inline-flex',
alignItems: 'center',
gap: 4,
cursor: 'pointer',
}}
>
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>military_tech</span>
{referralPoints} баллов
</a>
)}
</div>
{/* Поля — 2 колонки */}
<div style={{ padding: '0 24px 24px 24px' }}>
<div className="page-profile-fields" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 16px' }}>
<div>
<label htmlFor="profile-first-name" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Имя</label>
<input
id="profile-first-name"
type="text"
value={formData.first_name}
onChange={(e) => setFormData((p) => ({ ...p, first_name: filterNameInput(e.target.value) }))}
onBlur={handleProfileFieldBlur}
placeholder="Имя"
style={{
width: '100%',
padding: '14px 16px',
borderRadius: 12,
border: '1px solid #E6E6E6',
background: '#FAFAFA',
fontSize: 15,
color: '#282C32',
boxSizing: 'border-box',
}}
/>
</div>
<div>
<label htmlFor="profile-last-name" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Фамилия</label>
<input
id="profile-last-name"
type="text"
value={formData.last_name}
onChange={(e) => setFormData((p) => ({ ...p, last_name: filterNameInput(e.target.value) }))}
onBlur={handleProfileFieldBlur}
placeholder="Фамилия"
style={{
width: '100%',
padding: '14px 16px',
borderRadius: 12,
border: '1px solid #E6E6E6',
background: '#FAFAFA',
fontSize: 15,
color: '#282C32',
boxSizing: 'border-box',
}}
/>
</div>
<div>
<label htmlFor="profile-phone" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Телефон</label>
<input
id="profile-phone"
type="tel"
inputMode="tel"
value={formatPhoneForDisplay(formData.phone)}
onChange={(e) => setFormData((p) => ({ ...p, phone: parsePhoneFromInput(e.target.value) }))}
onBlur={handleProfileFieldBlur}
placeholder="+7 (999) 123-45-67"
style={{
width: '100%',
padding: '14px 16px',
borderRadius: 12,
border: '1px solid #E6E6E6',
background: '#FAFAFA',
fontSize: 15,
color: '#282C32',
boxSizing: 'border-box',
}}
/>
</div>
<div>
<label htmlFor="profile-email" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Email</label>
<input
id="profile-email"
type="email"
value={formData.email}
onChange={(e) => setFormData((p) => ({ ...p, email: e.target.value.toLowerCase().trim() }))}
onBlur={handleProfileFieldBlur}
placeholder="Email"
style={{
width: '100%',
padding: '14px 16px',
borderRadius: 12,
border: '1px solid #E6E6E6',
background: formData.email?.endsWith('@platform.local') ? '#FFF9C4' : '#FAFAFA',
fontSize: 15,
color: '#282C32',
boxSizing: 'border-box',
}}
/>
{formData.email?.endsWith('@platform.local') && (
<p style={{ fontSize: 10, color: '#FBC02D', margin: '2px 0 0 0' }}>
Установите реальный Email для доступа к аккаунту
</p>
)}
</div>
{/* Город */}
<div style={{ position: 'relative' }}>
<label htmlFor="profile-city" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Город</label>
<input
id="profile-city"
ref={cityInputRef}
type="text"
value={citySearchQuery}
onChange={(e) => setCitySearchQuery(e.target.value)}
onFocus={() => {
setIsCityInputFocused(true);
if (citySearchQuery.trim().length >= 2) handleCitySearch(citySearchQuery);
}}
onBlur={() => setTimeout(() => setIsCityInputFocused(false), 200)}
placeholder="Введите город"
autoComplete="off"
style={{
width: '100%',
padding: '14px 16px',
borderRadius: 12,
border: '1px solid #E6E6E6',
background: '#FAFAFA',
fontSize: 15,
color: '#282C32',
boxSizing: 'border-box',
}}
/>
{isSearchingCities && (
<div style={{ position: 'absolute', right: 14, top: '50%', transform: 'translateY(-50%)' }}>
<div
style={{
width: 18,
height: 18,
border: '2px solid #E6E6E6',
borderTopColor: 'var(--md-sys-color-primary)',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
}}
/>
</div>
)}
{isCityInputFocused && citySearchResults.length > 0 && (
<div
ref={dropdownRef}
style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
marginTop: 4,
maxHeight: 200,
overflowY: 'auto',
background: '#fff',
borderRadius: 12,
border: '1px solid #E6E6E6',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
zIndex: 10,
}}
>
{citySearchResults.map((city, idx) => (
<button
key={`${city.name}-${idx}`}
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
setCitySearchQuery(city.name);
if (city.timezone) handleChangePreference('timezone', city.timezone);
handleChangePreference('city', city.name);
setIsCityInputFocused(false);
scheduleSettingsSave();
}}
style={{
width: '100%',
padding: '12px 16px',
textAlign: 'left',
border: 'none',
background: 'none',
cursor: 'pointer',
fontSize: 14,
color: '#282C32',
}}
>
<div>{city.name}</div>
{city.timezone && (
<div style={{ fontSize: 12, color: '#858585', marginTop: 2 }}>
Часовой пояс: {city.timezone}
</div>
)}
</button>
))}
</div>
)}
<p style={{ fontSize: 10, color: '#858585', margin: '2px 0 0 0' }}>
Выберите город для определения часового пояса
</p>
</div>
{/* Часовой пояс */}
<div>
<label htmlFor="profile-timezone" style={{ display: 'block', fontSize: 12, fontWeight: 500, color: '#858585', marginBottom: 4 }}>Часовой пояс</label>
<input
id="profile-timezone"
type="text"
value={settings?.preferences?.timezone ? `${settings.preferences.timezone}` : ''}
readOnly
disabled
placeholder="Определяется по городу"
style={{
width: '100%',
padding: '14px 16px',
borderRadius: 12,
border: '1px solid #E6E6E6',
background: '#F0F0F0',
fontSize: 15,
color: '#858585',
boxSizing: 'border-box',
}}
/>
<p style={{ fontSize: 10, color: '#858585', margin: '2px 0 0 0' }}>
Устанавливается автоматически по городу
</p>
</div>
</div>
<TelegramSection onLinkedChange={() => refreshUser()} />
{loadError && (
<p style={{ color: 'var(--md-sys-color-error)', fontSize: 13, marginBottom: 16 }}>{loadError}</p>
)}
<button
type="button"
onClick={handleLogout}
style={{
width: '100%',
marginTop: 12,
padding: '12px 24px',
borderRadius: 14,
border: '1px solid var(--md-sys-color-error)',
background: 'transparent',
color: 'var(--md-sys-color-error)',
fontSize: 14,
cursor: 'pointer',
}}
>
Выйти из аккаунта
</button>
</div>
</form>
</div>
{/* Правая часть — настройки уведомлений или контент таба */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
{activeTab === 'profile' && (
<div
style={{
background: '#fff',
borderRadius: 20,
boxShadow: '0 4px 24px rgba(0,0,0,0.06)',
padding: '20px',
}}
>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 24 }}>
<button
type="submit"
form="profile-form"
disabled={saving}
style={{
padding: '16px 32px',
borderRadius: 14,
border: 'none',
background: saveSuccess ? 'var(--md-sys-color-tertiary-container, #e8def8)' : 'var(--md-sys-color-primary)',
color: saveSuccess ? 'var(--md-sys-color-on-tertiary-container, #1d192b)' : 'var(--md-sys-color-on-primary)',
fontSize: 16,
fontWeight: 600,
cursor: saving ? 'not-allowed' : 'pointer',
opacity: saving ? 0.7 : 1,
}}
>
{saving ? 'Сохранение...' : saveSuccess ? 'Профиль успешно обновлён' : 'Сохранить'}
</button>
</div>
<h2 style={{ fontSize: 18, fontWeight: 700, margin: '0 0 16px 0', color: '#282C32' }}>
Настройки уведомлений
</h2>
<NotificationSettingsSection
preferences={notificationPrefs}
onChange={(p) => {
setNotificationPrefs(p);
scheduleSettingsSave();
}}
userRole={user?.role}
hasTelegram={!!user?.telegram_id}
disabled={saving}
/>
<ParentChildNotificationSettings />
{user?.role === 'mentor' && (
<div style={{ marginTop: 24 }}>
<div
style={{
fontSize: 10,
fontWeight: 600,
letterSpacing: '0.05em',
color: 'var(--md-sys-color-on-surface-variant)',
marginBottom: 8,
}}
>
ПРОВЕРКА ДЗ ЧЕРЕЗ ИИ
</div>
<div
style={{
border: '1px solid var(--ios26-list-divider)',
borderRadius: 10,
overflow: 'hidden',
display: 'grid',
gridTemplateColumns: '1fr auto',
background: 'var(--md-sys-color-surface-container-low)',
}}
>
<div style={{ padding: 8, fontWeight: 600, fontSize: 11, color: 'var(--md-sys-color-on-surface-variant)' }}>Режим</div>
<div style={{ padding: 8, textAlign: 'center', fontWeight: 600, fontSize: 11, color: 'var(--md-sys-color-on-surface-variant)', minWidth: 64 }}>Вкл</div>
<div
style={{
padding: 8,
borderTop: '1px solid var(--ios26-list-divider)',
background: '#fff',
fontSize: 12,
}}
>
<div style={{ fontWeight: 500 }}>Доверять ИИ (черновик)</div>
<div style={{ fontSize: 10, color: 'var(--md-sys-color-on-surface-variant)', marginTop: 1 }}>
ИИ выставит оценку и комментарий и сохранит как черновик. Вы сможете отредактировать и опубликовать.
</div>
</div>
<div style={{ padding: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', borderTop: '1px solid var(--ios26-list-divider)', background: '#fff' }}>
<Switch
checked={!!settings?.mentor_homework_ai?.ai_trust_draft}
onChange={(v) => {
setSettings((prev) => ({
...prev!,
mentor_homework_ai: {
...prev?.mentor_homework_ai,
ai_trust_draft: v,
ai_trust_publish: v ? false : (prev?.mentor_homework_ai?.ai_trust_publish ?? false),
},
}));
scheduleSettingsSave();
}}
disabled={saving}
size="compact"
/>
</div>
<div
style={{
padding: 8,
borderTop: '1px solid var(--ios26-list-divider)',
background: '#fff',
fontSize: 12,
}}
>
<div style={{ fontWeight: 500 }}>Полностью доверять ИИ</div>
<div style={{ fontSize: 10, color: 'var(--md-sys-color-on-surface-variant)', marginTop: 1 }}>
ИИ выставит оценку и комментарий и опубликует результат студенту сразу после сдачи ДЗ.
</div>
</div>
<div style={{ padding: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', borderTop: '1px solid var(--ios26-list-divider)', background: '#fff' }}>
<Switch
checked={!!settings?.mentor_homework_ai?.ai_trust_publish}
onChange={(v) => {
setSettings((prev) => ({
...prev!,
mentor_homework_ai: {
...prev?.mentor_homework_ai,
ai_trust_publish: v,
ai_trust_draft: v ? false : (prev?.mentor_homework_ai?.ai_trust_draft ?? false),
},
}));
scheduleSettingsSave();
}}
disabled={saving}
size="compact"
/>
</div>
</div>
</div>
)}
</div>
)}
{activeTab !== 'profile' && (
<div
style={{
background: '#fff',
borderRadius: 20,
boxShadow: '0 4px 24px rgba(0,0,0,0.06)',
padding: 24,
minHeight: 400,
}}
>
{activeTab === 'payment' && <ProfilePaymentTab />}
</div>
)}
</div>
</div>
</div>
);
}
export default function ProfilePageWrapper() {
return (
<Suspense fallback={<LoadingSpinner size="large" />}>
<ProfilePage />
</Suspense>
);
}