902 lines
36 KiB
TypeScript
902 lines
36 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { useAuth } from '@/contexts/AuthContext';
|
||
import { useRouter } from 'next/navigation';
|
||
import { sendMentorshipRequest, getMyMentorshipRequests, getMyMentors, getMyInvitations, confirmInvitationAsStudent, rejectInvitationAsStudent, type MentorInvitation } from '@/api/students';
|
||
import { format } from 'date-fns';
|
||
import { ru } from 'date-fns/locale';
|
||
import { loadComponent } from '@/lib/material-components';
|
||
|
||
export default function RequestMentorPage() {
|
||
const { user, loading: authLoading } = useAuth();
|
||
const router = useRouter();
|
||
const [componentsLoaded, setComponentsLoaded] = useState(false);
|
||
const [mentorCode, setMentorCode] = useState('');
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [success, setSuccess] = useState(false);
|
||
const [addError, setAddError] = useState('');
|
||
const [showAddPanel, setShowAddPanel] = useState(false);
|
||
const [activeTab, setActiveTab] = useState<'connect' | 'awaiting' | 'invitations'>('connect');
|
||
const [myRequests, setMyRequests] = useState<Array<{
|
||
id: number;
|
||
status: string;
|
||
created_at: string | null;
|
||
updated_at: string | null;
|
||
mentor: { id: number; email: string; first_name: string; last_name: string; avatar_url?: string | null };
|
||
}>>([]);
|
||
const [loadingRequests, setLoadingRequests] = useState(false);
|
||
const [myMentors, setMyMentors] = useState<Array<{ id: number; email: string; first_name: string; last_name: string; avatar_url?: string | null }>>([]);
|
||
const [myInvitations, setMyInvitations] = useState<MentorInvitation[]>([]);
|
||
const [loadingInvitations, setLoadingInvitations] = useState(false);
|
||
const [confirmingId, setConfirmingId] = useState<number | null>(null);
|
||
const [rejectingId, setRejectingId] = useState<number | null>(null);
|
||
const [invitationError, setInvitationError] = useState<string | null>(null);
|
||
|
||
const loadData = useCallback(() => {
|
||
if (user?.role !== 'client') return;
|
||
setLoadingRequests(true);
|
||
setLoadingInvitations(true);
|
||
Promise.all([
|
||
getMyMentorshipRequests(),
|
||
getMyMentors(),
|
||
getMyInvitations(),
|
||
])
|
||
.then(([requests, mentors, invitations]) => {
|
||
setMyRequests(requests);
|
||
setMyMentors(mentors);
|
||
setMyInvitations(invitations);
|
||
setInvitationError(null);
|
||
})
|
||
.catch(() => {})
|
||
.finally(() => {
|
||
setLoadingRequests(false);
|
||
setLoadingInvitations(false);
|
||
});
|
||
}, [user?.role]);
|
||
|
||
useEffect(() => {
|
||
loadComponent('elevated-card').then(() => setComponentsLoaded(true)).catch(() => setComponentsLoaded(true));
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (user?.role !== 'client') {
|
||
router.replace('/dashboard');
|
||
return;
|
||
}
|
||
}, [user, router]);
|
||
|
||
useEffect(() => {
|
||
loadData();
|
||
}, [loadData]);
|
||
|
||
const handleSubmit = async () => {
|
||
setAddError('');
|
||
const code = mentorCode.trim().toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 8);
|
||
if (code.length !== 8) {
|
||
setAddError('Введите 8-символьный код (цифры и латинские буквы)');
|
||
return;
|
||
}
|
||
setSubmitting(true);
|
||
try {
|
||
await sendMentorshipRequest(code);
|
||
setSuccess(true);
|
||
setMentorCode('');
|
||
setActiveTab('awaiting'); // Показать вкладку с отправленным запросом
|
||
loadData();
|
||
} catch (err: any) {
|
||
setAddError(err?.response?.data?.error || 'Не удалось отправить запрос');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
if (authLoading || !user) {
|
||
return (
|
||
<div style={{ padding: 40, textAlign: 'center', minHeight: '50vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
Загрузка...
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (user?.role !== 'client') {
|
||
return null;
|
||
}
|
||
|
||
if (!componentsLoaded) {
|
||
return (
|
||
<div style={{ padding: 24, minHeight: '50vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
Загрузка...
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const pendingRequests = myRequests.filter((r) => r.status === 'pending_mentor' || r.status === 'pending');
|
||
const pendingInvitationsFromMentors = myInvitations.filter((i) =>
|
||
['pending_student', 'pending_parent', 'pending'].includes(i.status)
|
||
);
|
||
const rejectedRequests = myRequests.filter((r) => r.status === 'rejected');
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
padding: '24px',
|
||
}}
|
||
>
|
||
{/* Табы всегда видны — Менторы | Ожидают ответа (ваши запросы) | Входящие приглашения (от менторов) */}
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
gap: 4,
|
||
marginBottom: 24,
|
||
borderBottom: '1px solid var(--ios26-list-divider, rgba(0,0,0,0.12))',
|
||
paddingBottom: 0,
|
||
}}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveTab('connect')}
|
||
style={{
|
||
padding: '12px 20px',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'connect' ? '2px solid var(--md-sys-color-primary)' : '2px solid transparent',
|
||
background: 'transparent',
|
||
color: activeTab === 'connect' ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-on-surface-variant)',
|
||
fontSize: 16,
|
||
fontWeight: 600,
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
Менторы
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveTab('awaiting')}
|
||
style={{
|
||
padding: '12px 20px',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'awaiting' ? '2px solid var(--md-sys-color-primary)' : '2px solid transparent',
|
||
background: 'transparent',
|
||
color: activeTab === 'awaiting' ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-on-surface-variant)',
|
||
fontSize: 16,
|
||
fontWeight: 600,
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
Ожидают ответа {pendingRequests.length > 0 && `(${pendingRequests.length})`}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveTab('invitations')}
|
||
style={{
|
||
padding: '12px 20px',
|
||
border: 'none',
|
||
borderBottom: activeTab === 'invitations' ? '2px solid var(--md-sys-color-primary)' : '2px solid transparent',
|
||
background: 'transparent',
|
||
color: activeTab === 'invitations' ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-on-surface-variant)',
|
||
fontSize: 16,
|
||
fontWeight: 600,
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
Входящие приглашения {pendingInvitationsFromMentors.length > 0 && `(${pendingInvitationsFromMentors.length})`}
|
||
</button>
|
||
</div>
|
||
|
||
{loadingRequests || loadingInvitations ? (
|
||
<div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
|
||
Загрузка...
|
||
</div>
|
||
) : activeTab === 'invitations' ? (
|
||
pendingInvitationsFromMentors.length === 0 ? (
|
||
<md-elevated-card style={{ padding: 40, borderRadius: 20, textAlign: 'center' }}>
|
||
<p style={{ fontSize: 16, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Нет входящих приглашений
|
||
</p>
|
||
</md-elevated-card>
|
||
) : (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
{invitationError && (
|
||
<div
|
||
style={{
|
||
padding: 12,
|
||
borderRadius: 12,
|
||
background: 'var(--md-sys-color-error-container)',
|
||
color: 'var(--md-sys-color-on-error-container)',
|
||
fontSize: 14,
|
||
}}
|
||
>
|
||
{invitationError}
|
||
</div>
|
||
)}
|
||
<div
|
||
className="students-cards-grid"
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
|
||
gap: '16px',
|
||
alignItems: 'start',
|
||
}}
|
||
>
|
||
{pendingInvitationsFromMentors.map((inv) => {
|
||
const mentorName = [inv.mentor?.first_name, inv.mentor?.last_name].filter(Boolean).join(' ') || inv.mentor?.email || 'Ментор';
|
||
const initials = [inv.mentor?.first_name?.[0], inv.mentor?.last_name?.[0]].filter(Boolean).map((c) => c?.toUpperCase()).join('') || inv.mentor?.email?.[0]?.toUpperCase() || 'М';
|
||
return (
|
||
<md-elevated-card
|
||
key={inv.id}
|
||
style={{
|
||
borderRadius: 24,
|
||
overflow: 'hidden',
|
||
background: 'var(--md-sys-color-surface)',
|
||
boxShadow: '0 8px 24px rgba(0,0,0,0.08)',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
height: '300px',
|
||
width: '100%',
|
||
background: 'linear-gradient(135deg, rgba(116,68,253,0.18), rgba(255,255,255,0.5))',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 80,
|
||
height: 80,
|
||
borderRadius: '50%',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'rgba(103, 80, 164, 0.18)',
|
||
color: 'var(--md-sys-color-primary)',
|
||
fontSize: 32,
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
{initials}
|
||
</div>
|
||
</div>
|
||
<div style={{ padding: '14px 14px 16px' }}>
|
||
<div style={{ fontSize: 18, fontWeight: 700, marginBottom: 6 }}>{mentorName}</div>
|
||
<div style={{ fontSize: 15, color: 'var(--md-sys-color-on-surface-variant)', marginBottom: 12 }}>
|
||
{inv.mentor?.email}
|
||
</div>
|
||
{inv.created_at && (
|
||
<div style={{ fontSize: 12, color: 'var(--md-sys-color-outline)', marginBottom: 12 }}>
|
||
{format(new Date(inv.created_at), 'd MMM yyyy, HH:mm', { locale: ru })}
|
||
</div>
|
||
)}
|
||
{inv.status === 'pending_parent' ? (
|
||
<span
|
||
style={{
|
||
display: 'inline-block',
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
padding: '6px 12px',
|
||
borderRadius: 8,
|
||
background: 'var(--md-sys-color-tertiary-container)',
|
||
color: 'var(--md-sys-color-on-tertiary-container)',
|
||
}}
|
||
>
|
||
Ожидаем подтверждения родителя
|
||
</span>
|
||
) : (
|
||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||
<button
|
||
type="button"
|
||
disabled={confirmingId === inv.id || rejectingId === inv.id}
|
||
onClick={async () => {
|
||
setConfirmingId(inv.id);
|
||
try {
|
||
await confirmInvitationAsStudent(inv.id);
|
||
loadData();
|
||
} catch (err: any) {
|
||
const msg = err?.response?.data?.error || 'Ошибка';
|
||
setInvitationError(msg);
|
||
setTimeout(() => setInvitationError(null), 5000);
|
||
} finally {
|
||
setConfirmingId(null);
|
||
}
|
||
}}
|
||
style={{
|
||
padding: '8px 14px',
|
||
borderRadius: 12,
|
||
border: 'none',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
cursor: confirmingId === inv.id || rejectingId === inv.id ? 'not-allowed' : 'pointer',
|
||
flex: 1,
|
||
minWidth: 0,
|
||
}}
|
||
>
|
||
{confirmingId === inv.id ? '…' : 'Принять'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
disabled={confirmingId === inv.id || rejectingId === inv.id}
|
||
onClick={async () => {
|
||
setRejectingId(inv.id);
|
||
try {
|
||
await rejectInvitationAsStudent(inv.id);
|
||
loadData();
|
||
} catch (err: any) {
|
||
const msg = err?.response?.data?.error || 'Ошибка';
|
||
setInvitationError(msg);
|
||
setTimeout(() => setInvitationError(null), 5000);
|
||
} finally {
|
||
setRejectingId(null);
|
||
}
|
||
}}
|
||
style={{
|
||
padding: '8px 14px',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'transparent',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
cursor: confirmingId === inv.id || rejectingId === inv.id ? 'not-allowed' : 'pointer',
|
||
flex: 1,
|
||
minWidth: 0,
|
||
}}
|
||
>
|
||
{rejectingId === inv.id ? '…' : 'Отклонить'}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</md-elevated-card>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
) : activeTab === 'awaiting' ? (
|
||
pendingRequests.length === 0 ? (
|
||
<md-elevated-card style={{ padding: 40, borderRadius: 20, textAlign: 'center' }}>
|
||
<p style={{ fontSize: 16, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Нет запросов, ожидающих ответа
|
||
</p>
|
||
</md-elevated-card>
|
||
) : (
|
||
<div
|
||
className="students-cards-grid"
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
|
||
gap: '16px',
|
||
alignItems: 'start',
|
||
}}
|
||
>
|
||
{pendingRequests.map((r) => {
|
||
const mentorName = [r.mentor?.first_name, r.mentor?.last_name].filter(Boolean).join(' ') || r.mentor?.email || 'Ментор';
|
||
const initials = [r.mentor?.first_name?.[0], r.mentor?.last_name?.[0]].filter(Boolean).map((c) => c?.toUpperCase()).join('') || r.mentor?.email?.[0]?.toUpperCase() || 'М';
|
||
return (
|
||
<md-elevated-card
|
||
key={r.id}
|
||
style={{
|
||
borderRadius: '24px',
|
||
overflow: 'hidden',
|
||
background: 'var(--md-sys-color-surface)',
|
||
boxShadow: '0 8px 24px rgba(0,0,0,0.08)',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
height: '300px',
|
||
width: '100%',
|
||
background: 'linear-gradient(135deg, rgba(116,68,253,0.18), rgba(255,255,255,0.5))',
|
||
position: 'relative',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 80,
|
||
height: 80,
|
||
borderRadius: '50%',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'rgba(103, 80, 164, 0.18)',
|
||
color: 'var(--md-sys-color-primary)',
|
||
fontSize: 32,
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
{initials}
|
||
</div>
|
||
</div>
|
||
<div style={{ padding: '14px 14px 16px' }}>
|
||
<div
|
||
style={{
|
||
fontSize: '18px',
|
||
fontWeight: 700,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
lineHeight: 1.2,
|
||
marginBottom: 6,
|
||
display: '-webkit-box',
|
||
WebkitLineClamp: 2,
|
||
WebkitBoxOrient: 'vertical' as any,
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{mentorName}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: '15px',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
lineHeight: 1.2,
|
||
display: '-webkit-box',
|
||
WebkitLineClamp: 2,
|
||
WebkitBoxOrient: 'vertical' as any,
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{r.mentor?.email || '—'}
|
||
</div>
|
||
{r.created_at && (
|
||
<div style={{ fontSize: 12, color: 'var(--md-sys-color-outline)', marginTop: 6 }}>
|
||
{format(new Date(r.created_at), 'd MMM yyyy, HH:mm', { locale: ru })}
|
||
</div>
|
||
)}
|
||
<span
|
||
style={{
|
||
display: 'inline-block',
|
||
marginTop: 8,
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
padding: '4px 10px',
|
||
borderRadius: 8,
|
||
background: 'var(--md-sys-color-tertiary-container)',
|
||
color: 'var(--md-sys-color-on-tertiary-container)',
|
||
}}
|
||
>
|
||
Ожидает
|
||
</span>
|
||
</div>
|
||
</md-elevated-card>
|
||
);
|
||
})}
|
||
</div>
|
||
)
|
||
) : (
|
||
<div
|
||
className="students-cards-grid"
|
||
style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
|
||
gap: '16px',
|
||
alignItems: 'start',
|
||
}}
|
||
>
|
||
{/* Карточки подключённых менторов (из client.mentors — включая принятые приглашения) */}
|
||
{myMentors.map((m) => {
|
||
const mentorName = [m.first_name, m.last_name].filter(Boolean).join(' ') || m.email || 'Ментор';
|
||
const initials = [m.first_name?.[0], m.last_name?.[0]].filter(Boolean).map((c) => c?.toUpperCase()).join('') || m.email?.[0]?.toUpperCase() || 'М';
|
||
const avatarUrl = m.avatar_url || null;
|
||
const req = myRequests.find((r) => r.mentor?.id === m.id);
|
||
const statusLabel = 'Принято';
|
||
const statusBg = 'var(--md-sys-color-primary-container)';
|
||
const statusColor = 'var(--md-sys-color-on-primary-container)';
|
||
return (
|
||
<md-elevated-card
|
||
key={`mentor-${m.id}`}
|
||
style={{
|
||
borderRadius: '24px',
|
||
overflow: 'hidden',
|
||
background: 'var(--md-sys-color-surface)',
|
||
boxShadow: '0 8px 24px rgba(0,0,0,0.08)',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
height: '300px',
|
||
width: '100%',
|
||
background: avatarUrl ? 'transparent' : 'linear-gradient(135deg, rgba(116,68,253,0.18), rgba(255,255,255,0.5))',
|
||
position: 'relative',
|
||
}}
|
||
>
|
||
{avatarUrl ? (
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
src={avatarUrl}
|
||
alt={mentorName}
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
objectFit: 'cover',
|
||
display: 'block',
|
||
}}
|
||
/>
|
||
) : (
|
||
<div
|
||
style={{
|
||
position: 'absolute',
|
||
inset: 0,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'linear-gradient(135deg, rgba(116,68,253,0.18), rgba(255,255,255,0.5))',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 80,
|
||
height: 80,
|
||
borderRadius: '50%',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'rgba(103, 80, 164, 0.18)',
|
||
color: 'var(--md-sys-color-primary)',
|
||
fontSize: 32,
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
{initials}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div style={{ padding: '14px 14px 16px' }}>
|
||
<div
|
||
style={{
|
||
fontSize: '18px',
|
||
fontWeight: 700,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
lineHeight: 1.2,
|
||
marginBottom: 6,
|
||
display: '-webkit-box',
|
||
WebkitLineClamp: 2,
|
||
WebkitBoxOrient: 'vertical' as any,
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{mentorName}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: '15px',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
lineHeight: 1.2,
|
||
display: '-webkit-box',
|
||
WebkitLineClamp: 2,
|
||
WebkitBoxOrient: 'vertical' as any,
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{m.email || '—'}
|
||
</div>
|
||
{req?.created_at && (
|
||
<div style={{ fontSize: 12, color: 'var(--md-sys-color-outline)', marginTop: 6 }}>
|
||
{format(new Date(req.created_at), 'd MMM yyyy, HH:mm', { locale: ru })}
|
||
</div>
|
||
)}
|
||
<span
|
||
style={{
|
||
display: 'inline-block',
|
||
marginTop: 8,
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
padding: '4px 10px',
|
||
borderRadius: 8,
|
||
background: statusBg,
|
||
color: statusColor,
|
||
}}
|
||
>
|
||
{statusLabel}
|
||
</span>
|
||
</div>
|
||
</md-elevated-card>
|
||
);
|
||
})}
|
||
{/* Карточки отклонённых запросов */}
|
||
{rejectedRequests.map((r) => {
|
||
const mentorName = [r.mentor?.first_name, r.mentor?.last_name].filter(Boolean).join(' ') || r.mentor?.email || 'Ментор';
|
||
const initials = [r.mentor?.first_name?.[0], r.mentor?.last_name?.[0]].filter(Boolean).map((c) => c?.toUpperCase()).join('') || r.mentor?.email?.[0]?.toUpperCase() || 'М';
|
||
return (
|
||
<md-elevated-card
|
||
key={`rejected-${r.id}`}
|
||
style={{
|
||
borderRadius: '24px',
|
||
overflow: 'hidden',
|
||
background: 'var(--md-sys-color-surface)',
|
||
boxShadow: '0 8px 24px rgba(0,0,0,0.08)',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
height: '300px',
|
||
width: '100%',
|
||
background: 'linear-gradient(135deg, rgba(116,68,253,0.18), rgba(255,255,255,0.5))',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 80,
|
||
height: 80,
|
||
borderRadius: '50%',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'rgba(103, 80, 164, 0.18)',
|
||
color: 'var(--md-sys-color-primary)',
|
||
fontSize: 32,
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
{initials}
|
||
</div>
|
||
</div>
|
||
<div style={{ padding: '14px 14px 16px' }}>
|
||
<div style={{ fontSize: 18, fontWeight: 700, marginBottom: 6 }}>{mentorName}</div>
|
||
<div style={{ fontSize: 15, color: 'var(--md-sys-color-on-surface-variant)', marginBottom: 6 }}>{r.mentor?.email || '—'}</div>
|
||
{r.created_at && (
|
||
<div style={{ fontSize: 12, color: 'var(--md-sys-color-outline)', marginBottom: 8 }}>
|
||
{format(new Date(r.created_at), 'd MMM yyyy, HH:mm', { locale: ru })}
|
||
</div>
|
||
)}
|
||
<span
|
||
style={{
|
||
display: 'inline-block',
|
||
fontSize: 13,
|
||
fontWeight: 600,
|
||
padding: '4px 10px',
|
||
borderRadius: 8,
|
||
background: 'var(--md-sys-color-error-container)',
|
||
color: 'var(--md-sys-color-on-error-container)',
|
||
}}
|
||
>
|
||
Отклонено
|
||
</span>
|
||
</div>
|
||
</md-elevated-card>
|
||
);
|
||
})}
|
||
{/* Карточка «Подключиться к ментору» */}
|
||
<md-elevated-card
|
||
style={{
|
||
borderRadius: '24px',
|
||
overflow: 'hidden',
|
||
cursor: 'pointer',
|
||
background: 'var(--md-sys-color-surface-container-low)',
|
||
boxShadow: '0 8px 24px rgba(0,0,0,0.06)',
|
||
}}
|
||
onClick={() => {
|
||
setShowAddPanel(true);
|
||
setAddError('');
|
||
setSuccess(false);
|
||
setMentorCode('');
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
height: '300px',
|
||
width: '100%',
|
||
background: 'linear-gradient(135deg, rgba(116,68,253,0.18), rgba(255,255,255,0.5))',
|
||
position: 'relative',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 80,
|
||
height: 80,
|
||
borderRadius: '50%',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'rgba(103, 80, 164, 0.18)',
|
||
color: 'var(--md-sys-color-primary)',
|
||
fontSize: 40,
|
||
fontWeight: 600,
|
||
}}
|
||
>
|
||
+
|
||
</div>
|
||
</div>
|
||
<div style={{ padding: '14px 14px 16px', textAlign: 'center' }}>
|
||
<div
|
||
style={{
|
||
fontSize: 18,
|
||
fontWeight: 700,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
marginBottom: 6,
|
||
}}
|
||
>
|
||
Подключиться к ментору
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: 15,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
lineHeight: 1.2,
|
||
}}
|
||
>
|
||
Введите код ментора
|
||
</div>
|
||
</div>
|
||
</md-elevated-card>
|
||
</div>
|
||
)}
|
||
|
||
{/* Боковая панель с формой (как на странице студентов) */}
|
||
{showAddPanel && (
|
||
<div
|
||
className="students-side-panel"
|
||
style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
right: 0,
|
||
height: '100vh',
|
||
width: 'min(420px, 100vw)',
|
||
padding: '20px 20px 24px',
|
||
background: 'var(--md-sys-color-surface)',
|
||
boxShadow: '0 0 24px rgba(0,0,0,0.18)',
|
||
borderTopLeftRadius: 24,
|
||
borderBottomLeftRadius: 24,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 16,
|
||
zIndex: 40,
|
||
}}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<div>
|
||
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--md-sys-color-on-surface)', marginBottom: 4 }}>
|
||
Подключиться к ментору
|
||
</div>
|
||
<div style={{ fontSize: 16, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Введите 8-символьный код ментора
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setShowAddPanel(false);
|
||
setAddError('');
|
||
setSuccess(false);
|
||
}}
|
||
style={{
|
||
border: 'none',
|
||
background: 'transparent',
|
||
cursor: 'pointer',
|
||
fontSize: 18,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
}}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
{success ? (
|
||
<div
|
||
style={{
|
||
padding: 20,
|
||
borderRadius: 12,
|
||
background: 'var(--md-sys-color-primary-container)',
|
||
color: 'var(--md-sys-color-on-primary-container)',
|
||
fontSize: 16,
|
||
}}
|
||
>
|
||
Запрос отправлен. Ожидайте ответа ментора.
|
||
</div>
|
||
) : (
|
||
<>
|
||
<label style={{ fontSize: 14, fontWeight: 600, color: 'var(--md-sys-color-on-surface)' }}>
|
||
Код ментора
|
||
</label>
|
||
<input
|
||
type="text"
|
||
inputMode="text"
|
||
maxLength={8}
|
||
value={mentorCode}
|
||
onChange={(e) => {
|
||
const v = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 8);
|
||
setMentorCode(v);
|
||
setAddError('');
|
||
}}
|
||
placeholder="A1B2C3D4"
|
||
disabled={submitting}
|
||
style={{
|
||
width: '100%',
|
||
padding: '14px 16px',
|
||
borderRadius: 12,
|
||
border: `1px solid ${addError ? 'var(--md-sys-color-error)' : 'var(--md-sys-color-outline)'}`,
|
||
background: 'var(--md-sys-color-surface-container-low)',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 18,
|
||
letterSpacing: 4,
|
||
boxSizing: 'border-box',
|
||
}}
|
||
/>
|
||
{addError && (
|
||
<p style={{ fontSize: 14, color: 'var(--md-sys-color-error)' }}>{addError}</p>
|
||
)}
|
||
<p style={{ fontSize: 13, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Ментор получит уведомление и сможет принять или отклонить запрос.
|
||
</p>
|
||
<button
|
||
type="button"
|
||
disabled={submitting || mentorCode.length !== 8}
|
||
onClick={() => handleSubmit()}
|
||
style={{
|
||
padding: '12px 24px',
|
||
borderRadius: 12,
|
||
border: 'none',
|
||
background:
|
||
submitting || mentorCode.length !== 8
|
||
? 'var(--md-sys-color-surface-container-high)'
|
||
: 'var(--md-sys-color-primary)',
|
||
color:
|
||
submitting || mentorCode.length !== 8
|
||
? 'var(--md-sys-color-on-surface-variant)'
|
||
: 'var(--md-sys-color-on-primary)',
|
||
fontSize: 16,
|
||
fontWeight: 600,
|
||
cursor: submitting || mentorCode.length !== 8 ? 'not-allowed' : 'pointer',
|
||
opacity: submitting || mentorCode.length !== 8 ? 0.7 : 1,
|
||
}}
|
||
>
|
||
{submitting ? 'Отправка…' : 'Отправить запрос'}
|
||
</button>
|
||
</>
|
||
)}
|
||
{user?.universal_code && (
|
||
<div
|
||
style={{
|
||
marginTop: 24,
|
||
padding: 16,
|
||
borderRadius: 12,
|
||
background: 'var(--md-sys-color-primary)',
|
||
}}
|
||
>
|
||
<div style={{ fontSize: 13, color: '#fff', marginBottom: 8 }}>
|
||
Ваш 8-символьный код
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', marginBottom: 8 }}>
|
||
{(user.universal_code || '').split('').map((char, i) => (
|
||
<span
|
||
key={i}
|
||
style={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
padding: '8px 10px',
|
||
border: '1px solid #fff',
|
||
borderRadius: 6,
|
||
fontSize: 18,
|
||
fontWeight: 700,
|
||
color: '#fff',
|
||
}}
|
||
>
|
||
{char}
|
||
</span>
|
||
))}
|
||
</div>
|
||
<div style={{ fontSize: 12, color: '#fff', opacity: 0.9 }}>
|
||
Поделитесь кодом с ментором — он сможет добавить вас по приглашению
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|