Refond l'application en parcours multi-pages

This commit is contained in:
2026-04-12 11:52:31 +02:00
parent 87fcbc3e07
commit 3441b21de6
6 changed files with 2016 additions and 1460 deletions

View File

@@ -5,12 +5,14 @@ Application web mobile-first pour téléphone et tablette, pensée comme applica
## Ce que fait cette première version ## Ce que fait cette première version
- configure une rencontre `Twice` ou `Time` - configure une rencontre `Twice` ou `Time`
- sépare l'application en pages dédiées : configuration, phase chrono, phase cube
- gère les blocks de 180 secondes et le temps par coup de 20 secondes - gère les blocks de 180 secondes et le temps par coup de 20 secondes
- suit les quotas `FAST`, `FREEZE` et `MASTERS` - suit les quotas `FAST`, `FREEZE` et `MASTERS`
- orchestre la phase cube avec désignation du cube, capture des temps et préparation du block suivant - orchestre la phase cube avec désignation du cube, capture des temps et préparation du block suivant
- applique la logique du double coup V2 en `Twice` - applique la logique du double coup V2 en `Twice`
- applique les ajustements `bloc -` et `bloc +` en `Time` avec plafond de 120 s pris en compte - applique les ajustements `bloc -` et `bloc +` en `Time` avec plafond de 120 s pris en compte
- conserve un historique local dans le navigateur - conserve un historique local dans le navigateur
- propose une page chrono pensée pour le téléphone avec deux grandes zones tactiles, une par joueur
## Hypothèse de produit ## Hypothèse de produit
@@ -27,7 +29,9 @@ L'application est ensuite disponible sur `http://localhost:8080`.
## Fichiers clés ## Fichiers clés
- `index.html` : structure de l'interface - `index.html` : page de configuration et reprise de match
- `chrono.html` : page dédiée à la phase chrono
- `cube.html` : page dédiée à la phase cube
- `styles.css` : design mobile/tablette - `styles.css` : design mobile/tablette
- `app.js` : logique de match et arbitrage - `app.js` : logique de match et arbitrage
- `docker-compose.yml` + `Dockerfile` : exécution locale - `docker-compose.yml` + `Dockerfile` : exécution locale

1957
app.js

File diff suppressed because it is too large Load Diff

143
chrono.html Normal file
View File

@@ -0,0 +1,143 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Phase chrono de ChessCubing Arena avec gros boutons face-à-face."
/>
<title>ChessCubing Arena | Phase Chrono</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body data-page="chrono" class="phase-body">
<main class="phase-shell">
<header class="phase-header">
<a class="brand-link" href="index.html">Configuration</a>
<div class="phase-title">
<p class="eyebrow">Phase chrono</p>
<h1 id="chronoTitle">Block 1</h1>
<p id="chronoSubtitle" class="phase-subtitle"></p>
</div>
<button class="button ghost small utility-button" id="openArbiterButton" type="button">
Arbitre
</button>
</header>
<section class="status-strip">
<article class="status-card">
<span>Temps block</span>
<strong id="blockTimer">03:00</strong>
</article>
<article class="status-card">
<span>Temps coup</span>
<strong id="moveTimer">00:20</strong>
</article>
<article class="status-card wide">
<span id="chronoCenterLabel">Trait</span>
<strong id="chronoCenterValue">Blanc</strong>
</article>
</section>
<section class="faceoff-board">
<article class="player-zone opponent-zone" id="blackZone">
<div class="zone-inner mirrored-mobile">
<div class="zone-head">
<div>
<span class="seat-tag dark-seat">Noir</span>
<h2 id="blackNameChrono">Noir</h2>
</div>
<div class="zone-stats">
<strong id="blackMovesChrono">0 / 6</strong>
<span id="blackClockChrono"></span>
</div>
</div>
<button class="zone-button dark-button" id="blackMoveButton" type="button">
En attente
</button>
<p class="zone-foot" id="blackHintChrono"></p>
</div>
</article>
<article class="phase-spine">
<div class="spine-card">
<p class="micro-label" id="spineLabel">État du block</p>
<strong id="spineHeadline">Prêt à démarrer</strong>
<p id="spineText"></p>
</div>
<button class="button primary spine-button" id="primaryChronoButton" type="button">
Démarrer le block
</button>
</article>
<article class="player-zone" id="whiteZone">
<div class="zone-inner">
<div class="zone-head">
<div>
<span class="seat-tag light-seat">Blanc</span>
<h2 id="whiteNameChrono">Blanc</h2>
</div>
<div class="zone-stats">
<strong id="whiteMovesChrono">0 / 6</strong>
<span id="whiteClockChrono"></span>
</div>
</div>
<button class="zone-button light-button" id="whiteMoveButton" type="button">
En attente
</button>
<p class="zone-foot" id="whiteHintChrono"></p>
</div>
</article>
</section>
</main>
<section class="modal hidden" id="arbiterModal" aria-hidden="true">
<div class="modal-backdrop" data-close-modal="true"></div>
<div class="modal-card">
<div class="modal-head">
<div>
<p class="eyebrow">Outils arbitre</p>
<h2>Contrôles avancés</h2>
</div>
<button class="button ghost small" id="closeArbiterButton" type="button">Fermer</button>
</div>
<p class="section-copy" id="arbiterStatus"></p>
<div class="modal-actions">
<button class="button secondary" id="arbiterPauseButton" type="button">
Pause / reprise
</button>
<button class="button secondary" id="arbiterCloseBlockButton" type="button">
Clore le block
</button>
<button class="button secondary" id="arbiterTimeoutButton" type="button">
Dépassement 20 s
</button>
<button class="button secondary" id="arbiterSwitchTurnButton" type="button">
Corriger le trait
</button>
<button class="button ghost" id="arbiterWhiteWinButton" type="button">
Blanc gagne
</button>
<button class="button ghost" id="arbiterBlackWinButton" type="button">
Noir gagne
</button>
<button class="button ghost danger" id="arbiterStopButton" type="button">
Abandon / arrêt
</button>
<button class="button ghost" id="arbiterResetButton" type="button">
Réinitialiser le match
</button>
</div>
</div>
</section>
<script type="module" src="app.js"></script>
</body>
</html>

127
cube.html Normal file
View File

@@ -0,0 +1,127 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Phase cube de ChessCubing Arena avec gros boutons de fin."
/>
<title>ChessCubing Arena | Phase Cube</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body data-page="cube" class="phase-body">
<main class="phase-shell cube-shell">
<header class="phase-header">
<a class="brand-link" href="index.html">Configuration</a>
<div class="phase-title">
<p class="eyebrow">Phase cube</p>
<h1 id="cubeTitle">Cube n°1</h1>
<p id="cubeSubtitle" class="phase-subtitle"></p>
</div>
<button class="button ghost small utility-button" id="openCubeHelpButton" type="button">
Arbitre
</button>
</header>
<section class="status-strip">
<article class="status-card">
<span>Block</span>
<strong id="cubeBlockLabel">1</strong>
</article>
<article class="status-card">
<span>Chrono cube</span>
<strong id="cubeElapsed">00:00.0</strong>
</article>
<article class="status-card wide">
<span id="cubeCenterLabel">État</span>
<strong id="cubeCenterValue">Prêt</strong>
</article>
</section>
<section class="faceoff-board">
<article class="player-zone opponent-zone" id="blackCubeZone">
<div class="zone-inner mirrored-mobile">
<div class="zone-head">
<div>
<span class="seat-tag dark-seat">Noir</span>
<h2 id="blackNameCube">Noir</h2>
</div>
<div class="zone-stats">
<strong id="blackCubeResult">--</strong>
<span id="blackCubeCap"></span>
</div>
</div>
<button class="zone-button dark-button" id="blackCubeButton" type="button">
Prêt
</button>
<p class="zone-foot" id="blackHintCube"></p>
</div>
</article>
<article class="phase-spine">
<div class="spine-card">
<p class="micro-label" id="cubeSpineLabel">Blocage</p>
<strong id="cubeSpineHeadline">Lancer la phase cube</strong>
<p id="cubeSpineText"></p>
</div>
<button class="button primary spine-button" id="primaryCubeButton" type="button">
Démarrer la phase cube
</button>
</article>
<article class="player-zone" id="whiteCubeZone">
<div class="zone-inner">
<div class="zone-head">
<div>
<span class="seat-tag light-seat">Blanc</span>
<h2 id="whiteNameCube">Blanc</h2>
</div>
<div class="zone-stats">
<strong id="whiteCubeResult">--</strong>
<span id="whiteCubeCap"></span>
</div>
</div>
<button class="zone-button light-button" id="whiteCubeButton" type="button">
Prêt
</button>
<p class="zone-foot" id="whiteHintCube"></p>
</div>
</article>
</section>
</main>
<section class="modal hidden" id="cubeHelpModal" aria-hidden="true">
<div class="modal-backdrop" data-close-cube-modal="true"></div>
<div class="modal-card">
<div class="modal-head">
<div>
<p class="eyebrow">Outils arbitre</p>
<h2>Phase cube</h2>
</div>
<button class="button ghost small" id="closeCubeHelpButton" type="button">
Fermer
</button>
</div>
<p class="section-copy" id="cubeHelpStatus"></p>
<div class="modal-actions">
<button class="button secondary" id="replayCubeButton" type="button">
Rejouer la phase cube
</button>
<button class="button ghost danger" id="cubeResetButton" type="button">
Réinitialiser le match
</button>
</div>
</div>
</section>
<script type="module" src="app.js"></script>
</body>
</html>

View File

@@ -5,72 +5,62 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
name="description" name="description"
content="Application officielle de suivi de match pour ChessCubing Twice et ChessCubing Time." content="Configuration de match pour ChessCubing Arena sur téléphone et tablette."
/> />
<title>ChessCubing Arena</title> <title>ChessCubing Arena | Configuration</title>
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
</head> </head>
<body> <body data-page="setup">
<div class="ambient ambient-left"></div> <div class="ambient ambient-left"></div>
<div class="ambient ambient-right"></div> <div class="ambient ambient-right"></div>
<div class="layout">
<header class="hero"> <div class="setup-shell">
<header class="hero hero-setup">
<div class="hero-copy"> <div class="hero-copy">
<p class="eyebrow">Application officielle de match</p> <p class="eyebrow">Application officielle de match</p>
<h1>ChessCubing Arena</h1> <h1>ChessCubing Arena</h1>
<p class="lead"> <p class="lead">
Une web app pensée pour l'arbitrage sur téléphone et tablette, Une version mobile et tablette organisée par phases : configuration,
directement dérivée des règlements de page chrono ultra lisible, puis page cube dédiée.
<strong>ChessCubing Twice</strong> et
<strong>ChessCubing Time</strong>.
</p> </p>
<div class="hero-actions">
<button class="button primary" data-scroll-target="setupPanel">
Configurer une rencontre
</button>
<button class="button secondary" data-scroll-target="rulesPanel">
Voir la synthèse du règlement
</button>
</div>
<div class="hero-pills"> <div class="hero-pills">
<span>Mobile-first</span> <span>Pages séparées</span>
<span>Face-à-face téléphone</span>
<span>Twice + Time</span> <span>Twice + Time</span>
<span>Arbitrage en direct</span> <span>Arbitrage tactile</span>
<span>Fonctionne hors build</span>
</div> </div>
</div> </div>
<aside class="hero-preview"> <aside class="hero-preview">
<div class="preview-card"> <div class="preview-card">
<p class="preview-label">Ce que l'application gère</p> <p class="micro-label">Flux de match</p>
<ul class="preview-list"> <ol class="phase-list">
<li>Blocks de 180 s et quotas FAST / FREEZE / MASTERS</li> <li>Configurer la rencontre</li>
<li>Temps par coup de 20 s</li> <li>Passer à la page chrono</li>
<li>Phase cube, égalité, gagnant et block suivant</li> <li>Basculer automatiquement sur la page cube</li>
<li>Mode Time avec chronos cumulés et blocs - / +</li> <li>Revenir sur la page chrono pour le block suivant</li>
</ul> </ol>
</div> </div>
<div class="preview-banner"> <div class="preview-banner">
<span class="mini-chip">Block 1</span> <span class="mini-chip">Téléphone</span>
<strong id="heroModeHint">Twice ou Time, selon la configuration</strong> <strong>Un joueur en haut, un joueur en bas</strong>
<p> <p>
L'app sert de chef d'orchestre pendant la partie sans imposer un La zone du haut s'inverse automatiquement pour que les deux
échiquier numérique. joueurs se fassent face autour de l'appareil.
</p> </p>
</div> </div>
</aside> </aside>
</header> </header>
<main class="workspace"> <main class="setup-grid">
<section class="panel" id="setupPanel"> <section class="panel">
<div class="section-heading"> <div class="section-heading">
<div> <div>
<p class="eyebrow">Préparer la rencontre</p> <p class="eyebrow">Nouvelle rencontre</p>
<h2>Configuration de match</h2> <h2>Configuration</h2>
</div> </div>
<p class="section-copy"> <p class="section-copy">
Lancement rapide pour club, démo ou tournoi. Les choix pilotent le Les réglages ci-dessous préparent les pages chrono et cube.
tableau d'arbitrage en direct.
</p> </p>
</div> </div>
@@ -92,16 +82,16 @@
<input type="radio" name="mode" value="twice" checked /> <input type="radio" name="mode" value="twice" checked />
<strong>ChessCubing Twice</strong> <strong>ChessCubing Twice</strong>
<span> <span>
Le gagnant du cube commence le block suivant et peut obtenir Le gagnant du cube ouvre le block suivant et peut obtenir un
un double coup selon la règle V2. double coup V2.
</span> </span>
</label> </label>
<label class="option-card"> <label class="option-card">
<input type="radio" name="mode" value="time" /> <input type="radio" name="mode" value="time" />
<strong>ChessCubing Time</strong> <strong>ChessCubing Time</strong>
<span> <span>
Même structure de blocks, avec deux chronos cumulés et un Même structure de blocks, avec chronos cumulés et alternance
impact cube en bloc - / +. bloc - / bloc +.
</span> </span>
</label> </label>
</div> </div>
@@ -113,41 +103,29 @@
<label class="option-card"> <label class="option-card">
<input type="radio" name="preset" value="fast" checked /> <input type="radio" name="preset" value="fast" checked />
<strong>FAST</strong> <strong>FAST</strong>
<span>6 coups par joueur et par block</span> <span>6 coups par joueur</span>
</label> </label>
<label class="option-card"> <label class="option-card">
<input type="radio" name="preset" value="freeze" /> <input type="radio" name="preset" value="freeze" />
<strong>FREEZE</strong> <strong>FREEZE</strong>
<span>8 coups par joueur et par block</span> <span>8 coups par joueur</span>
</label> </label>
<label class="option-card"> <label class="option-card">
<input type="radio" name="preset" value="masters" /> <input type="radio" name="preset" value="masters" />
<strong>MASTERS</strong> <strong>MASTERS</strong>
<span>10 coups par joueur et par block</span> <span>10 coups par joueur</span>
</label> </label>
</div> </div>
</fieldset> </fieldset>
<label class="field"> <label class="field">
<span>Joueur blanc</span> <span>Joueur blanc</span>
<input <input name="whiteName" type="text" maxlength="40" placeholder="Blanc" required />
name="whiteName"
type="text"
maxlength="40"
placeholder="Blanc"
required
/>
</label> </label>
<label class="field"> <label class="field">
<span>Joueur noir</span> <span>Joueur noir</span>
<input <input name="blackName" type="text" maxlength="40" placeholder="Noir" required />
name="blackName"
type="text"
maxlength="40"
placeholder="Noir"
required
/>
</label> </label>
<label class="field"> <label class="field">
@@ -166,243 +144,77 @@
name="eventName" name="eventName"
type="text" type="text"
maxlength="60" maxlength="60"
placeholder="Club local, tournoi, démonstration" placeholder="Club, tournoi, démonstration"
/> />
</label> </label>
<label class="field span-2"> <label class="field span-2">
<span>Notes de mise en place</span> <span>Notes</span>
<textarea <textarea
name="notes" name="notes"
rows="3" rows="3"
placeholder="Tirage au sort effectué, app lancée, cubes vérifiés..." placeholder="Tirage au sort effectué, 8 cubes vérifiés, variante prête..."
></textarea> ></textarea>
</label> </label>
<div class="setup-summary span-2" id="setupSummary"></div> <div id="setupSummary" class="setup-summary span-2"></div>
<div class="setup-actions span-2"> <div class="setup-actions span-2">
<button class="button primary" type="submit"> <button class="button primary" type="submit">
Lancer le match Ouvrir la page chrono
</button> </button>
<button class="button ghost" type="button" id="loadDemoButton"> <button class="button secondary" id="loadDemoButton" type="button">
Charger une démo Charger une démo
</button> </button>
</div> </div>
</form> </form>
</section> </section>
<section class="panel live-panel hidden" id="livePanel"> <aside class="panel side-panel">
<div class="section-heading"> <div class="section-heading">
<div> <div>
<p class="eyebrow">Direction de match</p> <p class="eyebrow">Match en mémoire</p>
<h2 id="matchTitle">Rencontre en direct</h2> <h2>Reprise rapide</h2>
</div>
<div class="status-badge" id="phaseBadge">Prêt</div>
</div>
<div class="live-grid">
<article class="panel inset score-panel">
<div class="score-head">
<div>
<p class="micro-label" id="blockLabel">Block 1</p>
<h3 id="modeLabel">ChessCubing Twice</h3>
<p class="section-copy" id="blockMeta">
Les Blancs commencent le block 1.
</p>
</div>
<button class="button ghost small" id="resetMatchButton">
Réinitialiser
</button>
</div>
<div class="timer-grid">
<div class="timer-card emphasized">
<span>Chrono du block</span>
<strong id="blockTimer">03:00</strong>
<p id="blockStatusText">En attente du démarrage.</p>
</div>
<div class="timer-card">
<span>Temps par coup</span>
<strong id="moveTimer">00:20</strong>
<p id="turnLabel">Trait : Blanc</p>
</div> </div>
</div> </div>
<div class="player-grid"> <div id="resumeCard" class="resume-card empty">
<section class="player-card white-seat" id="whiteCard"> <p>Aucun match en cours pour l'instant.</p>
<div class="player-name-row">
<span class="player-color">Blanc</span>
<strong id="whiteNameDisplay">Blanc</strong>
</div>
<p id="whiteMoveCount">0 / 6 coups</p>
<p id="whiteClockLabel" class="muted"></p>
<p id="whiteCubeLabel" class="muted"></p>
</section>
<section class="player-card black-seat" id="blackCard">
<div class="player-name-row">
<span class="player-color">Noir</span>
<strong id="blackNameDisplay">Noir</strong>
</div>
<p id="blackMoveCount">0 / 6 coups</p>
<p id="blackClockLabel" class="muted"></p>
<p id="blackCubeLabel" class="muted"></p>
</section>
</div>
</article>
<article class="panel inset">
<h3>Commandes d'arbitrage</h3>
<div class="action-grid">
<button class="button primary" id="startPauseButton">
Démarrer le block
</button>
<button class="button secondary" id="confirmBlockButton">
Clore le block
</button>
<button class="button secondary" id="moveActionButton">
Coup compté
</button>
<button class="button secondary" id="reliefMoveButton">
Coup hors quota
</button>
<button class="button secondary" id="timeoutMoveButton">
Dépassement 20 s
</button>
<button class="button ghost" id="switchTurnButton">
Corriger le trait
</button>
</div> </div>
<div class="notice-card" id="contextNotice"></div> <div class="rules-stack">
<div class="double-card" id="doubleCard"></div> <article class="rule-card">
<span class="micro-label">Page chrono</span>
<div class="result-grid"> <strong>Gros boutons uniquement</strong>
<button class="button ghost" id="whiteWinButton">
Blanc gagne
</button>
<button class="button ghost" id="blackWinButton">
Noir gagne
</button>
<button class="button ghost danger" id="drawStopButton">
Abandon / arrêt
</button>
</div>
</article>
<article class="panel inset">
<h3>Phase cube</h3>
<div class="cube-head">
<div>
<span class="micro-label">Cube désigné</span>
<strong id="cubeNumber">-</strong>
</div>
<button class="button secondary" id="startCubeButton">
Démarrer la phase cube
</button>
</div>
<div class="cube-clock">
<strong id="cubeElapsed">00:00.0</strong>
<p id="cubeStatusText">
La phase cube se déclenche à la fin du block.
</p>
</div>
<div class="capture-grid">
<button class="button capture" id="captureWhiteCubeButton">
Arrêt Blanc
</button>
<button class="button capture" id="captureBlackCubeButton">
Arrêt Noir
</button>
</div>
<div class="cube-results">
<div>
<span>Blanc</span>
<strong id="whiteCubeResult">--</strong>
<small id="whiteCubeCap"></small>
</div>
<div>
<span>Noir</span>
<strong id="blackCubeResult">--</strong>
<small id="blackCubeCap"></small>
</div>
</div>
<div class="action-grid compact">
<button class="button primary" id="applyCubeButton">
Appliquer et préparer le block suivant
</button>
<button class="button ghost" id="redoCubeButton">
Rejouer la phase cube
</button>
</div>
</article>
<article class="panel inset">
<h3>Historique</h3>
<ul class="history-list" id="historyList"></ul>
</article>
<article class="panel inset" id="rulesPanel">
<h3>Synthèse règlementaire</h3>
<div class="rules-grid">
<div class="rule-card">
<span class="micro-label">Twice</span>
<strong>Blocks et cube</strong>
<p> <p>
Block de 180 s, 20 s max par coup, phase cube obligatoire Chaque joueur dispose d'une grande zone tactile pour signaler la
entre chaque block, gagnant du cube au départ suivant. fin de son coup, avec le haut de l'écran inversé sur téléphone.
</p> </p>
</div>
<div class="rule-card">
<span class="micro-label">Twice</span>
<strong>Double coup V2</strong>
<p>
Accordé si le gagnant du cube n'a pas joué le dernier coup
du block précédent. Premier coup gratuit sans échec,
deuxième coup compté.
</p>
</div>
<div class="rule-card">
<span class="micro-label">Time</span>
<strong>Impact cube</strong>
<p>
Block impair : le temps cube est retiré de son propre
chrono. Block pair : il est ajouté au chrono adverse, avec
plafond de 120 s pris en compte.
</p>
</div>
<div class="rule-card">
<span class="micro-label">Arbitrage</span>
<strong>Vérifications clés</strong>
<p>
Huit cubes, caches numérotés, mélanges identiques,
application lancée, variante choisie, tirage au sort fait,
aucun coup pendant la phase cube.
</p>
</div>
</div>
</article> </article>
</div> <article class="rule-card">
</section> <span class="micro-label">Page cube</span>
</main> <strong>Une page dédiée</strong>
<p>
<footer class="footer"> Le cube désigné, le chrono commun et les arrêts Blanc / Noir
sont isolés sur leur propre écran.
</p>
</article>
<article class="rule-card">
<span class="micro-label">Sources</span>
<strong>Règlements intégrés</strong>
<p> <p>
Sources intégrées :
<a href="ChessCubing_Twice_Reglement_Officiel_V2-1.pdf" target="_blank" <a href="ChessCubing_Twice_Reglement_Officiel_V2-1.pdf" target="_blank"
>règlement ChessCubing Twice</a >Règlement ChessCubing Twice</a
> >
et <br />
<a href="ChessCubing_Time_Reglement_Officiel_V1-1.pdf" target="_blank" <a href="ChessCubing_Time_Reglement_Officiel_V1-1.pdf" target="_blank"
>règlement ChessCubing Time</a >Règlement ChessCubing Time</a
>. >
</p> </p>
</footer> </article>
</div>
</aside>
</main>
</div> </div>
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>

File diff suppressed because it is too large Load Diff