Add loading match results by date
This commit is contained in:
133
src/App.tsx
133
src/App.tsx
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user