uchill/front_material/components/dashboard/SubjectSelect.tsx

396 lines
15 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 React, { useState, useRef, useEffect } from 'react';
import { Subject, MentorSubject } from '@/api/subjects';
interface SubjectSelectProps {
subjects: Subject[];
mentorSubjects: MentorSubject[];
value: number | null;
onChange: (value: number | null) => void;
disabled?: boolean;
required?: boolean;
}
export const SubjectSelect: React.FC<SubjectSelectProps> = ({
subjects,
mentorSubjects,
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 allSubjects = [
...subjects.map(s => ({ ...s, type: 'standard' as const })),
...mentorSubjects.map(s => ({ ...s, type: 'mentor' as const })),
];
// Получаем выбранный предмет
const selectedSubject = allSubjects.find(s => s.id === value);
// Фильтруем предметы по поисковому запросу
const filteredSubjects = allSubjects.filter(subject => {
const name = subject.name.toLowerCase();
const query = searchQuery.toLowerCase();
return name.includes(query);
});
// Группируем предметы по типу
const standardSubjects = filteredSubjects.filter(s => s.type === 'standard');
const mentorSubjectsFiltered = filteredSubjects.filter(s => s.type === 'mentor');
// Закрываем 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 = (subjectId: number) => {
onChange(subjectId);
setIsOpen(false);
setSearchQuery('');
};
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: selectedSubject ? '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 }}>
{selectedSubject ? (
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{selectedSubject.name}
</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: '400px',
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,
}}>
{filteredSubjects.length === 0 ? (
<div style={{
padding: '24px',
textAlign: 'center',
color: 'var(--md-sys-color-on-surface-variant)',
fontSize: '14px',
}}>
{searchQuery ? 'Предметы не найдены' : 'Нет доступных предметов'}
</div>
) : (
<>
{/* Стандартные предметы */}
{standardSubjects.length > 0 && (
<>
{!searchQuery && (
<div style={{
padding: '8px 16px',
fontSize: '12px',
fontWeight: '500',
color: 'var(--md-sys-color-on-surface-variant)',
background: 'var(--md-sys-color-surface-variant)',
}}>
Стандартные предметы
</div>
)}
{standardSubjects.map((subject) => (
<button
key={`standard-${subject.id}`}
type="button"
onClick={() => handleSelect(subject.id)}
style={{
width: '100%',
padding: '12px 16px',
background: value === subject.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 !== subject.id) {
e.currentTarget.style.background = 'var(--md-sys-color-surface-variant)';
}
}}
onMouseLeave={(e) => {
if (value !== subject.id) {
e.currentTarget.style.background = 'transparent';
}
}}
>
<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',
}}>
{subject.name}
</div>
</div>
{value === subject.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>
))}
</>
)}
{/* Предметы ментора */}
{mentorSubjectsFiltered.length > 0 && (
<>
{!searchQuery && standardSubjects.length > 0 && (
<div style={{
padding: '8px 16px',
fontSize: '12px',
fontWeight: '500',
color: 'var(--md-sys-color-on-surface-variant)',
background: 'var(--md-sys-color-surface-variant)',
marginTop: '8px',
}}>
Мои предметы
</div>
)}
{mentorSubjectsFiltered.map((subject) => (
<button
key={`mentor-${subject.id}`}
type="button"
onClick={() => handleSelect(subject.id)}
style={{
width: '100%',
padding: '12px 16px',
background: value === subject.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 !== subject.id) {
e.currentTarget.style.background = 'var(--md-sys-color-surface-variant)';
}
}}
onMouseLeave={(e) => {
if (value !== subject.id) {
e.currentTarget.style.background = 'transparent';
}
}}
>
<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',
}}>
{subject.name}
</div>
</div>
{/* Галочка для выбранного элемента */}
{value === subject.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>
);
};