diff --git a/src/App.css b/src/App.css index 9101bb0..97dca1c 100644 --- a/src/App.css +++ b/src/App.css @@ -319,6 +319,15 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } +/* 其他區名單跨滿整行,放在 A/B 區下方。 */ +.field-area-other { + grid-column: 1 / -1; +} + +.field-area-other textarea { + min-height: 72px; +} + .field { display: grid; gap: 8px; @@ -1501,6 +1510,18 @@ white-space: nowrap; } +.team-picker-extra-badge { + margin-left: 6px; + padding: 2px 8px; + border-radius: 999px; + font-size: 0.68rem; + font-style: normal; + vertical-align: 2px; + color: #8a4d12; + background: rgba(248, 168, 45, 0.18); + box-shadow: inset 0 0 0 1px rgba(199, 108, 44, 0.32); +} + .team-picker-option strong, .preset-team-card strong { display: block; diff --git a/src/App.tsx b/src/App.tsx index 2e3eddc..be7af27 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,6 +47,7 @@ import type { const STORAGE_KEYS = { areaA: 'badminton-scoreboard::area-a', areaB: 'badminton-scoreboard::area-b', + areaOther: 'badminton-scoreboard::area-other', history: 'badminton-scoreboard::history', playCounts: 'badminton-scoreboard::play-counts', } as const @@ -123,6 +124,10 @@ function App() { const [areaBInput, setAreaBInput] = useState(() => loadStoredText(STORAGE_KEYS.areaB, defaultAreaB.join('\n')), ) + // 其他區:不參與分組配對,但會帶進記分板讓臨時上場的人可以被選到。 + const [areaOtherInput, setAreaOtherInput] = useState(() => + loadStoredText(STORAGE_KEYS.areaOther, ''), + ) const [groups, setGroups] = useState([]) const [groupSource, setGroupSource] = useState<'idle' | 'db' | 'manual'>('idle') const [loadStatus, setLoadStatus] = useState('idle') @@ -163,6 +168,7 @@ function App() { const parsedAreaA = useMemo(() => parseRoster(areaAInput), [areaAInput]) const parsedAreaB = useMemo(() => parseRoster(areaBInput), [areaBInput]) + const parsedAreaOther = useMemo(() => parseRoster(areaOtherInput), [areaOtherInput]) const selectedGroup = groups.find((group) => group.id === selectedGroupId) ?? null const leftTeam = activeMatchup.leftTeam const rightTeam = activeMatchup.rightTeam @@ -177,6 +183,10 @@ function App() { window.localStorage.setItem(STORAGE_KEYS.areaB, areaBInput) }, [areaBInput]) + useEffect(() => { + window.localStorage.setItem(STORAGE_KEYS.areaOther, areaOtherInput) + }, [areaOtherInput]) + useEffect(() => { window.localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history)) }, [history]) @@ -1068,6 +1078,7 @@ function App() { void loadGroupsFromDb()} onTargetDateChange={setTargetDate} @@ -1088,6 +1100,7 @@ function App() { void loadGroupsFromDb()} onTargetDateChange={setTargetDate} @@ -1107,6 +1121,7 @@ function App() { element={ = { type ScoreboardPageProps = { currentSelectionOrder: string[] + extraPlayers: string[] finishDialogError: string finishDialogOpen: boolean finishDialogUploading: boolean @@ -92,6 +93,7 @@ type ScoreboardPageProps = { export function ScoreboardPage({ currentSelectionOrder, + extraPlayers, finishDialogError, finishDialogOpen, finishDialogUploading, @@ -189,8 +191,37 @@ export function ScoreboardPage({ } }) + // 其他區的人排在正規名單後面,一樣可以被選上場。 + extraPlayers.forEach((player) => { + if (!seen.has(player)) { + seen.add(player) + players.push(player) + } + }) + return players - }, [selectedGroup]) + }, [selectedGroup, extraPlayers]) + + // 標記哪些人來自其他區:不參與自動選擇,選人面板上也會標示。 + const extraPlayerSet = useMemo(() => { + if (!selectedGroup) { + return new Set() + } + + const groupPlayers = new Set() + + selectedGroup.teams.forEach((team) => { + if (!team.isPlaceholderA) { + groupPlayers.add(team.playerA) + } + + if (!team.isPlaceholderB) { + groupPlayers.add(team.playerB) + } + }) + + return new Set(extraPlayers.filter((player) => !groupPlayers.has(player))) + }, [selectedGroup, extraPlayers]) const presetTeams = useMemo( () => @@ -500,8 +531,9 @@ export function ScoreboardPage({ } const autoPickDraftPlayers = () => { + // 其他區的人只能手動勾選,自動選擇留給正規輪替的名單。 const shuffled = selectablePlayers.filter( - (player) => !AUTO_PICK_EXCLUDED_NAMES.has(player), + (player) => !AUTO_PICK_EXCLUDED_NAMES.has(player) && !extraPlayerSet.has(player), ) for (let index = shuffled.length - 1; index > 0; index -= 1) { @@ -697,6 +729,7 @@ export function ScoreboardPage({ playCounts: Record draftTargetScore: string group: RoundGroup @@ -905,6 +939,7 @@ type TeamPickerModalProps = { function TeamPickerModal({ draftPlayers, + extraPlayerSet, playCounts, draftTargetScore, group, @@ -969,6 +1004,7 @@ function TeamPickerModal({ const checked = draftPlayers.includes(playerName) const selectedOrder = checked ? draftPlayers.indexOf(playerName) + 1 : null const playedCount = playCounts[playerName] ?? 0 + const isExtra = extraPlayerSet.has(playerName) return (