diff --git a/frontend/src/autoRefresh.ts b/frontend/src/autoRefresh.ts new file mode 100644 index 0000000..c9430b5 --- /dev/null +++ b/frontend/src/autoRefresh.ts @@ -0,0 +1,36 @@ +import { useSyncExternalStore } from "react"; + +export const REFRESH_INTERVAL_MS = Number(import.meta.env.VITE_REFRESH_INTERVAL_MS ?? 10_000); + +const KEY = "offtech.autoRefresh"; +const listeners = new Set<() => void>(); + +function read(): boolean { + try { + return localStorage.getItem(KEY) !== "off"; + } catch { + return true; + } +} + +let enabled = read(); + +export function setAutoRefresh(value: boolean): void { + enabled = value; + try { + localStorage.setItem(KEY, value ? "on" : "off"); + } catch { + } + listeners.forEach((l) => l()); +} + +export function useAutoRefreshEnabled(): boolean { + return useSyncExternalStore( + (cb) => { + listeners.add(cb); + return () => listeners.delete(cb); + }, + () => enabled, + () => true, + ); +} diff --git a/frontend/src/components/LiveIndicator.css b/frontend/src/components/LiveIndicator.css new file mode 100644 index 0000000..0890aca --- /dev/null +++ b/frontend/src/components/LiveIndicator.css @@ -0,0 +1,42 @@ +.live { + margin: 0 0 12px; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 16px; + font-size: 14px; + color: #6b7280; +} +.live-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: #9ca3af; + flex: none; +} +.live-dot.on { + background: #16a34a; +} +.live-dot.stale { + background: #d97706; +} +.live-dot.busy { + animation: live-pulse 0.9s ease-in-out infinite; +} +.live-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; +} +@keyframes live-pulse { + 50% { + opacity: 0.3; + } +} +@media (prefers-reduced-motion: reduce) { + .live-dot.busy { + animation: none; + } +} diff --git a/frontend/src/components/LiveIndicator.tsx b/frontend/src/components/LiveIndicator.tsx new file mode 100644 index 0000000..694c375 --- /dev/null +++ b/frontend/src/components/LiveIndicator.tsx @@ -0,0 +1,35 @@ +import { setAutoRefresh, useAutoRefreshEnabled } from "../autoRefresh"; +import { formatTime } from "../liveFetchLogic"; +import "./LiveIndicator.css"; + +interface Props { + updatedAt: Date | null; + refreshing: boolean; + refreshError: string | null; +} + +export function LiveIndicator({ updatedAt, refreshing, refreshError }: Props) { + const auto = useAutoRefreshEnabled(); + const time = updatedAt ? formatTime(updatedAt) : "—"; + + let text = `Обновлено ${time}`; + let dot = "on"; + if (refreshError) { + text = `Нет связи с сервером, показаны данные от ${time}`; + dot = "stale"; + } else if (!auto) { + text = `Автообновление выключено, данные от ${time}`; + dot = "off"; + } + + return ( +