watch-party/frontend/src/components/TimeSyncNotice.tsx
Nik Afiq 9674fc9cbe Squashed commit of the following:
commit da3f3bff39f09e753c1c52de0c47a12383cd20dc
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 02:49:31 2025 +0900

    docs: add README files for frontend and backend with setup instructions and commands

commit df71faa0751d1cef3ad6d50d0293fb8f8239d000
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 02:44:13 2025 +0900

    chore: update Vite version and add Vitest configuration

    - Updated Vite dependency from rolldown-vite@7.1.12 to ^5.4.11 in package.json.
    - Added setup file for Vitest to handle Vite SSR helpers, preventing ReferenceError during unit tests.
    - Created a new tsconfig.vitest.json file extending the main tsconfig for Vitest compatibility.
    - Added vitest.config.ts to configure Vitest with Node environment and setup files.

commit f3a1710dd0fe12998965992f6b5f8803422087cf
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 02:37:32 2025 +0900

    feat: add testing framework and implement unit tests for API and config

    - Added Vitest as a testing framework with scripts for running tests.
    - Created unit tests for the `fetchSchedule` and `fetchServerNow` functions in `watchparty.test.ts`.
    - Implemented unit tests for configuration functions in `config.test.ts`.
    - Added utility functions for parsing time in `time.ts` with corresponding tests in `time.test.ts`.
    - Updated API error handling to use `unknown` type for better type safety.
    - Refactored `TimeSyncNotice` and `Timer` components to improve performance and error handling.
    - Enhanced toast notifications by moving related functions to `toastBus.ts`.
    - Improved type definitions across various files for better type safety and clarity.

commit 0d436849fc4a0b87d0c73f4fe14fe1e272d47ad9
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 02:30:01 2025 +0900

    Refactor components to utilize centralized configuration: update TimeSyncNotice and Timer to use config for intervals; enhance error handling and retry logic in ShowsPage.

commit 8dbd4d207a471d05fab8c6f9cd95e3f4f7ec9099
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 02:22:28 2025 +0900

    Refactor API endpoint handling: replace join function with buildApiUrl for cleaner URL construction; update BrowserRouter basename to use config

commit 3e327aa73877034019dffe262580536f4be7c62e
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 02:18:31 2025 +0900

    Refactor configuration management: introduce config.ts for centralized app configuration; update API endpoint handling and logger to use new config

commit 131984d1baf368f94a14a62986eaf028ebbd7c86
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 02:11:33 2025 +0900

    Refactor API types: move ScheduleResponse and ShowItem types to a new types.ts file; update imports in watchparty and Timer components

commit 8faa4661a9ccc0691490a5766f0eb1d97f24b6e5
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 02:05:42 2025 +0900

    Refactor API handling: introduce centralized error handling and logging; replace direct fetch calls with apiFetch in Timer, ShowsPage, and hooks

commit ffde7e89fcab6f48c6023afab73e4b2e1122efa5
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 01:52:36 2025 +0900

    Add dist directory to .gitignore

commit 128a5be6eaa16bf4db5f7dd832b1d461fa2b835d
Author: Nik Afiq <nik.afiq98@ymail.com>
Date:   Sat Dec 6 01:52:28 2025 +0900

    Add toast notifications and debug overlay components; refactor Timer and ShowsPage for error handling
2025-12-06 03:03:30 +09:00

118 lines
4.2 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.

import { useEffect, useMemo, useState } from "react";
import { useTimeSkew } from "../hooks/useTimeSkew";
import { config } from "../config";
function formatDelay(ms: number, withSign = true) {
const sign = withSign ? (ms > 0 ? "+" : ms < 0 ? "" : "") : "";
const abs = Math.abs(ms);
if (abs >= 1000) {
const s = Math.floor(abs / 1000);
const rem = abs % 1000;
return `${sign}${s}s${rem ? ` ${rem}ms` : ""}`;
}
return `${sign}${abs}ms`;
}
export default function TimeSyncNotice({
thresholdMs = 500,
intervalMs,
lang = "ja",
dismissTtlMs = 5 * 60_000, // ✅ reappear after 5 minutes by default
storageScope = "session", // "session" | "local"
}: {
thresholdMs?: number;
intervalMs?: number;
lang?: "ja" | "en";
dismissTtlMs?: number;
storageScope?: "session" | "local";
}) {
const { skewMs, rttMs, recheck, error } = useTimeSkew({ intervalMs: intervalMs ?? config.intervals.timeSkewMs });
const KEY = "timesync.dismissedUntil";
const store = useMemo(
() => (storageScope === "local" ? window.localStorage : window.sessionStorage),
[storageScope],
);
// read dismissed state (true if now < dismissedUntil)
const [dismissed, setDismissed] = useState<boolean>(() => {
try {
const raw = store.getItem(KEY);
const until = raw ? Number(raw) : 0;
return Number.isFinite(until) && Date.now() < until;
} catch {
return false;
}
});
// auto-unset when TTL passes (while component stays mounted)
useEffect(() => {
if (!dismissed) return;
let id: number | null = null;
try {
const raw = store.getItem(KEY);
const until = raw ? Number(raw) : 0;
const delay = Math.max(0, until - Date.now());
if (delay > 0) {
id = window.setTimeout(() => setDismissed(false), delay);
} else {
setDismissed(false);
store.removeItem(KEY);
}
} catch {
// ignore storage errors
}
return () => { if (id) clearTimeout(id); };
}, [dismissed, store]);
// if skew returns within threshold, clear dismissal so it can show again later if it drifts
useEffect(() => {
if (skewMs != null && Math.abs(skewMs) <= thresholdMs && dismissed) {
setDismissed(false);
try { store.removeItem(KEY); } catch { void 0; }
}
}, [skewMs, thresholdMs, dismissed, store]);
if (dismissed) return null;
const hasSkew = skewMs != null;
const skewVal = skewMs ?? 0;
if (!error && (!hasSkew || Math.abs(skewVal) <= thresholdMs)) return null;
const ahead = skewVal > 0;
const msgJa = error
? `時刻同期に失敗しました: ${error}`
: ahead
? `端末の時計が正確な時刻より ${formatDelay(skewVal)} 進んでいます(通信往復遅延 ${rttMs ?? "-"}ms`
: `端末の時計が正確な時刻より ${formatDelay(-skewVal)} 遅れています(通信往復遅延 ${rttMs ?? "-"}ms`;
const msgEn = error
? `Time sync failed: ${error}`
: ahead
? `Your device clock is ${formatDelay(skewVal)} ahead of the correct time (RTT ${rttMs ?? "-"}ms).`
: `Your device clock is ${formatDelay(-skewVal)} behind the correct time (RTT ${rttMs ?? "-"}ms).`;
const onClose = () => {
setDismissed(true);
try {
const until = Date.now() + dismissTtlMs;
store.setItem(KEY, String(until));
} catch {
// ignore storage failures
}
};
return (
<div className="timesync-banner" role="status" aria-live="polite">
<div className="timesync-msg">{lang === "ja" ? msgJa : msgEn}</div>
<div className="timesync-actions">
<button className="timesync-btn" onClick={() => recheck?.()}>
{lang === "ja" ? "再測定" : "Re-check"}
</button>
<button className="timesync-close" onClick={onClose} aria-label={lang === "ja" ? "閉じる" : "Close"}>
×
</button>
</div>
</div>
);
}