856 lines
31 KiB
TypeScript
856 lines
31 KiB
TypeScript
'use client';
|
||
|
||
/**
|
||
* Панель выхода из LiveKit (выезжает справа, как на странице ДЗ):
|
||
* 1) Выйти | Выйти и завершить занятие
|
||
* 2) Если «завершить» — Выдать ДЗ? Да | Нет | Позже
|
||
* 3) Если «Да» — форма: текст + файлы, кнопки Позже | Сохранить
|
||
*/
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { completeLesson, uploadLessonFile } from '@/api/schedule';
|
||
import { createHomework } from '@/api/homework';
|
||
|
||
const MAX_LESSON_FILES = 10;
|
||
const MAX_FILE_SIZE_MB = 10;
|
||
|
||
function formatSize(bytes: number): string {
|
||
if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} МБ`;
|
||
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} КБ`;
|
||
return `${bytes} Б`;
|
||
}
|
||
|
||
type FileKind = 'image' | 'video' | 'pdf' | 'other';
|
||
|
||
function getFileKind(file: File): FileKind {
|
||
const t = file.type?.toLowerCase() ?? '';
|
||
const name = file.name.toLowerCase();
|
||
if (t.startsWith('image/') || /\.(jpe?g|png|gif|webp|bmp|svg|ico)$/i.test(name)) return 'image';
|
||
if (t.startsWith('video/') || /\.(mp4|webm|ogg|mov|avi|mkv)$/i.test(name)) return 'video';
|
||
if (t === 'application/pdf' || name.endsWith('.pdf')) return 'pdf';
|
||
return 'other';
|
||
}
|
||
|
||
function FilePreviewChip({ file, onRemove, disabled }: { file: File; onRemove: () => void; disabled?: boolean }) {
|
||
const kind = getFileKind(file);
|
||
const [objectUrl, setObjectUrl] = useState<string | null>(null);
|
||
const name = file.name.length > 28 ? file.name.slice(0, 25) + '…' : file.name;
|
||
|
||
useEffect(() => {
|
||
if (kind === 'image' || kind === 'video' || kind === 'pdf') {
|
||
const url = URL.createObjectURL(file);
|
||
setObjectUrl(url);
|
||
return () => URL.revokeObjectURL(url);
|
||
}
|
||
}, [file, kind]);
|
||
|
||
const previewBlock = (() => {
|
||
if (kind === 'image' && objectUrl) {
|
||
return (
|
||
<img
|
||
src={objectUrl}
|
||
alt=""
|
||
style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 8 }}
|
||
/>
|
||
);
|
||
}
|
||
if (kind === 'video' && objectUrl) {
|
||
return (
|
||
<video
|
||
src={objectUrl}
|
||
muted
|
||
playsInline
|
||
preload="metadata"
|
||
style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 8 }}
|
||
/>
|
||
);
|
||
}
|
||
if (kind === 'pdf' && objectUrl) {
|
||
return (
|
||
<iframe
|
||
src={objectUrl}
|
||
title={file.name}
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
border: 'none',
|
||
borderRadius: 8,
|
||
minHeight: 140,
|
||
}}
|
||
/>
|
||
);
|
||
}
|
||
return (
|
||
<div
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
minHeight: 60,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
borderRadius: 8,
|
||
color: 'var(--md-sys-color-primary)',
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined" style={{ fontSize: 28 }}>description</span>
|
||
</div>
|
||
);
|
||
})();
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--md-sys-color-outline-variant)',
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
<div style={{ aspectRatio: '4/3', minHeight: 80, maxHeight: 140 }}>
|
||
{previewBlock}
|
||
</div>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
padding: '8px 12px',
|
||
}}
|
||
>
|
||
<div style={{ minWidth: 0, flex: 1 }}>
|
||
<div style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{name}
|
||
</div>
|
||
<div style={{ fontSize: 11, color: 'var(--md-sys-color-on-surface-variant)' }}>{formatSize(file.size)}</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={onRemove}
|
||
disabled={disabled}
|
||
aria-label="Удалить"
|
||
style={{
|
||
width: 28,
|
||
height: 28,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
borderRadius: 8,
|
||
border: 'none',
|
||
background: 'rgba(186,26,26,0.15)',
|
||
color: 'var(--md-sys-color-error)',
|
||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined" style={{ fontSize: 16 }}>close</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export type ExitStep = 'choose_exit' | 'grades' | 'comment' | 'choose_hw' | 'hw_form';
|
||
|
||
export interface ExitLessonModalProps {
|
||
isOpen: boolean;
|
||
/** Если null, показывается только «Выйти» (без завершения занятия). */
|
||
lessonId: number | null;
|
||
onClose: () => void;
|
||
/** Вызвать после выхода (отключиться от комнаты и уйти). */
|
||
onExit: () => void;
|
||
}
|
||
|
||
export function ExitLessonModal({ isOpen, lessonId, onClose, onExit }: ExitLessonModalProps) {
|
||
const [step, setStep] = useState<ExitStep>('choose_exit');
|
||
const [formData, setFormData] = useState({
|
||
mentor_grade: '',
|
||
school_grade: '',
|
||
mentor_notes: '',
|
||
});
|
||
const [homeworkText, setHomeworkText] = useState('');
|
||
const [files, setFiles] = useState<File[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const [mounted, setMounted] = useState(false);
|
||
const [visible, setVisible] = useState(false);
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
setMounted(true);
|
||
const t = requestAnimationFrame(() => {
|
||
requestAnimationFrame(() => setVisible(true));
|
||
});
|
||
return () => cancelAnimationFrame(t);
|
||
} else {
|
||
setVisible(false);
|
||
}
|
||
}, [isOpen]);
|
||
|
||
const reset = () => {
|
||
setStep('choose_exit');
|
||
setFormData({ mentor_grade: '', school_grade: '', mentor_notes: '' });
|
||
setHomeworkText('');
|
||
setFiles([]);
|
||
setError(null);
|
||
};
|
||
|
||
const handleClose = () => {
|
||
if (!loading) {
|
||
reset();
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
const handleTransitionEnd = (e: React.TransitionEvent) => {
|
||
if (e.propertyName !== 'transform') return;
|
||
if (!isOpen) setMounted(false);
|
||
};
|
||
|
||
const PANEL_WIDTH = 420;
|
||
if (!mounted) return null;
|
||
|
||
/** Просто выйти из комнаты (без завершения занятия). */
|
||
const handleJustExit = () => {
|
||
reset();
|
||
onClose();
|
||
onExit();
|
||
};
|
||
|
||
const notes = (formData.mentor_notes || '').trim();
|
||
const mentorGrade = formData.mentor_grade ? parseInt(formData.mentor_grade, 10) : undefined;
|
||
const schoolGrade = formData.school_grade ? parseInt(formData.school_grade, 10) : undefined;
|
||
|
||
/** Завершить занятие без ДЗ и выйти. */
|
||
const handleCompleteNoHw = async () => {
|
||
if (lessonId == null) return;
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const result = await completeLesson(String(lessonId), notes, mentorGrade, schoolGrade, undefined);
|
||
if (!result?.success) {
|
||
setError(result?.message || 'Не удалось завершить занятие.');
|
||
return;
|
||
}
|
||
reset();
|
||
onClose();
|
||
onExit();
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : 'Не удалось завершить занятие');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
/** Завершить занятие, создать черновик ДЗ «заполнить позже» и выйти. */
|
||
const handleCompleteHwLater = async () => {
|
||
if (lessonId == null) return;
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const completeResult = await completeLesson(String(lessonId), notes, mentorGrade, schoolGrade, undefined);
|
||
if (!completeResult?.success) {
|
||
setError(completeResult?.message || 'Не удалось завершить занятие.');
|
||
return;
|
||
}
|
||
await createHomework({
|
||
title: 'ДЗ (заполнить позже)',
|
||
description: '',
|
||
lesson_id: lessonId,
|
||
status: 'draft',
|
||
fill_later: true,
|
||
});
|
||
reset();
|
||
onClose();
|
||
onExit();
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : 'Не удалось завершить занятие');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
/** Сохранить ДЗ (текст + файлы), завершить занятие и выйти.
|
||
* Порядок: 1) загружаем файлы к уроку, 2) создаём ДЗ через API homeworks, 3) после успеха — complete урока.
|
||
* В complete передаём только ID только что загруженных файлов, чтобы в ДЗ попали только они (не старые). */
|
||
const handleSaveHw = async () => {
|
||
if (lessonId == null) return;
|
||
const text = homeworkText.trim();
|
||
if (!text && files.length === 0) {
|
||
setError('Добавьте текст или прикрепите файлы');
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const lessonFileIds: number[] = [];
|
||
for (const file of files) {
|
||
const created = await uploadLessonFile(lessonId, file);
|
||
const id = typeof created.id === 'number' ? created.id : parseInt(String(created.id), 10);
|
||
if (!isNaN(id)) lessonFileIds.push(id);
|
||
}
|
||
await createHomework({
|
||
title: 'Домашнее задание',
|
||
description: text || '',
|
||
lesson_id: lessonId,
|
||
status: 'published',
|
||
});
|
||
const result = await completeLesson(
|
||
String(lessonId),
|
||
notes,
|
||
mentorGrade,
|
||
schoolGrade,
|
||
text || undefined,
|
||
true,
|
||
lessonFileIds
|
||
);
|
||
if (!result?.success) {
|
||
setError(result?.message || 'Занятие не удалось завершить.');
|
||
return;
|
||
}
|
||
reset();
|
||
onClose();
|
||
onExit();
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : 'Не удалось сохранить ДЗ и завершить занятие');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
/** В форме ДЗ нажать «Позже» — то же, что и выбор «Позже» на шаге выбора ДЗ. */
|
||
const handleHwFormLater = () => {
|
||
handleCompleteHwLater();
|
||
};
|
||
|
||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const list = e.target.files;
|
||
if (!list?.length) return;
|
||
const add: File[] = [];
|
||
for (let i = 0; i < list.length; i++) {
|
||
const f = list[i];
|
||
if (f.size > MAX_FILE_SIZE_MB * 1024 * 1024) continue;
|
||
add.push(f);
|
||
}
|
||
setFiles((prev) => {
|
||
const next = [...prev, ...add].slice(0, MAX_LESSON_FILES);
|
||
return next;
|
||
});
|
||
e.target.value = '';
|
||
};
|
||
|
||
const fileInputId = 'exit-lesson-file-input';
|
||
|
||
return (
|
||
<div
|
||
className="ios26-panel"
|
||
style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
width: PANEL_WIDTH,
|
||
background: 'var(--md-sys-color-surface)',
|
||
boxShadow: '-4px 0 24px rgba(0,0,0,0.15)',
|
||
overflow: 'hidden',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
zIndex: 10002,
|
||
transform: isOpen && visible ? 'translateX(0)' : 'translateX(100%)',
|
||
transition: 'transform 0.3s ease',
|
||
}}
|
||
onTransitionEnd={handleTransitionEnd}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
padding: '20px 24px',
|
||
borderBottom: '1px solid var(--md-sys-color-outline-variant)',
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<h2 style={{ fontSize: 20, fontWeight: 600, margin: 0, color: 'var(--md-sys-color-on-surface)' }}>
|
||
{step === 'choose_exit' && 'Выйти из занятия'}
|
||
{step === 'grades' && 'Оценки'}
|
||
{step === 'comment' && 'Комментарий'}
|
||
{step === 'choose_hw' && 'Завершить занятие'}
|
||
{step === 'hw_form' && 'Домашнее задание'}
|
||
</h2>
|
||
<button
|
||
type="button"
|
||
onClick={handleClose}
|
||
disabled={loading}
|
||
style={{
|
||
width: 44,
|
||
height: 44,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'none',
|
||
border: 'none',
|
||
borderRadius: 12,
|
||
cursor: 'pointer',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined" style={{ fontSize: 24 }}>close</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div style={{ padding: '24px 28px', overflowY: 'auto', flex: 1 }}>
|
||
{step === 'choose_exit' && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||
<button
|
||
type="button"
|
||
onClick={handleJustExit}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 16,
|
||
fontWeight: 500,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
textAlign: 'left',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined">logout</span>
|
||
Выйти
|
||
</button>
|
||
{lessonId != null && (
|
||
<button
|
||
type="button"
|
||
onClick={() => setStep('grades')}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: 'none',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
fontSize: 16,
|
||
fontWeight: 600,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
textAlign: 'left',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined">check_circle</span>
|
||
Выйти и завершить занятие
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{step === 'grades' && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
<p style={{ margin: '0 0 8px', fontSize: 14, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Необязательно
|
||
</p>
|
||
<div>
|
||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Оценка за занятие (1–5)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min={1}
|
||
max={5}
|
||
value={formData.mentor_grade}
|
||
onChange={(e) => setFormData((d) => ({ ...d, mentor_grade: e.target.value }))}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
fontSize: 15,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
}}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label style={{ display: 'block', fontSize: 14, fontWeight: 500, marginBottom: 8, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Оценка в школе (1–5)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min={1}
|
||
max={5}
|
||
value={formData.school_grade}
|
||
onChange={(e) => setFormData((d) => ({ ...d, school_grade: e.target.value }))}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
fontSize: 15,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setStep('choose_exit')}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 15,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Назад
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setStep('comment')}
|
||
disabled={loading}
|
||
style={{
|
||
flex: 1,
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: 'none',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
fontSize: 15,
|
||
fontWeight: 600,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Далее
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{step === 'comment' && (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
<p style={{ margin: '0 0 8px', fontSize: 14, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Комментарий к занятию (необязательно)
|
||
</p>
|
||
<textarea
|
||
value={formData.mentor_notes}
|
||
onChange={(e) => setFormData((d) => ({ ...d, mentor_notes: e.target.value }))}
|
||
rows={5}
|
||
placeholder="Что прошли, успехи, рекомендации..."
|
||
disabled={loading}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
fontSize: 15,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
resize: 'vertical',
|
||
}}
|
||
/>
|
||
<div style={{ display: 'flex', gap: 12 }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setStep('grades')}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 15,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Назад
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setStep('choose_hw')}
|
||
disabled={loading}
|
||
style={{
|
||
flex: 1,
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: 'none',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
fontSize: 15,
|
||
fontWeight: 600,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Далее
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{step === 'choose_hw' && (
|
||
<>
|
||
<p style={{ margin: '0 0 16px', fontSize: 15, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Выдать домашнее задание?
|
||
</p>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setStep('comment')}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 16,
|
||
fontWeight: 500,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
textAlign: 'left',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined">arrow_back</span>
|
||
Назад
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setStep('hw_form')}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 16,
|
||
fontWeight: 500,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
textAlign: 'left',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined">assignment</span>
|
||
Да
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleCompleteNoHw}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 16,
|
||
fontWeight: 500,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
textAlign: 'left',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined">cancel</span>
|
||
Нет
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleCompleteHwLater}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 16,
|
||
fontWeight: 500,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
textAlign: 'left',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 12,
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined">schedule</span>
|
||
Позже (заполню на странице ДЗ)
|
||
</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{step === 'hw_form' && (
|
||
<>
|
||
<div style={{ marginBottom: 20 }}>
|
||
<label
|
||
htmlFor="exit-hw-text"
|
||
style={{
|
||
display: 'block',
|
||
fontSize: 14,
|
||
fontWeight: 600,
|
||
marginBottom: 8,
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
}}
|
||
>
|
||
Текст задания
|
||
</label>
|
||
<textarea
|
||
id="exit-hw-text"
|
||
value={homeworkText}
|
||
onChange={(e) => setHomeworkText(e.target.value)}
|
||
rows={4}
|
||
placeholder="Опишите домашнее задание..."
|
||
disabled={loading}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 16px',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface)',
|
||
fontSize: 15,
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
resize: 'vertical',
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ marginBottom: 20 }}>
|
||
<div style={{ fontSize: 14, fontWeight: 600, marginBottom: 8, color: 'var(--md-sys-color-on-surface-variant)' }}>
|
||
Файлы (до {MAX_FILE_SIZE_MB} МБ, не более {MAX_LESSON_FILES} шт.)
|
||
</div>
|
||
<input
|
||
id={fileInputId}
|
||
type="file"
|
||
multiple
|
||
onChange={handleFileChange}
|
||
disabled={loading}
|
||
accept="*"
|
||
style={{ position: 'absolute', width: 1, height: 1, opacity: 0, pointerEvents: 'none' }}
|
||
/>
|
||
<label
|
||
htmlFor={fileInputId}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: 10,
|
||
padding: '16px 20px',
|
||
borderRadius: 12,
|
||
border: '2px dashed var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
fontSize: 15,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
<span className="material-symbols-outlined" style={{ fontSize: 22 }}>upload_file</span>
|
||
Прикрепить файлы
|
||
</label>
|
||
{files.length > 0 && (
|
||
<div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||
{files.map((f, i) => (
|
||
<FilePreviewChip
|
||
key={`${f.name}-${i}-${f.size}`}
|
||
file={f}
|
||
onRemove={() => setFiles((p) => p.filter((_, j) => j !== i))}
|
||
disabled={loading}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
{error && (
|
||
<div
|
||
style={{
|
||
background: 'rgba(186,26,26,0.1)',
|
||
border: '1px solid var(--md-sys-color-error)',
|
||
borderRadius: 12,
|
||
padding: 12,
|
||
marginBottom: 16,
|
||
fontSize: 14,
|
||
color: 'var(--md-sys-color-error)',
|
||
}}
|
||
>
|
||
{error}
|
||
</div>
|
||
)}
|
||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => setStep('choose_hw')}
|
||
disabled={loading}
|
||
style={{
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'var(--md-sys-color-surface-variant)',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
fontSize: 15,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Назад
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleHwFormLater}
|
||
disabled={loading}
|
||
style={{
|
||
flex: 1,
|
||
minWidth: 100,
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: '1px solid var(--md-sys-color-outline)',
|
||
background: 'transparent',
|
||
color: 'var(--md-sys-color-on-surface-variant)',
|
||
fontSize: 15,
|
||
fontWeight: 500,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
}}
|
||
>
|
||
Позже
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={handleSaveHw}
|
||
disabled={loading}
|
||
style={{
|
||
flex: 1,
|
||
minWidth: 100,
|
||
padding: '14px 20px',
|
||
borderRadius: 14,
|
||
border: 'none',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
fontSize: 15,
|
||
fontWeight: 600,
|
||
cursor: loading ? 'not-allowed' : 'pointer',
|
||
opacity: loading ? 0.8 : 1,
|
||
}}
|
||
>
|
||
{loading ? 'Сохранение...' : 'Сохранить'}
|
||
</button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|