Crée l'application web mobile ChessCubing Arena

This commit is contained in:
2026-04-12 11:30:49 +02:00
commit 87fcbc3e07
11 changed files with 2462 additions and 0 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
.git
.codex
WhatsApp Video 2026-04-11 at 20.38.50.mp4

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.codex
WhatsApp Video 2026-04-11 at 20.38.50.mp4

View File

@@ -0,0 +1,93 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Symbol /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
>>
endobj
7 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260204130259+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260204130259+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
8 0 obj
<<
/Count 2 /Kids [ 4 0 R 5 0 R ] /Type /Pages
>>
endobj
9 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1359
>>
stream
Gatn&>Bed\&BE]".HWAPDHogm(\b&*UmlFgCq"E]`/?;V-u2J9/nt%6mmmXTY,H=MkC>:mju9'Gr#q0]MhLi)!B;^j_[-?1TtCGi-TRE(0J*fQ%`itXO=%ks('I+[+<e_.^ut3"I#r1=U-g=J+Ln);\0-KG!L3G__WG@CGoC"8$&@DM)+hT5:!]^R-2`13E]Q4OT"TdI)PDHg7p2Dtj".%_2M[L2VO*X5n&M=B?_BMET>Kr614`=[I.f0TjI[T0RM:%U2N@Y7i9Mg6da='c)l3AVABDW6'?F+%m/uBs3W-_$&PBnN!],]t:SlUJCd9t!$+FC.\C9pZEk([aDmeLABM+%!bicNkj'p4)oRane06k4Mc3*\u5a)>bC*>+TE`dW?l/+=u,H+/<f-hM")m7;?)`)JT+d_0#J/\kk6DG;9.b1mI`!S#t@.iD^Ms`2MB@=!Dcn]GALBRmOqR+k2<sG[i<`1g6iD'+7`gO^J2pe^p_%/E,`KF@M1k*9c_d]BUBDj"5NGlo3YM\+8@e+&Z6jCX0Lch:oB5HUkO2N6PUg1N+Tm<ce5@_+"/8mL;!Fm=U\8g`J/Bb;/\\!KJK6s<j.B4fidG(nc_&*ZJlUKgo2/AR5^`;3F=p*_&^T2*^`,4Z?4pRS;_kY'p(6enZB6'\V%!hj+jJN<S1E-s5nCegO)!5le[Hjo]A$/3Bd&]b,Fr<-OE>YF2&N8*%2NKQS"CmJFj84p^?$O+7F?<I=Z4=pmI!P(6aiF?f`Xf,p5W6gL.)UM*LD7:"^46,sl7]@Mqlrg3VBRo2U]ArN:gVS(NhTN5WK8!nX1:=o..7C_mVUNK/eMWljBI?0Ufr&Z?[NGB]lNcO55Q\UF]n=/q*l4iRC&U?[MBSc/UFnLWN.OH@K"[f`L?JDeaF-<SdXlr&[k0(QR?u8kT`+%hJ61)9d@jXQofmTFE&ID/@-86$qUKs]X'dB[m_3d62G#;G3dNso$;HbdGp0aS3E4$MjUA1Mgc?V-q?+L+A?%p7:6S1/Q4n1*f]1c[*`*JP:,s-XOVdbb+Pac8%aT4M8l59VnWtMLd(P)TZn+Z[+\O2Bc`Lhd_8aF@(fDI&b[Z!1O8(LU!11q6K-oRS:>.Qs+9*bclY+3K/Mm3GfUg7V7BiQo%)"6L[);`NNcVUfP)h*h0:O?-f^ir\]M>l[rLCCA%AA/K::Q7ZGWm*KJOM>4B]#80!Fb+V<j9=8[Z2Cb96spVj7YF?_:,\6Ctuj)W`^QNN%)7,>>!3AaTp\g4k:blTQH8=bUVFLEm!N@Xa7<_g(Z3Lh!(<U1mJ(@M9++K#nNNoMH!#s6s\4'SMGYd_mQM@=R!:ej%N)MuR$'R>-@o!<T2a8H~>endstream
endobj
10 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1031
>>
stream
Gb!#[9i'e'&A@g>b]-dO&1:1Ep').#.rj4,HL/UFVSss?K0QC(^Yc<6!\/nm)Qd&&W\fb7SnHp(Eu:]p1&kI%!*=5$^L=o5R'2$7P:=^oiHd\5R'=Uib:G*CXC<k6=i.2EU3SScqI-e'cjK,R;4?@$$VpYP(N-c:705]bJ_()E$O44/6\A4i-(_pM*p-fMi\.n:U#slke3#1OE)Utqo([lce]JdmaSAQ=4*^HYn7i_d&)bE9GhqA#*7Q/cR$Oj36M>r`J3kLY;\uY6:<?hR]6e8>.Th'9>/$/Jn)%7.;Zb-?d#4)5KQd>_Hg_>RQ,goA1aIP3/-b;9pi21Un^6;kn`.juQ,^>tPlY@L?NOd?f3;_>U*`?=[2N0Q5dK'mWW/]MiKY4PhrM2dMXjJV+5Vs3dSr/oe1lh(@!ArEh(b5pZgE`S+5\/e^C:mriF`*-D4ns[E3c6MFR#d<C(C>u=U\_>*3G/uhklF-4:MknriS+Oi9)-4UK7OYB,[#-s3?VN#!`tn9UFor]RZp*/si\BF!HCUJ9F*#A'clGD5j8**M>n>cl03J+u9Rs'0k3Wik9m]NPU`l#V-\0C2hI\l5eVpd!;>@qG?Ep0`$aWM0=lpH(rjG'=H"nM_9KCS_,kD]$:aBJs36EDZY*E=&GA3s&NAAT@@'<)V)h^877Cfrp@h)*BJs'bt03N8!hdiBr#%_*ld7lZ7Nnqd$00;pU"jl:,X8paH&O5?6jJ7H;BPO,6Fl!O#k@n;XcrEh9e`8Ji%1gnR-d^ggE[bc(e%9B2ekieTY+W%<^GG#0a),^rn%m)q+IM[\RA-FWZ-:*gYsfXB*j>V!*e&=)Pr[[(IbD%?g(cFY_ul.7gu7&/N`n"7@r`0gX3Mg:8_94Wo/C"f+3&@dq]K"Bl;h-sIXkd!(j'q0KiE2+o0fWQ&5Kf.H]l2D9"SmlRT\r,?0_d3Q^9l<C#Z3oH;f@KW:HB^"t2V/]Z-^)m=nV7/2:??Ai":@uf`?M.ZhCL<NM!1coIh[Z3$D.&,29`>%X"8bL~>endstream
endobj
xref
0 11
0000000000 65535 f
0000000073 00000 n
0000000114 00000 n
0000000221 00000 n
0000000298 00000 n
0000000501 00000 n
0000000705 00000 n
0000000773 00000 n
0000001056 00000 n
0000001121 00000 n
0000002571 00000 n
trailer
<<
/ID
[<64150b42f508c76f69b388aa96285995><64150b42f508c76f69b388aa96285995>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 7 0 R
/Root 6 0 R
/Size 11
>>
startxref
3694
%%EOF

View File

@@ -0,0 +1,124 @@
%PDF-1.4
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 7 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
8 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/PageMode /UseNone /Pages 11 0 R /Type /Catalog
>>
endobj
10 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260216162615+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260216162615+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
11 0 obj
<<
/Count 3 /Kids [ 5 0 R 6 0 R 8 0 R ] /Type /Pages
>>
endobj
12 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1279
>>
stream
Gat%">Bf'b&:Vs/Qq,TRFY29SZYp-rgRLXZ^!Q-S01*aQ`4+sIDZ>,tA-.T\TK/NP5gml(qW=7b1PkS=_GaJ8f5p`SL&p<g07]lr$M,-/k8-o1Cj.YX+\Pt&kMmO6E$Gi3Apo6=D!qiC.s/a]jlD;?E]!R&86Ge(2SGU:cf*3>S(l'PF9W,u7kjaKqX*%uYNss?:GDhGFu$aaq#eijU$,kQo,7X*c6D0eJco0q0%2!=Y)Ln*%GuFIG$6u68;9fhWaVs[??lVp&Um$H>GIGa*qRcN1bk'd%S^kWPT@3X;j@Q[GlYu2phY%(FfSnE=DE.!;4^*0)@iP730N7qYk@W,^b@)<*C1kGJZrdk5f!YgNO!`eQb$M@r.gTZQZ_5%-lQ"j#I:=4!pI#?"`40@)SK%UMADq`P6tK%S5uj\i]3Qp)^R)W;HTul]F#IM?m"AVjs_As*C!r)*3>BYFsAPe29Sn$Kkn'358YOhcShVDPZ:u`MAREaJd$h.,+'g(:d2\&c?ST$Yb?Ftb=4[!Y[0r7SM"`c`t4$Y1[tkliEHI\R7cMc#>keE6$QY6=rY;b/Fe0YeLN%i%I0iB^"$=e.D.aRJEQe2?AJ;jr,Wna@O^SB;]fHjbNpoE.)*H]ShGiY)WMFb#o@][<=),XH#6@ACWNh15)pgU'_-0kAa+Jg/Qe=tiJo6a"4`qL!i+iL,SLg#/d&_;Pd%'go98oK!6HGD=TXLI)RAn[:n96lHZg'Rr_\egdBis4Bqb6#+K,T/"jIY''KPP`hpI!Qe8)EXV.o2*oOB&o:'YEKKAAt6aRd5fccoJEQ<K0=kGBZ7`71Hi?5-enJ:TCIm>O(TTTldl>!k&W03Z8?"I,R0Zjj^T/Hn%dPp^^t%5LYWHMgTSXLfLe*T(c89X6Jn*='Z@LTR/V)#GE"lY9'EW2/!sN%_m!j89Wa;TTCkCqEk!Yi%b>=T7)u)HG6<0pR$G8RHa)"nb;/)e7Am%jt?J67KA5p7,$"Ql_-]-%+KjdNj71'=u&*E]<Y2Ck^9X%lUot"``Oa0Pse8`EX@e<n-hZ@pI*[/fJ*]`?hIW5YFmbH`LP2rHBc6r=gS/o'SOdEmn^&.4Z1:#nQ`Q+9()+F18'Z+]"91q@J]pp7_5:3K;tk]fm-N[-R[g7*G/K>GIf"itnKjorH3u]Okd]?>td&Uq9C>@;#['&2^of')Uain]R(bY&C!\;:EI!)k:]NN&gcriR&&'r2hFgLRi8RIV+B1/gH;t(!!HrgibW(:Q_-%f?>l*QOC=u(CsTor;~>endstream
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1179
>>
stream
Gat%!bAQ&q&DcM"B'EV)W[,@?]B`2YU-Mm=M:tDW%5"hCHRVkO,O"<7L3cs"S[=(N)&DC`c5@r?j-,.?;$$T]_s:S08IG8P8/BSSnlsc/=-$ESCUq_u[9Q1([`Qku&'uWeUYs2*$LYAtTU5f"iA!iQr#_<>ok$Eq-610VR\jE-60dHZp_T)E#0RCV8oA!%rk1&4Gu\(IfFLjT$>O1Gd(W!@oTJmF'=/YcJ)Ht1;B2+R_JnGUiO_(S8'dZP]h<=Vo@bo<U4:ddHNW&WIr0&c"M6T'3@8*e.L(`RpY5)e/Y6M%P7Ql^]ml[Zj2i06p(!_bFQ\Q1D5THYMU+eR=5?ir#V673f=MaE$Q*IY`G?:*:8m^$6P[o]Z-:eHic5XSAD&&!csNWa8,mXc2o=XcWJ'7R[BTkYO8r[rE^3%/Oi]kU_+_9*SKupTh#D&=ZTru`3A0f&&sSF[0%r/"O2f8UQb]^*2!1YLNm-4_%.r=c:V<?M7LQ"i;q6^@k>8JiJ[s.\.)B,BQ<I^8SaCPJ1fOH)I,rj6j<h0-A!,u7Hpcf<2tfQr(/ELndm3OLCDNCbYK?%VC-IaFhlp]5_-'Ped:KRb8+G^-fL:9HThDij4^HpoZn,ZJddAu%J`D&GlA0$i"t[;7:4YD-TPcb<?dVrMU%F=ompj<kJ.o:C8M.$HV'q]E>)f7M8/G<qeaa#*%5B/^%;@(d`t/lO<A>eu*@t-RBIU0^8^oASVi7_bMNcF[LM3jDST`dc]<d;\dUN\d@sBu=,Dh<f-8fbs@Ze1k_FgJg7j`rUHN2!<j4@eu0I=V@C//A5\JUU3WsZmBZKu]$`=niU9](-,H#DN8S'>?ur!e(\#juE\7oH3S4o0eIQ2p8a?denRQRJD:BsbHVd3b`Sp$)UTPO&f\i!h:u61Em!+O?hKaG?4K>0uqPEUb0L]5@''L@OYiK1M/W?::j9MYgq/eb]cXZ@0#b_`Pc+a)(X@_L`?5g9H9l_G[U]6tbhHa>!%Og;8:prIUM@hQY^Zj>lg,RjK$qQh;-e'@]kKbGF#k0ta1o[Q%rpC]"]`U)6I5($X/@7V\AR?6=;\NC9UCO%tm8n!j3I#VaZVd=aF9BN_*[af;>dI\)3a%.fNaDJk:[i1qmcXW:[jXe_SAq"gMMq#VAXOPPdN*MO='2IWlh,!WU6$+sZlEr~>endstream
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 784
>>
stream
Gat=(9lJc?%)(h*n<aZ;CWW4%D<onS/d6XH7BtNFKdAaFeFi!cP-^tSdVKp4(l"[$7m9,"5tVZGmoBI:DrBcL!TVNh!m?cIRPikXMuA5J:0'5O3DXn5Wp.O_@0WtMZsTU@(j?>pbDo71;\9D8plVB_*6V=`%B@7]+@^%(+$N!^%*dtNr\16!=%/9ZcCh+2d/KQ&Yr#:4imq%rBh70+Q4V]7[ngK%0kLEVOUa9P_;0q:Ccm,$5RfgZMlkAI>#=3Y.^,#&DN+'0*#jR=W),SNH9sNkWo,d'*cUKD%/AshY<a'+10m!R?Fnu32)qk+]sKMl:1t"u_VQ4ZNYeR=KV^a-[Qi7u:M5uqn1]g",Yh'aGl$Pd5A2og\(pJI4<Wu13TC`Uqu/nkIr\;)r[29(<#!^([5h,lnki7p8c=[1p^-eaP?F?i^!tD;n?S7&c,iCX-0aM"nuOf;1FJOemm^WA"$MJK%S%g&JlF4UdRiCEmDH<U/[<D7CS.g?C+A3)2Ve_r)1CqYkZsuZL.M]RPYRYq_2E-#On;^n39U&dXi'47;f\2;2r%Q,]c$5K22*>qB8;TRRV_PRr`W7+Z:rDb119beRPcCls(/g8[C:T8oa[F_Z:.%EJB%T9N$L8_2u;,$ES<5Z8?X$@mCi^#2d'J7GT&fpXBT@b^U<#s0;@^K*>`jJZ]*Jb%_niA%#GNrf'!@5TT:]B6Ob@FGBBM%j0TG._a:%[0_D*9<Bpu1BHG(2,B?CS\8ZC7_lBMo^VsH4^VMu)HAhUaXm9Ec$OgpZ!!~>endstream
endobj
xref
0 15
0000000000 65535 f
0000000073 00000 n
0000000134 00000 n
0000000241 00000 n
0000000353 00000 n
0000000468 00000 n
0000000673 00000 n
0000000878 00000 n
0000000955 00000 n
0000001160 00000 n
0000001229 00000 n
0000001513 00000 n
0000001585 00000 n
0000002956 00000 n
0000004227 00000 n
trailer
<<
/ID
[<b2d808ce539c534b92369a705967def5><b2d808ce539c534b92369a705967def5>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 10 0 R
/Root 9 0 R
/Size 15
>>
startxref
5102
%%EOF

4
Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY . /usr/share/nginx/html

33
README.md Normal file
View File

@@ -0,0 +1,33 @@
# ChessCubing Arena
Application web mobile-first pour téléphone et tablette, pensée comme application officielle de suivi de match pour `ChessCubing Twice` et `ChessCubing Time`.
## Ce que fait cette première version
- configure une rencontre `Twice` ou `Time`
- gère les blocks de 180 secondes et le temps par coup de 20 secondes
- 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
- 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
- conserve un historique local dans le navigateur
## Hypothèse de produit
Cette version est volontairement construite comme une **application d'arbitrage et de direction de match** autour d'un vrai échiquier physique, et non comme un moteur d'échecs complet. C'est le choix le plus fidèle aux règlements fournis et le plus réaliste pour une utilisation immédiate en club, en démonstration ou en tournoi.
## Démarrage avec Docker
```bash
docker compose down
docker compose up -d --build
```
L'application est ensuite disponible sur `http://localhost:8080`.
## Fichiers clés
- `index.html` : structure de l'interface
- `styles.css` : design mobile/tablette
- `app.js` : logique de match et arbitrage
- `docker-compose.yml` + `Dockerfile` : exécution locale

1129
app.js Normal file

File diff suppressed because it is too large Load Diff

7
docker-compose.yml Normal file
View File

@@ -0,0 +1,7 @@
services:
web:
build: .
container_name: chesscubing-web
ports:
- "8080:80"
restart: unless-stopped

410
index.html Normal file
View File

@@ -0,0 +1,410 @@
<!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="Application officielle de suivi de match pour ChessCubing Twice et ChessCubing Time."
/>
<title>ChessCubing Arena</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="ambient ambient-left"></div>
<div class="ambient ambient-right"></div>
<div class="layout">
<header class="hero">
<div class="hero-copy">
<p class="eyebrow">Application officielle de match</p>
<h1>ChessCubing Arena</h1>
<p class="lead">
Une web app pensée pour l'arbitrage sur téléphone et tablette,
directement dérivée des règlements de
<strong>ChessCubing Twice</strong> et
<strong>ChessCubing Time</strong>.
</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">
<span>Mobile-first</span>
<span>Twice + Time</span>
<span>Arbitrage en direct</span>
<span>Fonctionne hors build</span>
</div>
</div>
<aside class="hero-preview">
<div class="preview-card">
<p class="preview-label">Ce que l'application gère</p>
<ul class="preview-list">
<li>Blocks de 180 s et quotas FAST / FREEZE / MASTERS</li>
<li>Temps par coup de 20 s</li>
<li>Phase cube, égalité, gagnant et block suivant</li>
<li>Mode Time avec chronos cumulés et blocs - / +</li>
</ul>
</div>
<div class="preview-banner">
<span class="mini-chip">Block 1</span>
<strong id="heroModeHint">Twice ou Time, selon la configuration</strong>
<p>
L'app sert de chef d'orchestre pendant la partie sans imposer un
échiquier numérique.
</p>
</div>
</aside>
</header>
<main class="workspace">
<section class="panel" id="setupPanel">
<div class="section-heading">
<div>
<p class="eyebrow">Préparer la rencontre</p>
<h2>Configuration de match</h2>
</div>
<p class="section-copy">
Lancement rapide pour club, démo ou tournoi. Les choix pilotent le
tableau d'arbitrage en direct.
</p>
</div>
<form id="setupForm" class="setup-form">
<label class="field span-2">
<span>Nom de la rencontre</span>
<input
name="matchLabel"
type="text"
maxlength="80"
placeholder="Open ChessCubing de Paris"
/>
</label>
<fieldset class="field span-2">
<legend>Mode officiel</legend>
<div class="option-grid mode-grid">
<label class="option-card">
<input type="radio" name="mode" value="twice" checked />
<strong>ChessCubing Twice</strong>
<span>
Le gagnant du cube commence le block suivant et peut obtenir
un double coup selon la règle V2.
</span>
</label>
<label class="option-card">
<input type="radio" name="mode" value="time" />
<strong>ChessCubing Time</strong>
<span>
Même structure de blocks, avec deux chronos cumulés et un
impact cube en bloc - / +.
</span>
</label>
</div>
</fieldset>
<fieldset class="field span-2">
<legend>Cadence du block</legend>
<div class="option-grid preset-grid">
<label class="option-card">
<input type="radio" name="preset" value="fast" checked />
<strong>FAST</strong>
<span>6 coups par joueur et par block</span>
</label>
<label class="option-card">
<input type="radio" name="preset" value="freeze" />
<strong>FREEZE</strong>
<span>8 coups par joueur et par block</span>
</label>
<label class="option-card">
<input type="radio" name="preset" value="masters" />
<strong>MASTERS</strong>
<span>10 coups par joueur et par block</span>
</label>
</div>
</fieldset>
<label class="field">
<span>Joueur blanc</span>
<input
name="whiteName"
type="text"
maxlength="40"
placeholder="Blanc"
required
/>
</label>
<label class="field">
<span>Joueur noir</span>
<input
name="blackName"
type="text"
maxlength="40"
placeholder="Noir"
required
/>
</label>
<label class="field">
<span>Arbitre</span>
<input
name="arbiterName"
type="text"
maxlength="40"
placeholder="Arbitre principal"
/>
</label>
<label class="field">
<span>Club / événement</span>
<input
name="eventName"
type="text"
maxlength="60"
placeholder="Club local, tournoi, démonstration"
/>
</label>
<label class="field span-2">
<span>Notes de mise en place</span>
<textarea
name="notes"
rows="3"
placeholder="Tirage au sort effectué, app lancée, cubes vérifiés..."
></textarea>
</label>
<div class="setup-summary span-2" id="setupSummary"></div>
<div class="setup-actions span-2">
<button class="button primary" type="submit">
Lancer le match
</button>
<button class="button ghost" type="button" id="loadDemoButton">
Charger une démo
</button>
</div>
</form>
</section>
<section class="panel live-panel hidden" id="livePanel">
<div class="section-heading">
<div>
<p class="eyebrow">Direction de match</p>
<h2 id="matchTitle">Rencontre en direct</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 class="player-grid">
<section class="player-card white-seat" id="whiteCard">
<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 class="notice-card" id="contextNotice"></div>
<div class="double-card" id="doubleCard"></div>
<div class="result-grid">
<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>
Block de 180 s, 20 s max par coup, phase cube obligatoire
entre chaque block, gagnant du cube au départ suivant.
</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>
</div>
</section>
</main>
<footer class="footer">
<p>
Sources intégrées :
<a href="ChessCubing_Twice_Reglement_Officiel_V2-1.pdf" target="_blank"
>règlement ChessCubing Twice</a
>
et
<a href="ChessCubing_Time_Reglement_Officiel_V1-1.pdf" target="_blank"
>règlement ChessCubing Time</a
>.
</p>
</footer>
</div>
<script type="module" src="app.js"></script>
</body>
</html>

11
nginx.conf Normal file
View File

@@ -0,0 +1,11 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

646
styles.css Normal file
View File

@@ -0,0 +1,646 @@
:root {
--bg: #07151f;
--bg-soft: rgba(12, 31, 42, 0.84);
--panel: rgba(10, 26, 37, 0.86);
--panel-border: rgba(255, 255, 255, 0.08);
--panel-highlight: rgba(255, 193, 124, 0.22);
--text: #eef5f2;
--muted: #97adb0;
--warm: #ffb86c;
--warm-strong: #ff8f3c;
--cool: #5de2d8;
--cool-strong: #23bdb0;
--danger: #ff6b6b;
--shadow: 0 24px 70px rgba(0, 0, 0, 0.35);
--radius: 26px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Avenir Next", "Segoe UI", sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(255, 184, 108, 0.15), transparent 30%),
radial-gradient(circle at bottom right, rgba(93, 226, 216, 0.16), transparent 26%),
linear-gradient(160deg, #030c12 0%, #07151f 46%, #0a2331 100%);
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: radial-gradient(circle at center, black 48%, transparent 100%);
}
.ambient {
position: fixed;
width: 28rem;
height: 28rem;
border-radius: 50%;
filter: blur(70px);
opacity: 0.22;
pointer-events: none;
}
.ambient-left {
top: -10rem;
left: -8rem;
background: var(--warm-strong);
}
.ambient-right {
right: -10rem;
bottom: -8rem;
background: var(--cool-strong);
}
.layout {
position: relative;
width: min(1200px, calc(100% - 2rem));
margin: 0 auto;
padding: 1.2rem 0 2.4rem;
}
.hero,
.panel,
.footer {
animation: rise 0.7s ease both;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.95fr);
gap: 1.2rem;
align-items: stretch;
margin-bottom: 1.2rem;
}
.hero-copy,
.hero-preview,
.panel,
.panel.inset {
border: 1px solid var(--panel-border);
border-radius: calc(var(--radius) + 4px);
background: var(--panel);
backdrop-filter: blur(18px);
box-shadow: var(--shadow);
}
.hero-copy {
padding: 2rem;
}
.hero-preview {
padding: 1.4rem;
display: grid;
gap: 1rem;
}
.eyebrow,
.micro-label {
margin: 0 0 0.4rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--cool);
font-size: 0.76rem;
}
.micro-label {
color: var(--warm);
}
h1,
h2,
h3,
strong {
font-family: "Baskerville", "Georgia", serif;
}
h1 {
margin: 0;
font-size: clamp(2.8rem, 7vw, 4.8rem);
line-height: 0.96;
}
h2,
h3 {
margin: 0;
}
.lead,
.section-copy,
.preview-banner p,
.rule-card p,
.preview-list,
.footer p {
color: var(--muted);
}
.hero-actions,
.setup-actions,
.action-grid,
.result-grid,
.capture-grid {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.hero-actions {
margin: 1.6rem 0 1rem;
}
.hero-pills {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
}
.hero-pills span,
.mini-chip,
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.85rem;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
color: var(--text);
}
.preview-card,
.preview-banner,
.notice-card,
.double-card,
.rule-card,
.setup-summary,
.timer-card,
.player-card,
.cube-results > div {
padding: 1rem;
border-radius: 22px;
background:
linear-gradient(160deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
border: 1px solid rgba(255, 255, 255, 0.07);
}
.preview-list {
margin: 0.8rem 0 0;
padding-left: 1rem;
}
.preview-list li + li {
margin-top: 0.45rem;
}
.workspace {
display: grid;
gap: 1.2rem;
}
.panel {
padding: 1.4rem;
}
.panel.inset {
padding: 1.15rem;
}
.section-heading {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1.2rem;
}
.setup-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.field {
display: grid;
gap: 0.45rem;
}
.span-2 {
grid-column: 1 / -1;
}
legend,
.field > span {
font-weight: 600;
}
input,
textarea {
width: 100%;
padding: 0.95rem 1rem;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.18);
color: var(--text);
font: inherit;
}
textarea {
resize: vertical;
min-height: 6rem;
}
input:focus,
textarea:focus {
outline: 2px solid rgba(93, 226, 216, 0.35);
outline-offset: 1px;
}
fieldset {
margin: 0;
padding: 0;
border: 0;
}
.option-grid {
display: grid;
gap: 0.85rem;
}
.mode-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.preset-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.option-card {
position: relative;
display: grid;
gap: 0.45rem;
min-height: 9rem;
padding: 1rem 1rem 1rem 3rem;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.03);
cursor: pointer;
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
}
.option-card:hover {
transform: translateY(-3px);
border-color: rgba(255, 184, 108, 0.35);
}
.option-card input {
position: absolute;
top: 1.2rem;
left: 1rem;
width: 1.1rem;
height: 1.1rem;
}
.option-card:has(input:checked) {
background:
linear-gradient(160deg, rgba(255, 184, 108, 0.18), rgba(93, 226, 216, 0.08));
border-color: rgba(255, 184, 108, 0.55);
box-shadow: inset 0 0 0 1px rgba(255, 184, 108, 0.14);
}
.button {
appearance: none;
border: 0;
border-radius: 16px;
padding: 0.95rem 1.1rem;
font: inherit;
font-weight: 700;
cursor: pointer;
color: var(--text);
transition: transform 160ms ease, filter 160ms ease, background 160ms ease;
}
.button:hover {
transform: translateY(-2px);
filter: brightness(1.04);
}
.button:disabled {
opacity: 0.4;
cursor: not-allowed;
transform: none;
}
.button.primary {
background: linear-gradient(135deg, var(--warm-strong), var(--warm));
color: #1d140a;
}
.button.secondary {
background: linear-gradient(135deg, rgba(93, 226, 216, 0.18), rgba(93, 226, 216, 0.08));
border: 1px solid rgba(93, 226, 216, 0.25);
}
.button.ghost {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.button.capture {
flex: 1 1 12rem;
background: linear-gradient(135deg, rgba(255, 184, 108, 0.16), rgba(255, 184, 108, 0.08));
border: 1px solid rgba(255, 184, 108, 0.25);
}
.button.danger {
border-color: rgba(255, 107, 107, 0.25);
color: #ffd7d7;
}
.button.small {
padding: 0.7rem 0.95rem;
}
.setup-summary {
display: grid;
gap: 0.35rem;
color: var(--muted);
}
.live-grid {
display: grid;
grid-template-columns: 1.2fr 0.95fr;
gap: 1rem;
}
.live-grid > .panel.inset:nth-child(4),
.live-grid > .panel.inset:nth-child(5) {
grid-column: span 1;
}
.score-panel {
grid-column: 1 / -1;
}
.score-head {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1rem;
}
.timer-grid,
.player-grid,
.cube-results,
.rules-grid {
display: grid;
gap: 0.85rem;
}
.timer-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-bottom: 0.85rem;
}
.timer-card strong,
.cube-clock strong {
display: block;
margin: 0.4rem 0;
font-size: clamp(2rem, 6vw, 3.6rem);
line-height: 0.95;
}
.timer-card.emphasized {
background:
linear-gradient(160deg, rgba(255, 184, 108, 0.22), rgba(255, 255, 255, 0.03));
}
.player-grid,
.cube-results,
.rules-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.player-card {
position: relative;
overflow: hidden;
}
.player-card::after {
content: "";
position: absolute;
inset: auto -10% -50% auto;
width: 9rem;
height: 9rem;
border-radius: 50%;
opacity: 0.18;
}
.white-seat::after {
background: var(--warm);
}
.black-seat::after {
background: var(--cool);
}
.player-card.active {
border-color: var(--panel-highlight);
box-shadow: 0 0 0 1px rgba(255, 184, 108, 0.2), inset 0 0 0 1px rgba(255, 184, 108, 0.14);
animation: pulse 1.8s ease-in-out infinite;
}
.player-name-row {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
margin-bottom: 0.35rem;
}
.player-color {
padding: 0.25rem 0.55rem;
border-radius: 999px;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.1em;
background: rgba(255, 255, 255, 0.08);
}
.muted {
color: var(--muted);
}
.notice-card,
.double-card {
margin-top: 1rem;
color: var(--muted);
}
.double-card strong {
display: block;
margin-bottom: 0.35rem;
color: var(--warm);
}
.cube-head {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
margin-bottom: 1rem;
}
.cube-clock {
padding: 1rem;
border-radius: 22px;
background:
linear-gradient(160deg, rgba(93, 226, 216, 0.18), rgba(255, 255, 255, 0.03));
border: 1px solid rgba(93, 226, 216, 0.18);
margin-bottom: 1rem;
}
.cube-results {
margin: 1rem 0;
}
.cube-results strong {
font-size: 1.35rem;
display: block;
margin-top: 0.2rem;
}
.compact {
margin-top: 0.8rem;
}
.history-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.75rem;
max-height: 24rem;
overflow: auto;
}
.history-list li {
padding: 0.85rem 0.95rem;
border-radius: 18px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.history-list small {
display: block;
margin-bottom: 0.25rem;
color: var(--cool);
}
.footer {
margin-top: 1rem;
padding: 1rem 0 0;
}
.footer a {
color: var(--warm);
}
.hidden {
display: none;
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%,
100% {
box-shadow: 0 0 0 1px rgba(255, 184, 108, 0.2), inset 0 0 0 1px rgba(255, 184, 108, 0.14);
}
50% {
box-shadow: 0 0 0 1px rgba(255, 184, 108, 0.32), 0 0 32px rgba(255, 184, 108, 0.16);
}
}
@media (max-width: 960px) {
.hero,
.live-grid,
.mode-grid,
.preset-grid,
.player-grid,
.cube-results,
.rules-grid,
.timer-grid {
grid-template-columns: 1fr;
}
.setup-form,
.live-grid {
grid-template-columns: 1fr;
}
.section-heading,
.score-head,
.cube-head {
flex-direction: column;
}
}
@media (max-width: 640px) {
.layout {
width: min(100% - 1rem, 100%);
}
.hero-copy,
.hero-preview,
.panel {
padding: 1rem;
border-radius: 22px;
}
h1 {
font-size: 2.65rem;
}
.button {
width: 100%;
justify-content: center;
}
.hero-actions,
.setup-actions,
.action-grid,
.result-grid,
.capture-grid {
display: grid;
}
}