Show badge only in local test environment
This commit is contained in:
@@ -49,6 +49,9 @@ LINE 推播目標支援分成兩組:
|
|||||||
- `LINE_TARGET_ID_PROD`: 正式環境用對話
|
- `LINE_TARGET_ID_PROD`: 正式環境用對話
|
||||||
- `LINE_TARGET_MODE`: `local` 或 `prod`
|
- `LINE_TARGET_MODE`: `local` 或 `prod`
|
||||||
|
|
||||||
|
當系統偵測目前為 `local` 模式時,頁面右上角會顯示「測試環境」標籤。
|
||||||
|
正式環境 `prod` 不會顯示這個標籤。
|
||||||
|
|
||||||
## 資料庫欄位
|
## 資料庫欄位
|
||||||
|
|
||||||
- `time`: 目標日期,格式為 `YYYYMMDD`
|
- `time`: 目標日期,格式為 `YYYYMMDD`
|
||||||
|
|||||||
22
src/App.css
22
src/App.css
@@ -29,10 +29,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero-copy {
|
.hero-copy {
|
||||||
|
position: relative;
|
||||||
padding: 36px;
|
padding: 36px;
|
||||||
background: linear-gradient(145deg, rgba(255, 247, 234, 0.92), rgba(232, 244, 235, 0.84));
|
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 {
|
.eyebrow {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
color: #1d6c46;
|
color: #1d6c46;
|
||||||
@@ -371,6 +388,11 @@
|
|||||||
padding: 18px;
|
padding: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.env-badge {
|
||||||
|
position: static;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.team-line {
|
.team-line {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/App.tsx
27
src/App.tsx
@@ -26,6 +26,10 @@ type LoadMatchResultsResponse = {
|
|||||||
battlecombination: string | null
|
battlecombination: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HealthResponse = {
|
||||||
|
lineTargetMode?: string
|
||||||
|
}
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
areaA: 'badminton-match-hub::area-a',
|
areaA: 'badminton-match-hub::area-a',
|
||||||
areaB: 'badminton-match-hub::area-b',
|
areaB: 'badminton-match-hub::area-b',
|
||||||
@@ -49,6 +53,7 @@ function App() {
|
|||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [actionState, setActionState] = useState<ActionState>('idle')
|
const [actionState, setActionState] = useState<ActionState>('idle')
|
||||||
const [actionMessage, setActionMessage] = useState('')
|
const [actionMessage, setActionMessage] = useState('')
|
||||||
|
const [showTestBadge, setShowTestBadge] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.localStorage.setItem(STORAGE_KEYS.areaA, areaAInput)
|
window.localStorage.setItem(STORAGE_KEYS.areaA, areaAInput)
|
||||||
@@ -58,6 +63,13 @@ function App() {
|
|||||||
window.localStorage.setItem(STORAGE_KEYS.areaB, areaBInput)
|
window.localStorage.setItem(STORAGE_KEYS.areaB, areaBInput)
|
||||||
}, [areaBInput])
|
}, [areaBInput])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
const isLocalMode = await loadEnvironmentMode()
|
||||||
|
setShowTestBadge(isLocalMode)
|
||||||
|
})()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const parsedAreaA = parseRoster(areaAInput)
|
const parsedAreaA = parseRoster(areaAInput)
|
||||||
const parsedAreaB = parseRoster(areaBInput)
|
const parsedAreaB = parseRoster(areaBInput)
|
||||||
const teamCount = Math.max(parsedAreaA.length, parsedAreaB.length)
|
const teamCount = Math.max(parsedAreaA.length, parsedAreaB.length)
|
||||||
@@ -196,6 +208,7 @@ function App() {
|
|||||||
<main className="app-shell">
|
<main className="app-shell">
|
||||||
<section className="hero-card">
|
<section className="hero-card">
|
||||||
<div className="hero-copy">
|
<div className="hero-copy">
|
||||||
|
{showTestBadge ? <div className="env-badge">測試環境</div> : null}
|
||||||
<p className="eyebrow">羽毛球隊伍配對</p>
|
<p className="eyebrow">羽毛球隊伍配對</p>
|
||||||
<h1>羽毛球分組配對器</h1>
|
<h1>羽毛球分組配對器</h1>
|
||||||
<p className="hero-text">
|
<p className="hero-text">
|
||||||
@@ -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) {
|
function convertDbRecordToAppState(record: LoadMatchResultsResponse) {
|
||||||
const personnel = JSON.parse(record.personnel) as [number, string][]
|
const personnel = JSON.parse(record.personnel) as [number, string][]
|
||||||
const battlecombination = JSON.parse(record.battlecombination ?? '{}') as Record<
|
const battlecombination = JSON.parse(record.battlecombination ?? '{}') as Record<
|
||||||
|
|||||||
Reference in New Issue
Block a user