diff --git a/src/App.css b/src/App.css index 52a4ce5..6450b71 100644 --- a/src/App.css +++ b/src/App.css @@ -347,8 +347,10 @@ gap: 14px; padding: 12px; border-radius: 22px; - background: #060606; - box-shadow: 0 30px 60px rgba(0, 0, 0, 0.26); + background: + radial-gradient(circle at top right, rgba(255, 205, 96, 0.22), transparent 26%), + linear-gradient(145deg, rgba(8, 47, 73, 0.97), rgba(10, 96, 84, 0.93)); + box-shadow: 0 30px 60px rgba(8, 47, 73, 0.24); } .scoreboard-team-section { @@ -370,7 +372,8 @@ min-height: 64px; padding: 8px; border-radius: 4px; - background: #d8cfd2; + background: rgba(255, 248, 232, 0.92); + box-shadow: inset 0 0 0 1px rgba(195, 154, 88, 0.2); } .scoreboard-name-chip { @@ -380,8 +383,8 @@ min-height: 46px; padding: 4px 8px; border-radius: 4px; - color: #111; - background: rgba(255, 255, 255, 0.42); + color: #16342f; + background: rgba(255, 255, 255, 0.7); } .scoreboard-name-chip strong { @@ -391,8 +394,8 @@ } .scoreboard-name-chip-serving { - background: #c8f400; - box-shadow: inset 0 0 0 2px rgba(34, 48, 0, 0.18); + background: linear-gradient(180deg, #ebf8a4, #d6e164); + box-shadow: inset 0 0 0 2px rgba(111, 128, 27, 0.24); } .team-number { @@ -405,7 +408,7 @@ font-family: var(--mono); font-size: 1.15rem; color: #fff; - background: #06253e; + background: linear-gradient(180deg, rgba(8, 47, 73, 0.96), rgba(10, 96, 84, 0.92)); border-radius: 4px; } @@ -421,8 +424,27 @@ cursor: pointer; font: inherit; font-size: 1.2rem; - color: #111; - background: #d0d0d0; + color: #5b2f13; + background: linear-gradient(180deg, #fff8e8, #f2d9a3); + box-shadow: + inset 0 0 0 1px rgba(199, 155, 83, 0.35), + 0 6px 14px rgba(8, 47, 73, 0.14); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + filter 0.16s ease; +} + +.team-icon-button:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: + inset 0 0 0 1px rgba(199, 155, 83, 0.45), + 0 10px 18px rgba(8, 47, 73, 0.18); +} + +.team-icon-button:active:not(:disabled) { + transform: translateY(0); + filter: brightness(0.98); } .team-icon-button:disabled { @@ -442,28 +464,41 @@ padding: 8px 10px; cursor: pointer; text-align: left; - color: #fff; - background: #2f2f2f; + color: #f8f4ea; + background: rgba(255, 248, 232, 0.12); + box-shadow: inset 0 0 0 1px rgba(255, 236, 202, 0.12); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + background 0.16s ease; } .serve-lane:disabled { cursor: default; } +.serve-lane:hover:not(:disabled) { + transform: translateY(-1px); + background: rgba(255, 248, 232, 0.18); + box-shadow: + inset 0 0 0 1px rgba(255, 236, 202, 0.22), + 0 10px 18px rgba(8, 47, 73, 0.12); +} + .serve-lane small { justify-self: end; - color: rgba(255, 255, 255, 0.72); + color: rgba(248, 244, 234, 0.72); } .serve-lane-locked { - box-shadow: inset 0 0 0 1px rgba(200, 244, 0, 0.28); + box-shadow: inset 0 0 0 1px rgba(214, 225, 100, 0.42); } .serve-lane-box { width: 24px; height: 24px; border-radius: 4px; - background: #f7f7f7; + background: linear-gradient(180deg, #fff8e8, #f0dfbd); } .score-panel-surface { @@ -479,9 +514,10 @@ transform 0.16s ease, box-shadow 0.16s ease; background: - linear-gradient(transparent 0 40%, rgba(0, 0, 0, 0.24) 40% 60%, transparent 60% 100%), - linear-gradient(90deg, transparent 0 40%, rgba(0, 0, 0, 0.24) 40% 60%, transparent 60% 100%), - #2f2f2f; + linear-gradient(transparent 0 40%, rgba(11, 39, 34, 0.18) 40% 60%, transparent 60% 100%), + linear-gradient(90deg, transparent 0 40%, rgba(11, 39, 34, 0.18) 40% 60%, transparent 60% 100%), + linear-gradient(180deg, rgba(255, 248, 232, 0.2), rgba(255, 248, 232, 0.08)), + rgba(245, 237, 221, 0.96); } .score-panel-surface-live { @@ -490,14 +526,17 @@ .score-panel-surface-live:hover { transform: translateY(-1px); - box-shadow: inset 0 0 0 2px rgba(213, 234, 2, 0.2); + box-shadow: + inset 0 0 0 2px rgba(236, 193, 112, 0.44), + 0 10px 20px rgba(8, 47, 73, 0.12); } .score-panel-value { font-family: var(--mono); font-size: clamp(4.4rem, 11vw, 7.2rem); line-height: 1; - color: #d5ea02; + color: #0d544a; + text-shadow: 0 2px 0 rgba(255, 255, 255, 0.42); } .scoreboard-center-banner { @@ -509,13 +548,13 @@ .scoreboard-center-banner p { font-size: clamp(1.5rem, 3vw, 2.3rem); - color: #fff; + color: #fff7e9; text-align: center; } .scoreboard-center-banner small { font-size: 0.92rem; - color: rgba(255, 255, 255, 0.7); + color: rgba(255, 247, 233, 0.76); } .scoreboard-rail { @@ -536,8 +575,27 @@ border-radius: 10px; cursor: pointer; font: inherit; - color: #111; - background: #d0d0d0; + color: #5b2f13; + background: linear-gradient(180deg, #fff8e8, #f2d9a3); + box-shadow: + inset 0 0 0 1px rgba(199, 155, 83, 0.35), + 0 8px 18px rgba(8, 47, 73, 0.14); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + filter 0.16s ease; +} + +.rail-square-button:hover { + transform: translateY(-1px); + box-shadow: + inset 0 0 0 1px rgba(199, 155, 83, 0.45), + 0 12px 20px rgba(8, 47, 73, 0.18); +} + +.rail-square-button:active { + transform: translateY(0); + filter: brightness(0.98); } .rail-clock { @@ -547,8 +605,10 @@ border-radius: 12px; font-family: var(--mono); font-size: 1.7rem; - color: #fff; - background: #060606; + color: #fff7e9; + background: + radial-gradient(circle at top right, rgba(255, 205, 96, 0.16), transparent 28%), + linear-gradient(145deg, rgba(8, 47, 73, 0.97), rgba(10, 96, 84, 0.93)); } .rail-pill { @@ -560,11 +620,30 @@ font-size: 1rem; color: #4a2e1d; background: linear-gradient(180deg, #fff0c7, #f8c870); + box-shadow: + inset 0 0 0 1px rgba(199, 155, 83, 0.28), + 0 10px 18px rgba(8, 47, 73, 0.14); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + filter 0.16s ease; +} + +.rail-pill:hover { + transform: translateY(-1px); + box-shadow: + inset 0 0 0 1px rgba(199, 155, 83, 0.34), + 0 14px 22px rgba(8, 47, 73, 0.18); +} + +.rail-pill:active { + transform: translateY(0); + filter: brightness(0.98); } .rail-pill-danger { color: #fff; - background: linear-gradient(180deg, #d41d1d, #a91212); + background: linear-gradient(180deg, #d95a44, #b53a28); } .rail-pill-muted { @@ -602,6 +681,25 @@ font-size: 2.4rem; color: #b34e3a; background: linear-gradient(180deg, #ffe5bf, #f0bd7c); + box-shadow: + inset 0 0 0 1px rgba(199, 125, 63, 0.34), + 0 14px 26px rgba(8, 47, 73, 0.2); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + filter 0.16s ease; +} + +.team-picker-close:hover { + transform: translateY(-1px) scale(1.01); + box-shadow: + inset 0 0 0 1px rgba(199, 125, 63, 0.42), + 0 18px 30px rgba(8, 47, 73, 0.24); +} + +.team-picker-close:active { + transform: translateY(0); + filter: brightness(0.98); } .team-picker-ribbon { @@ -658,6 +756,27 @@ margin-top: 4px; } +.team-picker-config { + display: grid; + gap: 8px; + color: #4a2e1d; +} + +.team-picker-config span { + font-weight: 700; +} + +.team-picker-score-input { + width: 100%; + max-width: 140px; + padding: 12px 14px; + border: 1px solid rgba(124, 98, 61, 0.22); + border-radius: 14px; + background: rgba(255, 255, 255, 0.92); + color: #2e231b; + font: inherit; +} + .team-picker-list { display: grid; gap: 14px; @@ -678,6 +797,17 @@ text-align: left; color: #2e231b; background: rgba(255, 249, 238, 0.92); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + border-color 0.16s ease, + background 0.16s ease; +} + +.team-picker-option:hover { + transform: translateY(-1px); + border-color: rgba(199, 155, 83, 0.34); + box-shadow: 0 12px 22px rgba(8, 47, 73, 0.08); } .team-picker-option-active { @@ -730,6 +860,29 @@ padding: 16px 18px; cursor: pointer; font: inherit; + box-shadow: + inset 0 0 0 1px rgba(199, 155, 83, 0.24), + 0 10px 18px rgba(8, 47, 73, 0.12); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + filter 0.16s ease; +} + +.team-picker-ghost:hover, +.team-picker-confirm:hover:not(:disabled), +.team-picker-clear:hover { + transform: translateY(-1px); + box-shadow: + inset 0 0 0 1px rgba(199, 155, 83, 0.34), + 0 14px 22px rgba(8, 47, 73, 0.16); +} + +.team-picker-ghost:active, +.team-picker-confirm:active:not(:disabled), +.team-picker-clear:active { + transform: translateY(0); + filter: brightness(0.98); } .team-picker-ghost { @@ -829,6 +982,25 @@ font-size: 1.8rem; color: #b34e3a; background: linear-gradient(180deg, #ffe5bf, #f0bd7c); + box-shadow: + inset 0 0 0 1px rgba(199, 125, 63, 0.34), + 0 10px 18px rgba(8, 47, 73, 0.16); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + filter 0.16s ease; +} + +.finish-dialog-close:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: + inset 0 0 0 1px rgba(199, 125, 63, 0.42), + 0 14px 22px rgba(8, 47, 73, 0.2); +} + +.finish-dialog-close:active:not(:disabled) { + transform: translateY(0); + filter: brightness(0.98); } .finish-score { diff --git a/src/App.tsx b/src/App.tsx index f09f09a..f95fdff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,11 +17,11 @@ import { HistoryPage } from './pages/HistoryPage' import { ScoreboardPage } from './pages/ScoreboardPage' import { TeamSelectionPage } from './pages/TeamSelectionPage' import type { + ActiveMatchup, GroupTeam, HistoryUploadPayload, LoadStatus, MatchHistoryItem, - Matchup, PointHistoryEntry, RoundGroup, ScoreSide, @@ -75,9 +75,9 @@ function App() { const [loadStatus, setLoadStatus] = useState('idle') const [loadMessage, setLoadMessage] = useState('') const [selectedGroupId, setSelectedGroupId] = useState(null) - const [matchup, setMatchup] = useState({ - leftTeamId: null, - rightTeamId: null, + const [activeMatchup, setActiveMatchup] = useState({ + leftTeam: null, + rightTeam: null, }) const [scoreState, setScoreState] = useState(initialScoreState) const [scoreHistory, setScoreHistory] = useState([]) @@ -94,10 +94,8 @@ function App() { const parsedAreaA = useMemo(() => parseRoster(areaAInput), [areaAInput]) const parsedAreaB = useMemo(() => parseRoster(areaBInput), [areaBInput]) const selectedGroup = groups.find((group) => group.id === selectedGroupId) ?? null - const leftTeam = - selectedGroup?.teams.find((team) => team.id === matchup.leftTeamId) ?? null - const rightTeam = - selectedGroup?.teams.find((team) => team.id === matchup.rightTeamId) ?? null + const leftTeam = activeMatchup.leftTeam + const rightTeam = activeMatchup.rightTeam useEffect(() => { window.localStorage.setItem(STORAGE_KEYS.targetDate, targetDate) @@ -132,19 +130,26 @@ function App() { const secondTeam = nextGroup?.teams[1] ?? null setSelectedGroupId(nextGroup?.id ?? null) - setMatchup({ - leftTeamId: firstTeam?.id ?? null, - rightTeamId: secondTeam?.id ?? null, + setActiveMatchup({ + leftTeam: firstTeam, + rightTeam: secondTeam, }) resetScoring() } - const applyMatchup = (leftTeamId: number, rightTeamId: number) => { - setMatchup({ - leftTeamId, - rightTeamId, + const applyMatchup = ( + leftTeam: GroupTeam, + rightTeam: GroupTeam, + targetScore: number, + ) => { + setActiveMatchup({ + leftTeam, + rightTeam, + }) + resetScoring({ + ...initialScoreState, + targetScore, }) - resetScoring() } const loadGroupsFromDb = async () => { @@ -163,7 +168,7 @@ function App() { if (!record) { setGroups([]) setSelectedGroupId(null) - setMatchup({ leftTeamId: null, rightTeamId: null }) + setActiveMatchup({ leftTeam: null, rightTeam: null }) setGroupSource('idle') setLoadStatus('empty') setLoadMessage('指定日期沒有資料,請改用手動配對。') @@ -181,7 +186,7 @@ function App() { } catch (error) { setGroups([]) setSelectedGroupId(null) - setMatchup({ leftTeamId: null, rightTeamId: null }) + setActiveMatchup({ leftTeam: null, rightTeam: null }) setGroupSource('idle') setLoadStatus('error') setLoadMessage(error instanceof Error ? error.message : '讀取資料失敗。') @@ -192,7 +197,7 @@ function App() { if (parsedAreaA.length === 0 || parsedAreaB.length === 0) { setGroups([]) setSelectedGroupId(null) - setMatchup({ leftTeamId: null, rightTeamId: null }) + setActiveMatchup({ leftTeam: null, rightTeam: null }) setGroupSource('idle') setLoadStatus('error') setLoadMessage('A 區與 B 區至少都要有 1 位成員。') @@ -212,9 +217,9 @@ function App() { return } - setMatchup((current) => ({ - leftTeamId: current.rightTeamId, - rightTeamId: current.leftTeamId, + setActiveMatchup((current) => ({ + leftTeam: current.rightTeam, + rightTeam: current.leftTeam, })) setScoreState((current) => ({ @@ -487,13 +492,13 @@ function App() { path="/scoreboard" element={ 0} leftTeam={leftTeam} - matchup={matchup} rightTeam={rightTeam} scoreState={scoreState} selectedGroup={selectedGroup} @@ -587,6 +592,19 @@ function formatPlayedAt(timestamp: number) { return new Date(timestamp * 1000).toLocaleString('zh-TW', { hour12: false }) } +function getSelectionOrder(leftTeam: GroupTeam | null, rightTeam: GroupTeam | null) { + if (!leftTeam || !rightTeam) { + return [] + } + + return [ + leftTeam.playerA, + leftTeam.playerB, + rightTeam.playerB, + rightTeam.playerA, + ].filter((name) => name.trim().length > 0) +} + function loadStoredText(storageKey: string, fallback: string) { const value = window.localStorage.getItem(storageKey) return value && value.trim() ? value : fallback diff --git a/src/pages/ScoreboardPage.tsx b/src/pages/ScoreboardPage.tsx index 650edf9..f23d67a 100644 --- a/src/pages/ScoreboardPage.tsx +++ b/src/pages/ScoreboardPage.tsx @@ -10,7 +10,6 @@ import { import type { CourtSide, GroupTeam, - Matchup, PlayerSlot, RoundGroup, ScoreSide, @@ -18,18 +17,22 @@ import type { } from '../types' type ScoreboardPageProps = { + currentSelectionOrder: string[] finishDialogError: string finishDialogOpen: boolean finishDialogUploading: boolean groupSource: 'idle' | 'db' | 'manual' hasRecordedPoint: boolean leftTeam: GroupTeam | null - matchup: Matchup rightTeam: GroupTeam | null scoreState: ScoreState selectedGroup: RoundGroup | null targetDate: string - onApplyMatchup: (leftTeamId: number, rightTeamId: number) => void + onApplyMatchup: ( + leftTeam: GroupTeam, + rightTeam: GroupTeam, + targetScore: number, + ) => void onCloseFinishDialog: () => void onConfirmUpload: () => void onOpenFinishDialog: () => void @@ -42,13 +45,13 @@ type ScoreboardPageProps = { } export function ScoreboardPage({ + currentSelectionOrder, finishDialogError, finishDialogOpen, finishDialogUploading, groupSource, hasRecordedPoint, leftTeam, - matchup, rightTeam, scoreState, selectedGroup, @@ -65,7 +68,8 @@ export function ScoreboardPage({ onUndoLastPoint, }: ScoreboardPageProps) { const [pickerOpen, setPickerOpen] = useState(false) - const [draftTeamIds, setDraftTeamIds] = useState([]) + const [draftPlayers, setDraftPlayers] = useState([]) + const [draftTargetScore, setDraftTargetScore] = useState(() => String(scoreState.targetScore)) const [clock, setClock] = useState(() => formatClock()) useEffect(() => { @@ -76,6 +80,29 @@ export function ScoreboardPage({ return () => window.clearInterval(timer) }, []) + const selectablePlayers = useMemo(() => { + if (!selectedGroup) { + return [] + } + + const seen = new Set() + const players: string[] = [] + + selectedGroup.teams.forEach((team) => { + if (!team.isPlaceholderA && !seen.has(team.playerA)) { + seen.add(team.playerA) + players.push(team.playerA) + } + + if (!team.isPlaceholderB && !seen.has(team.playerB)) { + seen.add(team.playerB) + players.push(team.playerB) + } + }) + + return players + }, [selectedGroup]) + const canArrangeMatch = !hasRecordedPoint const canScore = scoreState.serving !== null @@ -134,9 +161,9 @@ export function ScoreboardPage({

Step 3

-

先到選隊伍頁面建立對戰組合

+

先從選隊伍頁面帶入一組名單

- 目前還沒有可用的組別。先載入指定日期資料,或手動建立分組後,再回來開始記分。 + 記分板會依照你挑選的組別顯示球員,再從設定隊伍裡決定這一場實際對打的兩隊。

前往選隊伍 @@ -152,46 +179,59 @@ export function ScoreboardPage({ : '尚未設定對戰隊伍' const openPicker = () => { - const next = [matchup.leftTeamId, matchup.rightTeamId].filter( - (value): value is number => value !== null, - ) - - setDraftTeamIds(next) + setDraftPlayers(currentSelectionOrder.filter((player) => selectablePlayers.includes(player))) + setDraftTargetScore(String(scoreState.targetScore)) setPickerOpen(true) } - const toggleDraftTeam = (teamId: number) => { - setDraftTeamIds((current) => { - if (current.includes(teamId)) { - return current.filter((value) => value !== teamId) + const toggleDraftPlayer = (playerName: string) => { + setDraftPlayers((current) => { + if (current.includes(playerName)) { + return current.filter((value) => value !== playerName) } - if (current.length >= 2) { - return [current[1], teamId] + if (current.length >= 4) { + return current } - return [...current, teamId] + return [...current, playerName] }) } const confirmDraftTeams = () => { - if (draftTeamIds.length !== 2) { + if (draftPlayers.length !== 4) { return } - onApplyMatchup(draftTeamIds[0], draftTeamIds[1]) + onApplyMatchup( + { + id: -1, + playerA: draftPlayers[0], + playerB: draftPlayers[1], + isPlaceholderA: false, + isPlaceholderB: false, + }, + { + id: -2, + playerA: draftPlayers[3], + playerB: draftPlayers[2], + isPlaceholderA: false, + isPlaceholderB: false, + }, + sanitizeTargetScore(draftTargetScore), + ) setPickerOpen(false) } - const autoPickDraftTeams = () => { - const shuffled = [...selectedGroup.teams] + const autoPickDraftPlayers = () => { + const shuffled = [...selectablePlayers] for (let index = shuffled.length - 1; index > 0; index -= 1) { const swapIndex = Math.floor(Math.random() * (index + 1)) ;[shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex], shuffled[index]] } - setDraftTeamIds(shuffled.slice(0, 2).map((team) => team.id)) + setDraftPlayers(shuffled.slice(0, 4)) } return ( @@ -215,13 +255,13 @@ export function ScoreboardPage({ />
-

{scoreState.serving === null ? '請設定發球方' : '點擊分數開始計分'}

+

{scoreState.serving === null ? '請先設定發球方' : '點擊分數即可記分'}

{scoreState.serving === null - ? '先在上方或下方按下先攻' - : `目前發球:${currentServer?.name ?? '-'}${ + ? `本場 ${scoreState.targetScore} 分獲勝` + : `發球:${currentServer?.name ?? '-'}${ currentReceiver ? ` / 接發:${currentReceiver.name}` : '' - }`} + } / 目標 ${scoreState.targetScore} 分`}
@@ -265,18 +305,20 @@ export function ScoreboardPage({ {pickerOpen ? ( setDraftTeamIds([])} + onAutoPick={autoPickDraftPlayers} + onClear={() => setDraftPlayers([])} onClose={() => setPickerOpen(false)} onConfirm={confirmDraftTeams} - onToggleTeam={toggleDraftTeam} + onDraftTargetScoreChange={setDraftTargetScore} + onTogglePlayer={toggleDraftPlayer} /> ) : null} @@ -284,10 +326,10 @@ export function ScoreboardPage({ ) @@ -431,10 +473,11 @@ function ScoreboardTeamPanel({ } type TeamPickerModalProps = { - currentLeftTeamId: number | null - currentRightTeamId: number | null - draftTeamIds: number[] + currentSelectionOrder: string[] + draftPlayers: string[] + draftTargetScore: string group: RoundGroup + selectablePlayers: string[] selectionCount: number sourceLabel: string targetDate: string @@ -442,14 +485,16 @@ type TeamPickerModalProps = { onClear: () => void onClose: () => void onConfirm: () => void - onToggleTeam: (teamId: number) => void + onDraftTargetScoreChange: (value: string) => void + onTogglePlayer: (playerName: string) => void } function TeamPickerModal({ - currentLeftTeamId, - currentRightTeamId, - draftTeamIds, + currentSelectionOrder, + draftPlayers, + draftTargetScore, group, + selectablePlayers, selectionCount, sourceLabel, targetDate, @@ -457,41 +502,67 @@ function TeamPickerModal({ onClear, onClose, onConfirm, - onToggleTeam, + onDraftTargetScoreChange, + onTogglePlayer, }: TeamPickerModalProps) { + const draftTeams = [ + draftPlayers.length >= 2 ? `${draftPlayers[0]} / ${draftPlayers[1]}` : '尚未選滿 2 位', + draftPlayers.length >= 4 ? `${draftPlayers[2]} / ${draftPlayers[3]}` : '尚未選滿 2 位', + ] + const currentTeams = [ + currentSelectionOrder.length >= 2 + ? `${currentSelectionOrder[0]} / ${currentSelectionOrder[1]}` + : null, + currentSelectionOrder.length >= 4 + ? `${currentSelectionOrder[2]} / ${currentSelectionOrder[3]}` + : null, + ] + return (
event.stopPropagation()} role="dialog" > -
- {selectionCount >= 2 ? '已完成選擇' : '請選擇 2 隊'} + {selectionCount >= 4 ? '已完成選擇' : '請依序選 4 位球員'}
- {selectionCount}/2 + {selectionCount}/4
- 從這一組挑選要對打的隊伍 + 依序選人後自動配隊

第 {group.id} 組 / {sourceLabel} / {targetDate || '-'}

+ +
- {group.teams.map((team) => { - const checked = draftTeamIds.includes(team.id) - const selectedOrder = checked ? draftTeamIds.indexOf(team.id) + 1 : null + {selectablePlayers.map((playerName) => { + const checked = draftPlayers.includes(playerName) + const selectedOrder = checked ? draftPlayers.indexOf(playerName) + 1 : null return ( ) @@ -522,7 +595,7 @@ function TeamPickerModal({

- 選好 2 隊後按確認,會直接帶入記分板的上方與下方位置。 + 先選到的第 1、2 位會帶到上方隊伍,第 3、4 位會帶到下方隊伍。

@@ -603,7 +668,7 @@ function FinishDialog({
-

要把這場戰績上傳到 DB 嗎?

+

要把這場戰績上傳到資料庫嗎?

{error ?

{error}

: null} @@ -646,7 +711,7 @@ function FinishDialog({ type="button" onClick={onConfirm} > - {uploading ? '上傳中...' : '上傳戰績'} + {uploading ? '上傳中...' : '確認上傳'}
@@ -662,6 +727,16 @@ function getPlayerNumber(teamSlot: 'top' | 'bottom', slot: PlayerSlot) { return slot === 'playerA' ? 4 : 3 } +function sanitizeTargetScore(value: string) { + const numeric = Number.parseInt(value.replace(/[^\d]/g, ''), 10) + + if (Number.isNaN(numeric)) { + return 21 + } + + return Math.min(99, Math.max(1, numeric)) +} + function formatClock() { return new Date().toLocaleTimeString('zh-TW', { hour: '2-digit', diff --git a/src/types.ts b/src/types.ts index 63613f0..ae98a65 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,6 +30,11 @@ export type Matchup = { rightTeamId: number | null } +export type ActiveMatchup = { + leftTeam: GroupTeam | null + rightTeam: GroupTeam | null +} + export type ScoreState = { scoreLeft: number scoreRight: number