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
118 lines
4.2 KiB
TypeScript
118 lines
4.2 KiB
TypeScript
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>
|
||
);
|
||
}
|