uchill/front_material/components/navigation/ChildSelector.tsx

320 lines
11 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 { useState, useRef, useEffect } from 'react';
import { useSelectedChild } from '@/contexts/SelectedChildContext';
import type { ChildStats } from '@/api/dashboard';
function getAvatarUrl(child: ChildStats | null): string | null {
if (!child) return null;
const url = child.avatar_url || child.avatar;
if (!url) return null;
if (typeof url === 'string' && url.startsWith('http')) return url;
const base = typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8123` : '';
return typeof url === 'string' && url.startsWith('/') ? `${base}${url}` : `${base}/${url}`;
}
/** Минималистичный выбор ребёнка слева от нижней навигации */
export function ChildSelectorCompact() {
const { selectedChild, childrenList, setSelectedChild, loading } = useSelectedChild();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const onOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', onOutside);
return () => document.removeEventListener('mousedown', onOutside);
}, []);
if (loading && childrenList.length === 0) {
return (
<div
className="ios26-bottom-nav-button"
style={{ width: 44, minWidth: 44, flexShrink: 0 }}
aria-hidden
>
<span className="ios26-bottom-nav-icon" style={{ opacity: 0.5 }}>person</span>
</div>
);
}
if (childrenList.length === 0) return null;
const avatarUrl = getAvatarUrl(selectedChild);
const initial = selectedChild?.name?.charAt(0)?.toUpperCase() ?? '?';
return (
<div ref={ref} data-tour="parent-child-selector" style={{ position: 'relative', flexShrink: 0 }}>
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="ios26-bottom-nav-button"
style={{
width: 44,
minWidth: 44,
padding: 6,
}}
aria-label={selectedChild ? `Студент: ${selectedChild.name}` : 'Выбрать студента'}
>
<span
style={{
width: 24,
height: 24,
borderRadius: '50%',
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--md-sys-color-primary-container)',
color: 'var(--md-sys-color-primary)',
fontSize: 12,
fontWeight: 600,
}}
>
{avatarUrl ? (
<img src={avatarUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
) : (
initial
)}
</span>
<span className="ios26-bottom-nav-label" style={{ fontSize: 9 }}>Студент</span>
</button>
{open && (
<div
style={{
position: 'absolute',
bottom: '100%',
left: 0,
marginBottom: 6,
minWidth: 160,
maxWidth: 220,
background: 'var(--md-sys-color-surface)',
borderRadius: 12,
border: '1px solid var(--md-sys-color-outline-variant)',
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
zIndex: 10052,
overflow: 'hidden',
}}
>
{childrenList.map((child) => {
const isSelected = selectedChild?.id === child.id;
const url = getAvatarUrl(child);
return (
<button
key={child.id}
type="button"
onClick={() => {
setSelectedChild(child);
setOpen(false);
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
width: '100%',
padding: '10px 14px',
border: 'none',
background: isSelected ? 'var(--md-sys-color-primary-container)' : 'transparent',
color: 'var(--md-sys-color-on-surface)',
fontSize: 14,
cursor: 'pointer',
textAlign: 'left',
}}
>
<span
style={{
width: 28,
height: 28,
borderRadius: '50%',
overflow: 'hidden',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--md-sys-color-surface-variant)',
color: 'var(--md-sys-color-on-surface-variant)',
fontSize: 12,
fontWeight: 600,
}}
>
{url ? (
<img src={url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
) : (
child.name.charAt(0).toUpperCase() || '?'
)}
</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{child.name}</span>
</button>
);
})}
</div>
)}
</div>
);
}
export function ChildSelector() {
const { selectedChild, childrenList, setSelectedChild, loading } = useSelectedChild();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const onOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', onOutside);
return () => document.removeEventListener('mousedown', onOutside);
}, []);
if (loading && childrenList.length === 0) {
return (
<div style={{
padding: '10px 16px',
background: 'var(--md-sys-color-surface-container)',
borderBottom: '1px solid var(--md-sys-color-outline-variant)',
fontSize: 14,
color: 'var(--md-sys-color-on-surface-variant)',
}}>
Загрузка...
</div>
);
}
if (childrenList.length === 0) return null;
const avatarUrl = getAvatarUrl(selectedChild);
const displayName = selectedChild?.name ?? 'Выберите ребёнка';
return (
<div
ref={ref}
style={{
padding: '10px 16px',
background: 'var(--md-sys-color-surface-container)',
borderBottom: '1px solid var(--md-sys-color-outline-variant)',
position: 'relative',
zIndex: 10050,
}}
>
<button
type="button"
onClick={() => setOpen((o) => !o)}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
width: '100%',
padding: '8px 12px',
borderRadius: 12,
border: '1px solid var(--md-sys-color-outline-variant)',
background: 'var(--md-sys-color-surface)',
color: 'var(--md-sys-color-on-surface)',
fontSize: 15,
fontWeight: 500,
cursor: 'pointer',
textAlign: 'left',
}}
>
<span
style={{
width: 36,
height: 36,
borderRadius: '50%',
overflow: 'hidden',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--md-sys-color-primary-container)',
color: 'var(--md-sys-color-primary)',
fontSize: 16,
fontWeight: 600,
}}
>
{avatarUrl ? (
<img src={avatarUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
) : (
displayName.charAt(0).toUpperCase() || '?'
)}
</span>
<span style={{ flex: 1 }}>{displayName}</span>
<span className="material-symbols-outlined" style={{ fontSize: 20, opacity: 0.7 }}>
{open ? 'expand_less' : 'expand_more'}
</span>
</button>
{open && (
<div
style={{
position: 'absolute',
top: '100%',
left: 16,
right: 16,
marginTop: 4,
background: 'var(--md-sys-color-surface)',
borderRadius: 12,
border: '1px solid var(--md-sys-color-outline-variant)',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
zIndex: 10051,
overflow: 'hidden',
}}
>
{childrenList.map((child) => {
const isSelected = selectedChild?.id === child.id;
const url = getAvatarUrl(child);
return (
<button
key={child.id}
type="button"
onClick={() => {
setSelectedChild(child);
setOpen(false);
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
width: '100%',
padding: '12px 16px',
border: 'none',
background: isSelected ? 'var(--md-sys-color-primary-container)' : 'transparent',
color: 'var(--md-sys-color-on-surface)',
fontSize: 15,
cursor: 'pointer',
textAlign: 'left',
}}
>
<span
style={{
width: 32,
height: 32,
borderRadius: '50%',
overflow: 'hidden',
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--md-sys-color-surface-variant)',
color: 'var(--md-sys-color-on-surface-variant)',
fontSize: 14,
fontWeight: 600,
}}
>
{url ? (
<img src={url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
) : (
child.name.charAt(0).toUpperCase() || '?'
)}
</span>
{child.name}
</button>
);
})}
</div>
)}
</div>
);
}