320 lines
11 KiB
TypeScript
320 lines
11 KiB
TypeScript
'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>
|
||
);
|
||
}
|