精簡選隊伍頁提示並更新 README

This commit is contained in:
2026-04-16 10:55:41 +08:00
parent bbedb70e7e
commit 98c289ea5b
4 changed files with 176 additions and 245 deletions

View File

@@ -178,6 +178,37 @@
background: rgba(255, 214, 224, 0.94);
}
.floating-status-bubble {
position: fixed;
left: 50%;
top: 50%;
z-index: 60;
transform: translate(-50%, -50%);
min-width: min(78vw, 320px);
max-width: min(84vw, 420px);
padding: 14px 20px;
border-radius: 999px;
text-align: center;
font-weight: 700;
color: #f8fff9;
background: linear-gradient(135deg, rgba(8, 47, 73, 0.96), rgba(10, 96, 84, 0.92));
box-shadow: 0 18px 40px rgba(8, 47, 73, 0.28);
pointer-events: none;
animation: status-bubble-in 0.18s ease-out;
}
@keyframes status-bubble-in {
from {
opacity: 0;
transform: translate(-50%, calc(-50% + 10px)) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
.selection-shell,
.selection-form,
.group-board {

View File

@@ -113,6 +113,18 @@ function App() {
window.localStorage.setItem(STORAGE_KEYS.history, JSON.stringify(history))
}, [history])
useEffect(() => {
if (loadStatus !== 'loaded' || !loadMessage) {
return
}
const timer = window.setTimeout(() => {
setLoadMessage('')
}, 500)
return () => window.clearTimeout(timer)
}, [loadMessage, loadStatus])
const resetScoring = (nextState: ScoreState = initialScoreState) => {
setScoreState(nextState)
setScoreHistory([])
@@ -450,8 +462,6 @@ function App() {
groupSource={groupSource}
loadMessage={loadMessage}
loadStatus={loadStatus}
parsedAreaA={parsedAreaA}
parsedAreaB={parsedAreaB}
selectedGroupId={selectedGroupId}
targetDate={targetDate}
onAreaAInputChange={setAreaAInput}
@@ -474,8 +484,6 @@ function App() {
groupSource={groupSource}
loadMessage={loadMessage}
loadStatus={loadStatus}
parsedAreaA={parsedAreaA}
parsedAreaB={parsedAreaB}
selectedGroupId={selectedGroupId}
targetDate={targetDate}
onAreaAInputChange={setAreaAInput}

View File

@@ -1,4 +1,4 @@
import { useNavigate } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { getTeamDisplayName } from '../lib/match'
import type { LoadStatus, RoundGroup } from '../types'
@@ -9,8 +9,6 @@ type TeamSelectionPageProps = {
groupSource: 'idle' | 'db' | 'manual'
loadMessage: string
loadStatus: LoadStatus
parsedAreaA: string[]
parsedAreaB: string[]
selectedGroupId: number | null
targetDate: string
onAreaAInputChange: (value: string) => void
@@ -29,8 +27,6 @@ export function TeamSelectionPage({
groupSource,
loadMessage,
loadStatus,
parsedAreaA,
parsedAreaB,
selectedGroupId,
targetDate,
onAreaAInputChange,
@@ -41,41 +37,21 @@ export function TeamSelectionPage({
onTargetDateChange,
onUseGroup,
}: TeamSelectionPageProps) {
const navigate = useNavigate()
const hasGroups = groups.length > 0
const showInlineStatus = loadStatus !== 'idle' && loadStatus !== 'loaded' && Boolean(loadMessage)
const sourceLabel =
groupSource === 'db' ? '資料庫載入' : groupSource === 'manual' ? '手動產生' : '尚未建立'
return (
<section className="selection-shell">
<article className="panel panel-hero selection-hero">
<div>
<p className="panel-kicker"> 1</p>
<h2></h2>
<p className="panel-copy">
DB A B
</p>
<section className="page-grid">
{loadStatus === 'loaded' && loadMessage ? (
<div className="floating-status-bubble" role="status" aria-live="polite">
{loadMessage}
</div>
<div className="summary-grid">
<article className="mini-stat">
<span>A </span>
<strong>{parsedAreaA.length}</strong>
</article>
<article className="mini-stat">
<span>B </span>
<strong>{parsedAreaB.length}</strong>
</article>
<article className="mini-stat">
<span></span>
<strong>{groups.length}</strong>
</article>
</div>
{loadMessage ? (
<p className={`status-banner status-banner-${loadStatus}`}>{loadMessage}</p>
) : null}
</article>
) : null}
<article className="panel">
<div className="selection-form">
<div className="selection-shell">
<div className="selection-toolbar">
<label className="field">
<span></span>
@@ -88,74 +64,64 @@ export function TeamSelectionPage({
<div className="button-stack">
<button className="primary-button" type="button" onClick={onLoadGroupsFromDb}>
</button>
<button className="secondary-button" type="button" onClick={onGenerateManualGroups}>
</button>
</div>
</div>
{showInlineStatus ? (
<div className={`status-banner status-banner-${loadStatus}`}>{loadMessage}</div>
) : null}
<div className="double-grid">
<label className="field">
<span>A </span>
<textarea
rows={8}
placeholder="每行一位球員"
value={areaAInput}
onChange={(event) => onAreaAInputChange(event.target.value)}
placeholder={'每行一隊,例如:\n柏威'}
/>
</label>
<label className="field">
<span>B </span>
<textarea
rows={8}
placeholder="每行一位球員"
value={areaBInput}
onChange={(event) => onAreaBInputChange(event.target.value)}
placeholder={'每行一隊,例如:\nRURU'}
/>
</label>
</div>
<div className="selection-hint">
<span>
{groupSource === 'db' ? 'DB' : groupSource === 'manual' ? '手動配對' : '尚未載入'}
</span>
<span></span>
<span>{sourceLabel}</span>
</div>
</div>
</article>
<article className="panel full-span">
<div className="panel-heading">
<article className="panel">
<div className="group-head">
<div>
<p className="panel-kicker"> 2</p>
<h2></h2>
<p className="panel-kicker">Step 2</p>
<h2></h2>
</div>
{selectedGroupId ? <span className="winner-badge"> {selectedGroupId} </span> : null}
</div>
{groups.length === 0 ? (
<div className="empty-state">
<h3></h3>
<p></p>
</div>
) : (
<div className="group-board">
{groups.map((group) => (
<div className="group-board">
{hasGroups ? (
groups.map((group) => (
<article
className={
group.id === selectedGroupId
? 'group-card group-card-active group-card-stage'
: 'group-card group-card-stage'
}
key={group.id}
className={`group-card ${selectedGroupId === group.id ? 'group-card-active' : ''}`}
>
<div className="group-head">
<div>
<p className="panel-kicker"> {group.id} </p>
<h3>{group.teams.length} </h3>
<h3></h3>
</div>
<div className="group-actions">
<button
@@ -163,33 +129,31 @@ export function TeamSelectionPage({
type="button"
onClick={() => onSelectGroup(group.id)}
>
</button>
<button
className="primary-button"
type="button"
onClick={() => {
onUseGroup(group.id)
navigate('/scoreboard')
}}
>
</button>
<Link className="primary-button inline-link" to="/scoreboard" onClick={() => onUseGroup(group.id)}>
</Link>
</div>
</div>
<div className="team-stage-grid">
{group.teams.map((team) => (
<article className="team-stage-card" key={`${group.id}-${team.id}`}>
<span className="team-index"> {team.id} </span>
<p className="team-name">{getTeamDisplayName(team)}</p>
<article key={`${group.id}-${team.id}`} className="team-stage-card">
<span className="team-index"> {team.id}</span>
<div className="team-name">{getTeamDisplayName(team)}</div>
</article>
))}
</div>
</article>
))}
</div>
)}
))
) : (
<div className="empty-state">
<h3></h3>
<p> AB </p>
</div>
)}
</div>
</article>
</section>
)