Files
dsp-thomann-interface/src/components/topbar/Topbar.tsx

231 lines
5.5 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { DSP, AppPage } from '../../types/types';
import AnimatedLogo from '../../assets/icons/AnimatedLogo';
import { PlusIcon } from '../../assets/icons/PlusIcon';
import { CrownIcon } from '../../assets/icons/CrownIcon';
import Led from '../Led/Led';
import { CloseIcon } from '../../assets/icons/CloseIcon';
import './Topbar.css';
function getStep(count: number) {
if (count <= 4) return 4;
if (count <= 6) return 6;
if (count <= 8) return 8;
return 10;
}
function Tab({
dsp,
active,
dimmed,
step,
onSelect,
onRemove,
onConnect,
onDisconnect,
}: {
dsp: DSP;
active: boolean;
dimmed: boolean;
step: number;
onSelect: () => void;
onRemove: () => void;
onConnect: (dsp: DSP) => void;
onDisconnect: (dsp: DSP) => void;
}) {
const isConnected = dsp.status === 'connected';
const handleTabKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onSelect();
}
};
return (
<div
className={[
'tab',
active && 'tab--active',
dimmed && 'tab--dimmed',
]
.filter(Boolean)
.join(' ')}
data-step={step}
tabIndex={0}
role="tab"
aria-selected={active}
onClick={onSelect}
onKeyDown={handleTabKeyDown}
>
<button
type="button"
className="tab-led"
aria-label={
isConnected
? `Disconnect ${dsp.name}`
: `Connect ${dsp.name}`
}
onClick={(e) => {
e.stopPropagation();
if (isConnected) {
onDisconnect(dsp);
} else {
onConnect(dsp);
}
}}
>
<Led status={dsp.status} />
</button>
<span className="tab-name" title={dsp.name}>
{dsp.name}
</span>
<button
type="button"
className="tab-close"
aria-label={`Remove ${dsp.name}`}
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
>
<CloseIcon />
</button>
</div>
);
}
function TopBar({
dsps,
selected,
page,
setPage,
setSelect,
onSelect,
onRemove,
onAddClick,
onConnect,
onDisconnect,
}: {
dsps: DSP[];
selected: number | null;
page: AppPage;
setPage: (page: AppPage) => void;
setSelect: (select: number | null) => void;
onSelect: (id: number) => void;
onRemove: (dsp: DSP) => void;
onAddClick: () => void;
onConnect: (dsp: DSP) => void;
onDisconnect: (dsp: DSP) => void;
}) {
const hasTabs = dsps.length > 0;
const tabstripRef = useRef<HTMLDivElement>(null);
const prevCount = useRef(dsps.length);
const step = getStep(dsps.length);
const updateFade = () => {
const el = tabstripRef.current;
if (!el) return;
const atStart = el.scrollLeft <= 1;
const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 1;
el.dataset.fadeStart = String(!atStart);
el.dataset.fadeEnd = String(!atEnd && el.scrollWidth > el.clientWidth);
};
useEffect(() => {
const el = tabstripRef.current;
if (!el) return;
const onWheel = (e: WheelEvent) => {
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
el.scrollLeft += e.deltaY;
e.preventDefault();
}
};
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, []);
useEffect(() => {
updateFade();
const el = tabstripRef.current;
el?.addEventListener('scroll', updateFade);
window.addEventListener('resize', updateFade);
return () => {
el?.removeEventListener('scroll', updateFade);
window.removeEventListener('resize', updateFade);
};
}, [dsps.length]);
useEffect(() => {
if (dsps.length > prevCount.current && tabstripRef.current) {
tabstripRef.current.scrollTo({
left: tabstripRef.current.scrollWidth,
behavior: 'smooth',
});
}
prevCount.current = dsps.length;
}, [dsps.length]);
return (
<header className="topbar">
<button
className="logo-slot"
onClick={() => {
setSelect(null);
setPage('home');
}}
aria-label="Go to home"
>
<AnimatedLogo />
</button>
<div className="tabstrip" ref={tabstripRef}>
{dsps.map((dsp) => (
<Tab
key={dsp.id}
dsp={dsp}
step={step}
active={selected === dsp.id && !(page == 'add-dsp')}
dimmed={page === 'add-dsp'}
onSelect={() => onSelect(dsp.id)}
onRemove={() => onRemove(dsp)}
onConnect={onConnect}
onDisconnect={onDisconnect}
/>
))}
</div>
<div className="tabstrip-actions">
{hasTabs ? (
<button className="tab-add tab-action" aria-label="Add DSP" onClick={onAddClick}>
<PlusIcon className="icon-tab-add" />
</button>
) : (
<button className="tab-add-empty tab-action" onClick={onAddClick}>
<PlusIcon className="icon-tab-add-empty" />
<span>Add DSP</span>
</button>
)}
<button
className="crown-action tab-action"
title="Credits & Support"
aria-label="Credits & Support"
onClick={() => {
setPage('credits');
setSelect(null);
}}
>
<CrownIcon className="icon-crown" />
</button>
</div>
</header>
);
}
export default TopBar;