diff --git a/README.md b/README.md index fe87334..ff821f0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,9 @@ LINE 推播目標支援分成兩組: - `LINE_TARGET_ID_PROD`: 正式環境用對話 - `LINE_TARGET_MODE`: `local` 或 `prod` +當系統偵測目前為 `local` 模式時,頁面右上角會顯示「測試環境」標籤。 +正式環境 `prod` 不會顯示這個標籤。 + ## 資料庫欄位 - `time`: 目標日期,格式為 `YYYYMMDD` diff --git a/src/App.css b/src/App.css index df373b3..6b20956 100644 --- a/src/App.css +++ b/src/App.css @@ -29,10 +29,27 @@ } .hero-copy { + position: relative; padding: 36px; background: linear-gradient(145deg, rgba(255, 247, 234, 0.92), rgba(232, 244, 235, 0.84)); } +.env-badge { + position: absolute; + top: 20px; + right: 20px; + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 0 14px; + border-radius: 999px; + background: rgba(186, 61, 47, 0.12); + color: #8d2d22; + font-size: 0.84rem; + font-weight: 700; + letter-spacing: 0.08em; +} + .eyebrow { margin: 0 0 12px; color: #1d6c46; @@ -371,6 +388,11 @@ padding: 18px; } + .env-badge { + position: static; + margin-bottom: 14px; + } + .team-line { flex-wrap: wrap; } diff --git a/src/App.tsx b/src/App.tsx index 34ad51b..6a0dfec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,10 @@ type LoadMatchResultsResponse = { battlecombination: string | null } +type HealthResponse = { + lineTargetMode?: string +} + const STORAGE_KEYS = { areaA: 'badminton-match-hub::area-a', areaB: 'badminton-match-hub::area-b', @@ -49,6 +53,7 @@ function App() { const [error, setError] = useState('') const [actionState, setActionState] = useState('idle') const [actionMessage, setActionMessage] = useState('') + const [showTestBadge, setShowTestBadge] = useState(false) useEffect(() => { window.localStorage.setItem(STORAGE_KEYS.areaA, areaAInput) @@ -58,6 +63,13 @@ function App() { window.localStorage.setItem(STORAGE_KEYS.areaB, areaBInput) }, [areaBInput]) + useEffect(() => { + void (async () => { + const isLocalMode = await loadEnvironmentMode() + setShowTestBadge(isLocalMode) + })() + }, []) + const parsedAreaA = parseRoster(areaAInput) const parsedAreaB = parseRoster(areaBInput) const teamCount = Math.max(parsedAreaA.length, parsedAreaB.length) @@ -196,6 +208,7 @@ function App() {
+ {showTestBadge ?
測試環境
: null}

羽毛球隊伍配對

羽毛球分組配對器

@@ -477,6 +490,20 @@ async function pushMatchResultsToLine(time: string, rounds: RoundResult[]) { } } +async function loadEnvironmentMode() { + try { + const response = await fetch('/api/health') + if (!response.ok) { + return false + } + + const payload = (await response.json()) as HealthResponse + return payload.lineTargetMode === 'local' + } catch { + return false + } +} + function convertDbRecordToAppState(record: LoadMatchResultsResponse) { const personnel = JSON.parse(record.personnel) as [number, string][] const battlecombination = JSON.parse(record.battlecombination ?? '{}') as Record<