uchill/front_material/components/dashboard/SubjectNameSelect.tsx

249 lines
8.9 KiB
TypeScript

/**
* Селектор предметов по названию (как SubjectSelect, для прогресса ученика).
*/
'use client';
import React, { useState, useRef, useEffect } from 'react';
export interface SubjectNameOption {
subject: string;
}
interface SubjectNameSelectProps {
options: SubjectNameOption[];
value: string | null;
onChange: (value: string | null) => void;
disabled?: boolean;
placeholder?: string;
}
export const SubjectNameSelect: React.FC<SubjectNameSelectProps> = ({
options,
value,
onChange,
disabled = false,
placeholder = 'Выберите предмет',
}) => {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const containerRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const filtered = options.filter((o) =>
o.subject.toLowerCase().includes(searchQuery.toLowerCase().trim())
);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
setIsOpen(false);
setSearchQuery('');
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
setTimeout(() => searchInputRef.current?.focus(), 0);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen]);
const selected = options.find((o) => o.subject === value);
return (
<div ref={containerRef} style={{ position: 'relative', width: '100%', minWidth: 160 }}>
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
style={{
width: '100%',
padding: '12px 16px',
fontSize: '16px',
color: selected ? '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 ? 2 : 1,
borderRadius: 4,
fontFamily: 'inherit',
cursor: disabled ? 'not-allowed' : 'pointer',
outline: 'none',
transition: 'all 0.2s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
textAlign: 'left',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flex: 1, minWidth: 0 }}>
{selected ? (
<span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{selected.subject}
</span>
) : (
<span>{placeholder}</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" />
</svg>
</button>
{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: 12,
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
zIndex: 1000,
maxHeight: 320,
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
}}
>
<div style={{ padding: 12, 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: 12, pointerEvents: 'none' }}
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Поиск предмета..."
style={{
width: '100%',
padding: '8px 12px 8px 40px',
fontSize: 14,
color: 'var(--md-sys-color-on-surface)',
background: 'var(--md-sys-color-surface-variant)',
border: 'none',
borderRadius: 8,
fontFamily: 'inherit',
outline: 'none',
}}
/>
{searchQuery && (
<button
type="button"
onClick={() => setSearchQuery('')}
style={{
position: 'absolute',
right: 8,
width: 24,
height: 24,
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 x1={6} y1={6} x2={18} y2={18} />
</svg>
</button>
)}
</div>
</div>
<div style={{ overflowY: 'auto', flex: 1 }}>
{filtered.length === 0 ? (
<div style={{ padding: 24, textAlign: 'center', color: 'var(--md-sys-color-on-surface-variant)', fontSize: 14 }}>
{searchQuery ? 'Предметы не найдены' : 'Нет доступных предметов'}
</div>
) : (
filtered.map((o) => (
<button
key={o.subject}
type="button"
onClick={() => {
onChange(o.subject);
setIsOpen(false);
setSearchQuery('');
}}
style={{
width: '100%',
padding: '12px 16px',
background: value === o.subject ? 'var(--md-sys-color-secondary-container)' : 'transparent',
border: 'none',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 12,
transition: 'background 0.2s ease',
textAlign: 'left',
}}
onMouseEnter={(e) => {
if (value !== o.subject) e.currentTarget.style.background = 'var(--md-sys-color-surface-variant)';
}}
onMouseLeave={(e) => {
if (value !== o.subject) e.currentTarget.style.background = 'transparent';
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 14,
fontWeight: 500,
color: 'var(--md-sys-color-on-surface)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{o.subject}
</div>
</div>
{value === o.subject && (
<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" />
</svg>
)}
</button>
))
)}
</div>
</div>
)}
</div>
);
};