Add loading match results by date

This commit is contained in:
2026-04-15 10:11:53 +08:00
parent 6c3ff0e3d1
commit 35eab44a7a
4 changed files with 165 additions and 22 deletions

View File

@@ -18,7 +18,13 @@ type RoundResult = {
teams: Team[]
}
type SaveState = 'idle' | 'saving' | 'saved' | 'error'
type ActionState = 'idle' | 'saving' | 'saved' | 'loading' | 'loaded' | 'error'
type LoadMatchResultsResponse = {
time: number
personnel: string
battlecombination: string | null
}
const STORAGE_KEYS = {
areaA: 'badminton-match-hub::area-a',
@@ -41,8 +47,8 @@ function App() {
const [targetDate, setTargetDate] = useState(() => formatDateInputValue())
const [results, setResults] = useState<RoundResult[]>([])
const [error, setError] = useState('')
const [saveState, setSaveState] = useState<SaveState>('idle')
const [saveMessage, setSaveMessage] = useState('')
const [actionState, setActionState] = useState<ActionState>('idle')
const [actionMessage, setActionMessage] = useState('')
useEffect(() => {
window.localStorage.setItem(STORAGE_KEYS.areaA, areaAInput)
@@ -62,8 +68,8 @@ function App() {
if (parsedAreaA.length === 0 || parsedAreaB.length === 0) {
setResults([])
setError('A 區與 B 區都至少要輸入 1 位成員。')
setSaveState('idle')
setSaveMessage('')
setActionState('idle')
setActionMessage('')
return
}
@@ -75,45 +81,73 @@ function App() {
setResults(nextResults)
setError('')
setSaveState('idle')
setSaveMessage('已產生配對,請按「上傳資料」。')
setActionState('idle')
setActionMessage('已產生配對,請按「上傳資料」。')
}
async function uploadResults() {
if (results.length === 0) {
setSaveState('error')
setSaveMessage('請先產生配對結果,再上傳資料。')
setActionState('error')
setActionMessage('請先產生配對結果,再上傳資料。')
return
}
if (!targetDate) {
setSaveState('error')
setSaveMessage('請先選擇目標日期。')
setActionState('error')
setActionMessage('請先選擇目標日期。')
return
}
setSaveState('saving')
setSaveMessage('上傳資料到資料庫中...')
setActionState('saving')
setActionMessage('上傳資料到資料庫中...')
try {
await saveMatchResults(convertDateToKey(targetDate), parsedAreaA, parsedAreaB, results)
setSaveState('saved')
setSaveMessage(`已同步到資料庫:${convertDateToKey(targetDate)}`)
setActionState('saved')
setActionMessage(`已同步到資料庫:${convertDateToKey(targetDate)}`)
} catch (saveError) {
setSaveState('error')
setSaveMessage(
setActionState('error')
setActionMessage(
saveError instanceof Error ? saveError.message : '資料庫儲存失敗,請稍後再試。',
)
}
}
async function loadResults() {
if (!targetDate) {
setActionState('error')
setActionMessage('請先選擇目標日期。')
return
}
setActionState('loading')
setActionMessage('讀取指定日期資料中...')
try {
const payload = await loadMatchResults(convertDateToKey(targetDate))
const loaded = convertDbRecordToAppState(payload)
setAreaAInput(loaded.areaA.join('\n'))
setAreaBInput(loaded.areaB.join('\n'))
setResults(loaded.results)
setError('')
setActionState('loaded')
setActionMessage(`已讀取資料:${convertDateToKey(targetDate)}`)
} catch (loadError) {
setResults([])
setActionState('error')
setActionMessage(
loadError instanceof Error ? loadError.message : '讀取資料失敗,請稍後再試。',
)
}
}
function resetDemo() {
setAreaAInput(defaultAreaA.join('\n'))
setAreaBInput(defaultAreaB.join('\n'))
setResults([])
setError('')
setSaveState('idle')
setSaveMessage('')
setActionState('idle')
setActionMessage('')
}
return (
@@ -135,8 +169,8 @@ function App() {
</button>
</div>
{saveMessage ? (
<p className={`save-status save-status-${saveState}`}>{saveMessage}</p>
{actionMessage ? (
<p className={`save-status save-status-${actionState}`}>{actionMessage}</p>
) : null}
</div>
@@ -177,10 +211,18 @@ function App() {
className="primary-button upload-button"
type="button"
onClick={() => void uploadResults()}
disabled={results.length === 0 || saveState === 'saving'}
disabled={results.length === 0 || actionState === 'saving' || actionState === 'loading'}
>
</button>
<button
className="ghost-button upload-button"
type="button"
onClick={() => void loadResults()}
disabled={actionState === 'saving' || actionState === 'loading'}
>
</button>
</div>
<div className="input-grid">
@@ -319,6 +361,53 @@ async function saveMatchResults(
}
}
async function loadMatchResults(time: string) {
const response = await fetch(`/api/match-results/${time}`)
const payload = (await response.json()) as {
ok?: boolean
message?: string
data?: LoadMatchResultsResponse
}
if (!response.ok || !payload.ok || !payload.data) {
throw new Error(payload.message ?? '指定日期沒有資料。')
}
return payload.data
}
function convertDbRecordToAppState(record: LoadMatchResultsResponse) {
const personnel = JSON.parse(record.personnel) as [number, string][]
const battlecombination = JSON.parse(record.battlecombination ?? '{}') as Record<
string,
[string, string][]
>
const areaA = personnel.filter(([group]) => group === 1).map(([, name]) => name)
const areaB = personnel.filter(([group]) => group === 0).map(([, name]) => name)
const results = Object.keys(battlecombination)
.sort((left, right) => Number(left) - Number(right))
.map((key, roundIndex) => ({
id: roundIndex + 1,
teams: battlecombination[key].map((team, teamIndex) => ({
id: teamIndex + 1,
playerA: createLoadedParticipant(team[0], 'A', roundIndex, teamIndex),
playerB: createLoadedParticipant(team[1], 'B', roundIndex, teamIndex),
})),
}))
return { areaA, areaB, results }
}
function createLoadedParticipant(name: string, zone: 'A' | 'B', roundIndex: number, teamIndex: number) {
return {
id: `${zone}-loaded-${roundIndex}-${teamIndex}-${name}`,
name,
isPlaceholder: name === PLACEHOLDER_NAME,
}
}
function formatDateInputValue() {
const now = new Date()
const year = now.getFullYear()