277 lines
8.6 KiB
TypeScript
277 lines
8.6 KiB
TypeScript
/**
|
||
* Карточка урока для Dashboard
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import React, { useState, useEffect, useCallback } from 'react';
|
||
import { useRouter } from 'next/navigation';
|
||
import { LessonPreview } from '@/api/dashboard';
|
||
import { createLiveKitRoom } from '@/api/livekit';
|
||
|
||
interface LessonCardProps {
|
||
lesson: LessonPreview;
|
||
showMentor?: boolean;
|
||
showClient?: boolean;
|
||
onClick?: () => void;
|
||
}
|
||
|
||
/** Подключение доступно за 10 минут до начала и до 15 минут после окончания */
|
||
function canJoinLesson(lesson: LessonPreview): boolean {
|
||
if (!lesson.start_time || !lesson.end_time) return false;
|
||
if (lesson.status === 'cancelled') return false;
|
||
const now = new Date();
|
||
const startTime = new Date(lesson.start_time);
|
||
const endTime = new Date(lesson.end_time);
|
||
const allowedStart = new Date(startTime.getTime() - 10 * 60 * 1000); // за 10 минут до начала
|
||
const allowedEnd = new Date(endTime.getTime() + 15 * 60 * 1000); // до 15 минут после окончания
|
||
return now >= allowedStart && now <= allowedEnd;
|
||
}
|
||
|
||
export const LessonCard: React.FC<LessonCardProps> = ({
|
||
lesson,
|
||
showMentor = false,
|
||
showClient = false,
|
||
onClick,
|
||
}) => {
|
||
const router = useRouter();
|
||
const [connectLoading, setConnectLoading] = useState(false);
|
||
const [canJoin, setCanJoin] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const check = () => setCanJoin(canJoinLesson(lesson));
|
||
check();
|
||
const interval = setInterval(check, 60000);
|
||
return () => clearInterval(interval);
|
||
}, [lesson.start_time, lesson.end_time, lesson.status]);
|
||
|
||
const handleConnect = useCallback(
|
||
async (e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
if (!canJoin || connectLoading) return;
|
||
setConnectLoading(true);
|
||
try {
|
||
const lessonId = typeof lesson.id === 'string' ? parseInt(lesson.id, 10) : lesson.id;
|
||
const res = await createLiveKitRoom(lessonId);
|
||
router.push(
|
||
`/livekit/${res.room_name}?lesson_id=${lesson.id}&token=${encodeURIComponent(res.access_token)}`
|
||
);
|
||
} catch (err) {
|
||
console.error('LiveKit connect error:', err);
|
||
setConnectLoading(false);
|
||
}
|
||
},
|
||
[canJoin, connectLoading, lesson.id, router]
|
||
);
|
||
|
||
const startTime = new Date(lesson.start_time);
|
||
const endTime = new Date(lesson.end_time);
|
||
|
||
const getStatusColor = (status: string) => {
|
||
switch (status) {
|
||
case 'completed':
|
||
// более серый фон для завершённых занятий
|
||
return 'color-mix(in srgb, var(--md-sys-color-surface-variant) 70%, #000 10%)';
|
||
case 'in_progress':
|
||
return 'var(--md-sys-color-tertiary)';
|
||
case 'cancelled':
|
||
return 'var(--md-sys-color-error)';
|
||
case 'scheduled':
|
||
default:
|
||
return 'var(--md-sys-color-primary)';
|
||
}
|
||
};
|
||
|
||
const getStatusTextColor = (status: string) => {
|
||
switch (status) {
|
||
case 'completed':
|
||
return 'var(--md-sys-color-on-surface-variant)';
|
||
case 'in_progress':
|
||
return 'var(--md-sys-color-on-tertiary)';
|
||
case 'cancelled':
|
||
return 'var(--md-sys-color-on-error)';
|
||
case 'scheduled':
|
||
default:
|
||
return 'var(--md-sys-color-on-primary)';
|
||
}
|
||
};
|
||
|
||
const getStatusText = (status: string) => {
|
||
switch (status) {
|
||
case 'completed':
|
||
return 'Завершено';
|
||
case 'in_progress':
|
||
return 'В процессе';
|
||
case 'cancelled':
|
||
return 'Отменено';
|
||
default:
|
||
return 'Запланировано';
|
||
}
|
||
};
|
||
|
||
const statusColor = getStatusColor(lesson.status);
|
||
const textColor = getStatusTextColor(lesson.status);
|
||
|
||
return (
|
||
<div
|
||
role={onClick ? 'button' : undefined}
|
||
tabIndex={onClick ? 0 : undefined}
|
||
onClick={onClick}
|
||
onKeyDown={onClick ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); } } : undefined}
|
||
style={{
|
||
background: statusColor,
|
||
borderRadius: '16px',
|
||
padding: '16px',
|
||
marginBottom: '12px',
|
||
transition: 'all 0.2s ease',
|
||
cursor: 'pointer',
|
||
position: 'relative',
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.15)';
|
||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||
}}
|
||
onMouseLeave={(e) => {
|
||
e.currentTarget.style.boxShadow = 'none';
|
||
e.currentTarget.style.transform = 'translateY(0)';
|
||
}}
|
||
>
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'flex-start',
|
||
marginBottom: '12px'
|
||
}}>
|
||
<div style={{ flex: 1 }}>
|
||
<h4 style={{
|
||
fontSize: '16px',
|
||
fontWeight: '500',
|
||
color: textColor,
|
||
margin: '0 0 4px 0'
|
||
}}>
|
||
{lesson.title}
|
||
</h4>
|
||
{lesson.subject && (
|
||
<p style={{
|
||
fontSize: '14px',
|
||
color: textColor,
|
||
margin: '0',
|
||
opacity: 0.9
|
||
}}>
|
||
{lesson.subject}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<span style={{
|
||
fontSize: '12px',
|
||
padding: '4px 10px',
|
||
borderRadius: '12px',
|
||
background: 'rgba(255, 255, 255, 0.25)',
|
||
color: textColor,
|
||
fontWeight: '500'
|
||
}}>
|
||
{getStatusText(lesson.status)}
|
||
</span>
|
||
</div>
|
||
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '16px',
|
||
fontSize: '14px',
|
||
color: textColor,
|
||
opacity: 0.9
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<circle cx="12" cy="12" r="10"></circle>
|
||
<polyline points="12 6 12 12 16 14"></polyline>
|
||
</svg>
|
||
<span>
|
||
{startTime.toLocaleDateString('ru-RU', {
|
||
day: 'numeric',
|
||
month: 'short'
|
||
})}
|
||
{' в '}
|
||
{startTime.toLocaleTimeString('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})}
|
||
{' - '}
|
||
{endTime.toLocaleTimeString('ru-RU', {
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{(showMentor && lesson.mentor) && (
|
||
<div style={{
|
||
marginTop: '12px',
|
||
paddingTop: '12px',
|
||
borderTop: `1px solid color-mix(in srgb, ${textColor} 40%, transparent)`,
|
||
fontSize: '14px',
|
||
color: textColor,
|
||
opacity: 0.9
|
||
}}>
|
||
Ментор: {lesson.mentor.first_name} {lesson.mentor.last_name}
|
||
</div>
|
||
)}
|
||
|
||
{(showClient && lesson.client) && (
|
||
<div style={{
|
||
marginTop: '12px',
|
||
paddingTop: '12px',
|
||
borderTop: `1px solid color-mix(in srgb, ${textColor} 40%, transparent)`,
|
||
fontSize: '14px',
|
||
color: textColor,
|
||
opacity: 0.9
|
||
}}>
|
||
Ученик: {lesson.client.first_name} {lesson.client.last_name}
|
||
</div>
|
||
)}
|
||
|
||
{canJoin && (lesson.status === 'scheduled' || lesson.status === 'in_progress') && (
|
||
<button
|
||
type="button"
|
||
onClick={handleConnect}
|
||
disabled={connectLoading}
|
||
style={{
|
||
marginTop: '12px',
|
||
width: '100%',
|
||
padding: '10px 16px',
|
||
borderRadius: '12px',
|
||
border: 'none',
|
||
background: 'rgba(255, 255, 255, 0.9)',
|
||
color: statusColor,
|
||
fontSize: '14px',
|
||
fontWeight: '600',
|
||
cursor: connectLoading ? 'wait' : 'pointer',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: '8px',
|
||
}}
|
||
>
|
||
{connectLoading ? (
|
||
<>
|
||
<span className="material-symbols-outlined" style={{ fontSize: 18, animation: 'spin 1s linear infinite' }}>
|
||
progress_activity
|
||
</span>
|
||
Подключение...
|
||
</>
|
||
) : (
|
||
<>
|
||
<span className="material-symbols-outlined" style={{ fontSize: 18 }}>
|
||
videocam
|
||
</span>
|
||
Подключиться к уроку
|
||
</>
|
||
)}
|
||
</button>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|