- Implemented device pairing with a four-digit code and TTL for remote settings. - Introduced automatic checks for device status and configuration validation. - Developed UI components for device setup and management, including error handling and retry options. - Created a device bridge for secure communication between the device and the server. - Added tests for managed state validation and device pairing logic. - Established a local end-to-end fixture for testing device service and API interactions.
24 lines
3.9 KiB
TypeScript
24 lines
3.9 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
||
import logo from './assets/ridebus-logo.svg?raw';
|
||
import { NumericText } from './NumericText';
|
||
import { Fullscreen, Refresh, Shield } from './Icons';
|
||
import type { ManagedState } from './useDeviceControl';
|
||
export function DeviceSetup({ control, failed, onRetry }: { control: { state: string; code?: string; expiresAt?: string; adminUrl?: string }; failed: boolean; onRetry: () => void }) {
|
||
const [now, setNow] = useState(Date.now());
|
||
useEffect(() => { document.title = 'Подключение · RideBus Board'; const interval = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(interval); }, []);
|
||
const remaining = Math.max(0, Math.min(300, Math.ceil((Date.parse(control.expiresAt || '') - now) / 1000)));
|
||
const pending = control.state === 'pending' && remaining > 0;
|
||
return <main className="device-setup" aria-label="Подключение табло"><div className="device-setup-brand"><span dangerouslySetInnerHTML={{ __html: logo }} />ridebus</div><div className="device-setup-card"><span className="eyebrow">RIDEBUS BOARD</span><h1>{control.state === 'revoked' ? 'Устройство отвязано' : 'Подключите табло'}</h1>
|
||
<p>{pending ? 'Введите этот код в админке: Устройства → Добавить устройство' : control.state === 'revoked' ? 'Для повторной настройки подключите устройство по новому коду' : control.state === 'storage-error' ? 'Разрешите браузеру сохранять cookies и данные сайта' : control.state === 'loading' ? 'Получаем код подключения…' : 'Для подключения нужен доступ к серверу'}</p>
|
||
{pending && <><div className="device-pair-code" aria-label={`Код подключения ${control.code}`}><NumericText value={control.code || ''} /></div><div className="device-pair-expiry">Код действует ещё <NumericText value={`${Math.floor(remaining / 60)}:${String(remaining % 60).padStart(2, '0')}`} /></div></>}
|
||
{control.adminUrl && <a href={control.adminUrl} target="_blank" rel="noreferrer">Открыть админку</a>}
|
||
{failed && <div className="device-setup-status" role="status">{control.state === 'storage-error' ? 'Не удалось сохранить привязку устройства' : 'Нет связи с сервером. Повторяем подключение'}</div>}
|
||
{!pending && control.state !== 'loading' && <button onClick={onRetry}><Refresh />{control.state === 'revoked' ? 'Подключить заново' : 'Повторить'}</button>}
|
||
</div><button className="device-setup-fullscreen" onClick={() => { void document.documentElement.requestFullscreen().catch(() => {}); }}><Fullscreen />На весь экран</button></main>;
|
||
}
|
||
export function ManagedPanel({ state, failed, onClose, onFullscreen }: { state: ManagedState; failed: boolean; onClose: () => void; onFullscreen: () => void }) {
|
||
const dialog = useRef<HTMLDialogElement>(null);
|
||
useEffect(() => { dialog.current?.showModal(); }, []);
|
||
return <dialog ref={dialog} className="managed-panel" aria-labelledby="managed-title" onCancel={event => { event.preventDefault(); onClose(); }}><Shield size={36} /><h2 id="managed-title">Управляется из админки</h2><strong>{state.device.name}</strong><p>Изменяйте остановку, оформление и экраны в разделе «Устройства».</p><dl><dt>Настройки</dt><dd>Версия {state.device.revision}</dd><dt>Соединение</dt><dd>{failed ? 'Нет связи · сохранённые настройки' : 'Подключено'}</dd></dl><div><button onClick={onFullscreen}><Fullscreen />Полный экран</button><button onClick={onClose}>Закрыть</button></div></dialog>;
|
||
}
|