uchill/front_material/components/dashboard/StudentSelect.tsx

365 lines
13 KiB
TypeScript
Raw Permalink 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 React, { useState, useRef, useEffect } from 'react';
import { Student } from '@/api/students';
interface StudentSelectProps {
students: Student[];
value: string;
onChange: (value: string) => void;
disabled?: boolean;
required?: boolean;
}
export const StudentSelect: React.FC<StudentSelectProps> = ({
students,
value,
onChange,
disabled = false,
required = false,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const containerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
// Получаем выбранного студента
const selectedStudent = students.find(s => String(s.id) === value);
// Фильтруем студентов по поисковому запросу
const filteredStudents = students.filter(student => {
const fullName = `${student.user?.first_name || ''} ${student.user?.last_name || ''}`.toLowerCase();
const email = student.user?.email?.toLowerCase() || '';
const query = searchQuery.toLowerCase();
return fullName.includes(query) || email.includes(query);
});
// Закрываем dropdown при клике вне компонента
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
setSearchQuery('');
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
// Фокусируем поле поиска при открытии
setTimeout(() => searchInputRef.current?.focus(), 0);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
const handleSelect = (studentId: string) => {
onChange(studentId);
setIsOpen(false);
setSearchQuery('');
};
const getStudentDisplayName = (student: Student) => {
const name = `${student.user?.first_name || ''} ${student.user?.last_name || ''}`.trim();
return name || student.user?.email || 'Без имени';
};
const getAvatarUrl = (student: Student) => {
return student.user?.avatar || null;
};
const getInitials = (student: Student) => {
const firstName = student.user?.first_name || '';
const lastName = student.user?.last_name || '';
if (firstName && lastName) {
return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
}
if (firstName) return firstName.charAt(0).toUpperCase();
if (lastName) return lastName.charAt(0).toUpperCase();
return student.user?.email?.charAt(0).toUpperCase() || '?';
};
return (
<div ref={containerRef} style={{ position: 'relative', width: '100%' }}>
{/* Основная кнопка выбора */}
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
style={{
width: '100%',
padding: '12px 16px',
fontSize: '16px',
color: selectedStudent ? 'var(--md-sys-color-on-surface)' : 'var(--md-sys-color-on-surface-variant)',
background: 'var(--md-sys-color-surface)',
border: `1px solid ${isOpen ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-outline)'}`,
borderWidth: isOpen ? '2px' : '1px',
borderRadius: '4px',
fontFamily: 'inherit',
cursor: disabled ? 'not-allowed' : 'pointer',
outline: 'none',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '12px',
textAlign: 'left',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flex: 1, minWidth: 0 }}>
{selectedStudent ? (
<>
{/* Аватар */}
<div style={{
width: '32px',
height: '32px',
borderRadius: '50%',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: getAvatarUrl(selectedStudent)
? `url(${getAvatarUrl(selectedStudent)}) center/cover`
: 'var(--md-sys-color-primary-container)',
color: 'var(--md-sys-color-on-primary-container)',
fontSize: '14px',
fontWeight: '500',
}}>
{!getAvatarUrl(selectedStudent) && getInitials(selectedStudent)}
</div>
{/* Имя */}
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{getStudentDisplayName(selectedStudent)}
</span>
</>
) : (
<span>Выберите ученика</span>
)}
</div>
{/* Иконка стрелки */}
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
style={{
flexShrink: 0,
transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease',
}}
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
{/* Dropdown меню */}
{isOpen && (
<div style={{
position: 'absolute',
top: 'calc(100% + 4px)',
left: 0,
right: 0,
background: 'var(--md-sys-color-surface)',
border: '1px solid var(--md-sys-color-outline-variant)',
borderRadius: '12px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
zIndex: 1000,
maxHeight: '320px',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}>
{/* Поле поиска */}
<div style={{
padding: '12px',
borderBottom: '1px solid var(--md-sys-color-outline-variant)',
}}>
<div style={{
position: 'relative',
display: 'flex',
alignItems: 'center',
}}>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="var(--md-sys-color-on-surface-variant)"
strokeWidth="2"
style={{
position: 'absolute',
left: '12px',
pointerEvents: 'none',
}}
>
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Поиск студента..."
style={{
width: '100%',
padding: '8px 12px 8px 40px',
fontSize: '14px',
color: 'var(--md-sys-color-on-surface)',
background: 'var(--md-sys-color-surface-variant)',
border: 'none',
borderRadius: '8px',
fontFamily: 'inherit',
outline: 'none',
}}
/>
{searchQuery && (
<button
type="button"
onClick={() => setSearchQuery('')}
style={{
position: 'absolute',
right: '8px',
width: '24px',
height: '24px',
padding: 0,
background: 'transparent',
border: 'none',
borderRadius: '50%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
)}
</div>
</div>
{/* Список студентов */}
<div style={{
overflowY: 'auto',
flex: 1,
}}>
{filteredStudents.length === 0 ? (
<div style={{
padding: '24px',
textAlign: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
fontSize: '14px',
}}>
{searchQuery ? 'Студенты не найдены' : 'Нет доступных студентов'}
</div>
) : (
filteredStudents.map((student) => (
<button
key={student.id}
type="button"
onClick={() => handleSelect(String(student.id))}
style={{
width: '100%',
padding: '12px 16px',
background: value === String(student.id)
? 'var(--md-sys-color-secondary-container)'
: 'transparent',
border: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '12px',
transition: 'background 0.2s ease',
textAlign: 'left',
}}
onMouseEnter={(e) => {
if (value !== String(student.id)) {
e.currentTarget.style.background = 'var(--md-sys-color-surface-variant)';
}
}}
onMouseLeave={(e) => {
if (value !== String(student.id)) {
e.currentTarget.style.background = 'transparent';
}
}}
>
{/* Аватар */}
<div style={{
width: '40px',
height: '40px',
borderRadius: '50%',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: getAvatarUrl(student)
? `url(${getAvatarUrl(student)}) center/cover`
: 'var(--md-sys-color-primary-container)',
color: 'var(--md-sys-color-on-primary-container)',
fontSize: '16px',
fontWeight: '500',
}}>
{!getAvatarUrl(student) && getInitials(student)}
</div>
{/* Информация о студенте */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
fontSize: '14px',
fontWeight: '500',
color: 'var(--md-sys-color-on-surface)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}>
{getStudentDisplayName(student)}
</div>
{student.user?.email && (
<div style={{
fontSize: '12px',
color: 'var(--md-sys-color-on-surface-variant)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
marginTop: '2px',
}}>
{student.user.email}
</div>
)}
</div>
{/* Галочка для выбранного элемента */}
{value === String(student.id) && (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="var(--md-sys-color-primary)"
strokeWidth="2"
style={{ flexShrink: 0 }}
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
)}
</button>
))
)}
</div>
</div>
)}
</div>
);
};