chg: dev: massive refactor on front-end for unification and readiness #8
This commit is contained in:
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@keyframes appear {
|
@keyframes appear {
|
||||||
from { opacity: 0; transform: scale(0.94); }
|
from { opacity: 0; transform: scale(0.94); }
|
||||||
to { opacity: 1; transform: scale(1); }
|
to { opacity: 1; transform: scale(1); }
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#hero-auth {
|
#hero-auth {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.auth-page {
|
.auth-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
200
assets/css/homepage/_battle-dialog.scss
Normal file
200
assets/css/homepage/_battle-dialog.scss
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Avatar ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.bd-avatar-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-ring-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-ring {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bd-avatar-gradient);
|
||||||
|
border: 2px solid var(--bd-avatar-border);
|
||||||
|
box-shadow: var(--bd-avatar-glow);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font: 800 24px 'Rajdhani', sans-serif;
|
||||||
|
color: var(--bd-avatar-color);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-bonus {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -6px;
|
||||||
|
right: -6px;
|
||||||
|
background: #ffd700;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1);
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: #000;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-name {
|
||||||
|
font: 700 15px 'Rajdhani', sans-serif;
|
||||||
|
color: var(--bd-avatar-color);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-avatar-side {
|
||||||
|
font: 600 10px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── StatRow ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.bd-stat-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 16px;
|
||||||
|
color: rgba(149, 207, 245, 0.4);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font: 500 13px 'Rajdhani', sans-serif;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
flex: 1;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: var(--bd-stat-value-color, rgba(255, 255, 255, 0.75));
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BonusPoints ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
.bd-bonus {
|
||||||
|
padding: 16px 20px 0;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
&__grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__column {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&--red {
|
||||||
|
border: 1px solid rgba(173, 10, 5, 0.2);
|
||||||
|
background: rgba(173, 10, 5, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
border: 1px solid rgba(149, 207, 245, 0.2);
|
||||||
|
background: rgba(149, 207, 245, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
font: 700 12px 'Rajdhani', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: #ffd700;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BattleDialog header actions & bonus score row ────────────────────────────
|
||||||
|
|
||||||
|
.bd-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-bonus-score {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&__red {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: #f67d52;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__blue {
|
||||||
|
font: 700 13px 'Rajdhani', sans-serif;
|
||||||
|
color: #95cff5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd-result-badge {
|
||||||
|
background: var(--bd-result-bg);
|
||||||
|
border: 1px solid var(--bd-result-border);
|
||||||
|
color: var(--bd-result-color);
|
||||||
|
}
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
main div.txt {
|
main div.txt {
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.hero-cta {
|
.hero-cta {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.feature-block {
|
.feature-block {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 80px 40px;
|
padding: 80px 40px;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background: #040608;
|
background: #040608;
|
||||||
border-top: 1px solid rgba(35, 111, 135, 0.12);
|
border-top: 1px solid rgba(35, 111, 135, 0.12);
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
header {
|
header {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.hero--compact {
|
.hero--compact {
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
padding: 36px 60px 48px;
|
padding: 36px 60px 48px;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
* {
|
* {
|
||||||
outline: none;
|
outline: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
.hero h1 {
|
.hero h1 {
|
||||||
font-size: 44px;
|
font-size: 44px;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
main {
|
main {
|
||||||
background: #07090d;
|
background: #07090d;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.back-from-game {
|
.back-from-game {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users .user-container .user-control {
|
#mine-wrapper .game-wrapper .users .user-container .user-control {
|
||||||
background: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
background: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||||
background: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
background: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 0%, rgba(125, 185, 232, 0) 100%);
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .grid {
|
#mine-wrapper .grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users .active-mines-container {
|
#mine-wrapper .game-wrapper .users .active-mines-container {
|
||||||
background: -moz-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
background: -moz-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||||
background: -webkit-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
background: -webkit-radial-gradient(center, ellipse cover, rgba(255, 252, 252, 1) 0%, rgba(255, 252, 252, 0.99) 1%, rgba(106, 106, 106, 0.39) 61%, rgba(106, 106, 106, 0) 100%);
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .game-overlay {
|
#mine-wrapper .game-wrapper .game-overlay {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
#mine-wrapper .game-wrapper .users {
|
#mine-wrapper .game-wrapper .users {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-timer-container {
|
#mine-wrapper .game-timer-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
#mine-wrapper .game-wrapper .users {
|
#mine-wrapper .game-wrapper .users {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
padding: 0 10px 0 0;
|
padding: 0 10px 0 0;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.opd-paper {
|
.opd-paper {
|
||||||
background: #07090d !important;
|
background: #07090d !important;
|
||||||
background-image: linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
background-image: linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@use "sass:color";
|
@use "sass:color";
|
||||||
|
|
||||||
.twofa-status {
|
.twofa-status {
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
@use 'homepage/reset';
|
@use 'homepage/reset';
|
||||||
@use 'homepage/animations';
|
@use 'homepage/animations';
|
||||||
@use 'homepage/header';
|
@use 'homepage/header';
|
||||||
@@ -12,4 +21,5 @@
|
|||||||
@use 'homepage/tech';
|
@use 'homepage/tech';
|
||||||
@use 'homepage/footer';
|
@use 'homepage/footer';
|
||||||
@use 'homepage/profile';
|
@use 'homepage/profile';
|
||||||
|
@use 'homepage/battle-dialog';
|
||||||
@use 'homepage/responsive';
|
@use 'homepage/responsive';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*!*
|
/*!*
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
*
|
*
|
||||||
* Copyright (c) 2026 @ www.splendidbear.org
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
*
|
*
|
||||||
* For the full copyright and license information, please view the LICENSE
|
* For the full copyright and license information, please view the LICENSE
|
||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*!*
|
/*!*
|
||||||
* This file is part of the SplendidBear Websites' projects.
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
*
|
*
|
||||||
* Copyright (c) 2026 @ www.splendidbear.org
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
*
|
*
|
||||||
* For the full copyright and license information, please view the LICENSE
|
* For the full copyright and license information, please view the LICENSE
|
||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*!*
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
.mine-beta {
|
.mine-beta {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -1,36 +1,32 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) {
|
import React, { useMemo, useRef } from 'react';
|
||||||
const [thumbUrl, setThumbUrl] = useState(initialThumbUrl || null);
|
import { string } from 'prop-types';
|
||||||
const [loading, setLoading] = useState(false);
|
import { useProfileDataProvider } from '@mine-hooks/useGameDataProvider';
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
export const AvatarUpload = ({ uploadUrl, initialThumbUrl, initials }) => {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
const [thumbUrl, setThumbUrl] = React.useState(initialThumbUrl || null);
|
||||||
|
const { uploadAvatarMutation: { isPending, error, mutate } } = useProfileDataProvider();
|
||||||
|
|
||||||
function handleClick() {
|
const handleChange = e => {
|
||||||
inputRef.current?.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleChange(e) {
|
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const fd = new FormData();
|
mutate({ uploadUrl, file }, {
|
||||||
fd.append('avatar', file);
|
onSuccess: data => {
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
fetch(uploadUrl, { method: 'POST', body: fd })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.error) {
|
|
||||||
setError(data.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setThumbUrl(data.thumbUrl);
|
setThumbUrl(data.thumbUrl);
|
||||||
|
|
||||||
const navImg = document.querySelector('.hero-auth-avatar:not(.hero-auth-avatar--initials)');
|
const navImg = document.querySelector('.hero-auth-avatar:not(.hero-auth-avatar--initials)');
|
||||||
const navInitials = document.querySelector('.hero-auth-avatar.hero-auth-avatar--initials');
|
const navInitials = document.querySelector('.hero-auth-avatar.hero-auth-avatar--initials');
|
||||||
|
|
||||||
if (navImg) {
|
if (navImg) {
|
||||||
navImg.src = data.thumbUrl;
|
navImg.src = data.thumbUrl;
|
||||||
} else if (navInitials) {
|
} else if (navInitials) {
|
||||||
@@ -40,16 +36,17 @@ export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) {
|
|||||||
img.className = 'hero-auth-avatar';
|
img.className = 'hero-auth-avatar';
|
||||||
navInitials.replaceWith(img);
|
navInitials.replaceWith(img);
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
.catch(() => setError('Upload failed. Please try again.'))
|
});
|
||||||
.finally(() => setLoading(false));
|
};
|
||||||
}
|
|
||||||
|
const errorMessage = useMemo(() => error?.message ?? null, [error]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`profile-avatar${loading ? ' profile-avatar--loading' : ''}`}
|
className={`profile-avatar${isPending ? ' profile-avatar--loading' : ''}`}
|
||||||
title="Click to change profile picture"
|
title="Click to change profile picture"
|
||||||
onClick={handleClick}
|
onClick={() => inputRef.current?.click()}
|
||||||
>
|
>
|
||||||
{thumbUrl
|
{thumbUrl
|
||||||
? <img src={thumbUrl} alt={initials} className="profile-avatar__img" />
|
? <img src={thumbUrl} alt={initials} className="profile-avatar__img" />
|
||||||
@@ -65,7 +62,13 @@ export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) {
|
|||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
{error && <div className="profile-avatar__error">{error}</div>}
|
{errorMessage && <div className="profile-avatar__error">{errorMessage}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvatarUpload.propTypes = {
|
||||||
|
uploadUrl: string.isRequired,
|
||||||
|
initialThumbUrl: string,
|
||||||
|
initials: string.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,34 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { array } from 'prop-types';
|
||||||
|
import { formatDuration } from '@global-utils/format';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
import { createTheme, styled, ThemeProvider } from '@mui/material/styles';
|
||||||
import Avatar from './battle-dialog/Avatar';
|
import { Avatar, BonusPoints, StatRow } from '@global-components';
|
||||||
import StatRow from './battle-dialog/StatRow';
|
|
||||||
import BonusPoints from './battle-dialog/BonusPoints';
|
|
||||||
|
|
||||||
const darkTheme = createTheme({ palette: { mode: 'dark' } });
|
const darkTheme = createTheme({ palette: { mode: 'dark' } });
|
||||||
|
|
||||||
const DIALOG_SX = {
|
|
||||||
'& .MuiDialog-paper': {
|
|
||||||
background: '#07090d',
|
|
||||||
backgroundImage: `
|
|
||||||
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
|
||||||
`,
|
|
||||||
backgroundSize: '46px 46px',
|
|
||||||
border: '1px solid rgba(35, 111, 135, 0.4)',
|
|
||||||
borderRadius: '12px',
|
|
||||||
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0,0,0,0.9)',
|
|
||||||
width: '580px',
|
|
||||||
maxWidth: '94vw',
|
|
||||||
overflow: 'hidden',
|
|
||||||
color: '#fff',
|
|
||||||
},
|
|
||||||
'& .MuiBackdrop-root': {
|
|
||||||
background: 'rgba(2, 4, 8, 0.88)',
|
|
||||||
backdropFilter: 'blur(4px)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const RESULT_META = {
|
const RESULT_META = {
|
||||||
win: {
|
win: {
|
||||||
label: 'Victory',
|
label: 'Victory',
|
||||||
@@ -53,7 +40,7 @@ const RESULT_META = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BattleDialog({ games }) {
|
export const BattleDialog = ({ games }) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [game, setGame] = useState(null);
|
const [game, setGame] = useState(null);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
@@ -73,7 +60,7 @@ export default function BattleDialog({ games }) {
|
|||||||
}, [games]);
|
}, [games]);
|
||||||
|
|
||||||
if (!game) {
|
if (!game) {
|
||||||
return <ThemeProvider theme={darkTheme}><Dialog open={false} sx={DIALOG_SX} /></ThemeProvider>;
|
return <ThemeProvider theme={darkTheme}><StyledDialog open={false} /></ThemeProvider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = RESULT_META[game.result] ?? RESULT_META.draw;
|
const meta = RESULT_META[game.result] ?? RESULT_META.draw;
|
||||||
@@ -86,18 +73,6 @@ export default function BattleDialog({ games }) {
|
|||||||
const canContinue = !resign && 26 > maxPoints;
|
const canContinue = !resign && 26 > maxPoints;
|
||||||
const playUrl = `${window.location.origin}/play/${game.uuid}`;
|
const playUrl = `${window.location.origin}/play/${game.uuid}`;
|
||||||
|
|
||||||
const formatDuration = (from, to) => {
|
|
||||||
if (!from || !to) return null;
|
|
||||||
const diffMs = new Date(to.replace(' ', 'T')) - new Date(from.replace(' ', 'T'));
|
|
||||||
if (isNaN(diffMs) || 0 >= diffMs) return null;
|
|
||||||
const totalSec = Math.floor(diffMs / 1000);
|
|
||||||
const h = Math.floor(totalSec / 3600);
|
|
||||||
const m = Math.floor((totalSec % 3600) / 60);
|
|
||||||
const s = totalSec % 60;
|
|
||||||
if (0 < h) return `${h}h ${m}m ${s}s`;
|
|
||||||
if (0 < m) return `${m}m ${s}s`;
|
|
||||||
return `${s}s`;
|
|
||||||
};
|
|
||||||
const duration = formatDuration(game.created, game.date);
|
const duration = formatDuration(game.created, game.date);
|
||||||
const pointDiff = Math.abs((game.redPoints ?? 0) - (game.bluePoints ?? 0));
|
const pointDiff = Math.abs((game.redPoints ?? 0) - (game.bluePoints ?? 0));
|
||||||
const winnerColor = (game.redPoints ?? 0) > (game.bluePoints ?? 0) ? '#f67d52'
|
const winnerColor = (game.redPoints ?? 0) > (game.bluePoints ?? 0) ? '#f67d52'
|
||||||
@@ -113,7 +88,7 @@ export default function BattleDialog({ games }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={darkTheme}>
|
<ThemeProvider theme={darkTheme}>
|
||||||
<Dialog open={open} onClose={() => setOpen(false)} sx={DIALOG_SX}>
|
<StyledDialog open={open} onClose={() => setOpen(false)}>
|
||||||
<div className="bd">
|
<div className="bd">
|
||||||
<div className="bd-header">
|
<div className="bd-header">
|
||||||
<div className="bd-header-left">
|
<div className="bd-header-left">
|
||||||
@@ -122,7 +97,7 @@ export default function BattleDialog({ games }) {
|
|||||||
<i className="fa fa-crosshairs" /> Match Details
|
<i className="fa fa-crosshairs" /> Match Details
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div className="bd-header-actions">
|
||||||
{canContinue ? (
|
{canContinue ? (
|
||||||
<a
|
<a
|
||||||
className="bd-continue"
|
className="bd-continue"
|
||||||
@@ -160,33 +135,19 @@ export default function BattleDialog({ games }) {
|
|||||||
<span className="bd-vs-score__sep">:</span>
|
<span className="bd-vs-score__sep">:</span>
|
||||||
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
|
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bd-vs-score" style={{ marginBottom: 8 }}>
|
<div className="bd-vs-score bd-bonus-score">
|
||||||
<span style={{
|
<span className="bd-bonus-score__red">
|
||||||
font: '700 13px \'Rajdhani\', sans-serif',
|
<i className="fa fa-star" /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
||||||
color: '#f67d52',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="fa fa-star" style={{ fontSize: 11 }} /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="bd-vs-score__sep">:</span>
|
<span className="bd-vs-score__sep">:</span>
|
||||||
<span style={{
|
<span className="bd-bonus-score__blue">
|
||||||
font: '700 13px \'Rajdhani\', sans-serif',
|
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" />
|
||||||
color: '#95cff5',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" style={{ fontSize: 11 }} />
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bd-vs-label">VS</div>
|
<div className="bd-vs-label">VS</div>
|
||||||
<div
|
<div
|
||||||
className="bd-result-badge"
|
className="bd-result-badge"
|
||||||
style={{ background: meta.bg, border: `1px solid ${meta.border}`, color: meta.color }}
|
style={{ '--bd-result-bg': meta.bg, '--bd-result-border': meta.border, '--bd-result-color': meta.color }}
|
||||||
>
|
>
|
||||||
<i className={`fa ${meta.icon}`} /> {meta.label}
|
<i className={`fa ${meta.icon}`} /> {meta.label}
|
||||||
</div>
|
</div>
|
||||||
@@ -226,7 +187,33 @@ export default function BattleDialog({ games }) {
|
|||||||
game={game}
|
game={game}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
BattleDialog.propTypes = {
|
||||||
|
games: array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
background: '#07090d',
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '46px 46px',
|
||||||
|
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0,0,0,0.9)',
|
||||||
|
width: '580px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ContactForm Component
|
* ContactForm Component
|
||||||
@@ -80,4 +81,9 @@ const ContactForm = ({ siteKey, recaptchaFieldId }) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ContactForm.propTypes = {
|
||||||
|
siteKey: string.isRequired,
|
||||||
|
recaptchaFieldId: string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default ContactForm;
|
export default ContactForm;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { shape, string } from 'prop-types';
|
||||||
|
|
||||||
const base64ToArrayBuffer = base64 => {
|
const base64ToArrayBuffer = base64 => {
|
||||||
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
||||||
@@ -108,3 +109,10 @@ const PasskeyLogin = ({ apiRoutes }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default PasskeyLogin;
|
export default PasskeyLogin;
|
||||||
|
|
||||||
|
PasskeyLogin.propTypes = {
|
||||||
|
apiRoutes: shape({
|
||||||
|
authenticationBegin: string.isRequired,
|
||||||
|
authenticationComplete: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,13 +9,15 @@
|
|||||||
|
|
||||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
|
import { arrayOf, shape, string, bool } from 'prop-types';
|
||||||
|
|
||||||
const DIALOG_SX = {
|
const StyledDialog = styled(Dialog)({
|
||||||
'& .MuiDialog-paper': {
|
'& .MuiDialog-paper': {
|
||||||
background: '#0a0e14',
|
background: '#0a0e14',
|
||||||
color: '#e0e0e0',
|
color: '#e0e0e0',
|
||||||
@@ -47,7 +49,7 @@ const DIALOG_SX = {
|
|||||||
background: 'rgba(2, 4, 8, 0.88)',
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
backdropFilter: 'blur(4px)',
|
backdropFilter: 'blur(4px)',
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const base64ToArrayBuffer = base64 => {
|
const base64ToArrayBuffer = base64 => {
|
||||||
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/')));
|
||||||
@@ -314,7 +316,7 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={addModalOpen} onClose={closeAddModal} sx={DIALOG_SX}>
|
<StyledDialog open={addModalOpen} onClose={closeAddModal}>
|
||||||
<DialogTitle>Add New Passkey</DialogTitle>
|
<DialogTitle>Add New Passkey</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -344,9 +346,9 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
|||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
|
|
||||||
<Dialog open={renameModalOpen} onClose={closeRenameModal} sx={DIALOG_SX}>
|
<StyledDialog open={renameModalOpen} onClose={closeRenameModal}>
|
||||||
<DialogTitle>Rename Passkey</DialogTitle>
|
<DialogTitle>Rename Passkey</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -374,9 +376,9 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
|||||||
Rename
|
Rename
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
|
|
||||||
<Dialog open={deleteModalOpen} onClose={closeDeleteModal} sx={DIALOG_SX}>
|
<StyledDialog open={deleteModalOpen} onClose={closeDeleteModal}>
|
||||||
<DialogTitle>Delete Passkey</DialogTitle>
|
<DialogTitle>Delete Passkey</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<p>
|
<p>
|
||||||
@@ -402,9 +404,25 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
|||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasskeyManager;
|
export default PasskeyManager;
|
||||||
|
|
||||||
|
PasskeyManager.propTypes = {
|
||||||
|
credentials: arrayOf(shape({
|
||||||
|
id: string.isRequired,
|
||||||
|
credentialName: string.isRequired,
|
||||||
|
createdAt: string,
|
||||||
|
lastUsedAt: string,
|
||||||
|
isBackupEligible: bool,
|
||||||
|
isBackupAuthenticated: bool,
|
||||||
|
})).isRequired,
|
||||||
|
apiRoutes: shape({
|
||||||
|
credentials: string.isRequired,
|
||||||
|
registrationBegin: string.isRequired,
|
||||||
|
registrationComplete: string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BarChart } from '@mui/x-charts/BarChart';
|
import { BarChart } from '@mui/x-charts/BarChart';
|
||||||
import { LineChart } from '@mui/x-charts/LineChart';
|
import { LineChart } from '@mui/x-charts/LineChart';
|
||||||
import { PieChart } from '@mui/x-charts/PieChart';
|
import { PieChart } from '@mui/x-charts/PieChart';
|
||||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
|
import { shape, arrayOf, number, string } from 'prop-types';
|
||||||
|
|
||||||
const darkTheme = createTheme({
|
const darkTheme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@@ -136,3 +146,20 @@ export default function ProfileCharts({ chartData }) {
|
|||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProfileCharts.propTypes = {
|
||||||
|
chartData: shape({
|
||||||
|
months: arrayOf(string).isRequired,
|
||||||
|
wins: arrayOf(number).isRequired,
|
||||||
|
losses: arrayOf(number).isRequired,
|
||||||
|
draws: arrayOf(number).isRequired,
|
||||||
|
pieWins: number.isRequired,
|
||||||
|
pieLosses: number.isRequired,
|
||||||
|
pieDraws: number.isRequired,
|
||||||
|
recentGames: shape({
|
||||||
|
labels: arrayOf(string).isRequired,
|
||||||
|
mines: arrayOf(number).isRequired,
|
||||||
|
bonus: arrayOf(number).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,92 +7,48 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
export default function Avatar({ name, color, avatarUrl, bonusPoints = 0 }) {
|
export const Avatar = ({ name, color, avatarUrl, bonusPoints = 0 }) => {
|
||||||
const isRed = 'red' === color;
|
const isRed = 'red' === color;
|
||||||
const initials = (name || '?').slice(0, 2).toUpperCase();
|
const initials = useMemo(() => (name || '?').slice(0, 2).toUpperCase(), [name]);
|
||||||
|
|
||||||
const gradient = isRed
|
const cssVars = isRed ? {
|
||||||
? 'linear-gradient(135deg, rgba(173,10,5,0.6) 0%, rgba(246,125,82,0.4) 100%)'
|
'--bd-avatar-gradient': 'linear-gradient(135deg, rgba(173,10,5,0.6) 0%, rgba(246,125,82,0.4) 100%)',
|
||||||
: 'linear-gradient(135deg, rgba(35,111,135,0.6) 0%, rgba(41,128,185,0.4) 100%)';
|
'--bd-avatar-glow': '0 0 0 3px rgba(173,10,5,0.2), 0 0 28px rgba(173,10,5,0.35)',
|
||||||
const glow = isRed
|
'--bd-avatar-border': 'rgba(173,10,5,0.5)',
|
||||||
? '0 0 0 3px rgba(173,10,5,0.2), 0 0 28px rgba(173,10,5,0.35)'
|
'--bd-avatar-color': '#f67d52',
|
||||||
: '0 0 0 3px rgba(35,111,135,0.2), 0 0 28px rgba(35,111,135,0.35)';
|
} : {
|
||||||
const border = isRed
|
'--bd-avatar-gradient': 'linear-gradient(135deg, rgba(35,111,135,0.6) 0%, rgba(41,128,185,0.4) 100%)',
|
||||||
? 'rgba(173,10,5,0.5)'
|
'--bd-avatar-glow': '0 0 0 3px rgba(35,111,135,0.2), 0 0 28px rgba(35,111,135,0.35)',
|
||||||
: 'rgba(35,111,135,0.5)';
|
'--bd-avatar-border': 'rgba(35,111,135,0.5)',
|
||||||
const textColor = isRed ? '#f67d52' : '#95cff5';
|
'--bd-avatar-color': '#95cff5',
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, position: 'relative' }}>
|
<div className="bd-avatar-wrap" style={cssVars}>
|
||||||
<div style={{ position: 'relative' }}>
|
<div className="bd-avatar-ring-wrap">
|
||||||
<div style={{
|
<div className="bd-avatar-ring">
|
||||||
width: 72, height: 72, borderRadius: '50%',
|
{avatarUrl
|
||||||
background: avatarUrl ? 'transparent' : gradient,
|
? <img src={avatarUrl} alt={name} className="bd-avatar-img" />
|
||||||
border: `2px solid ${border}`,
|
: initials}
|
||||||
boxShadow: glow,
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
||||||
font: '800 24px \'Rajdhani\', sans-serif',
|
|
||||||
color: textColor,
|
|
||||||
letterSpacing: 2,
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{avatarUrl ? (
|
|
||||||
<img
|
|
||||||
src={avatarUrl}
|
|
||||||
alt={name}
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
objectFit: 'cover',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
initials
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{0 < bonusPoints && (
|
{0 < bonusPoints && (
|
||||||
<div style={{
|
<div className="bd-avatar-bonus">
|
||||||
position: 'absolute',
|
<i className="fa fa-star" />
|
||||||
bottom: -6,
|
|
||||||
right: -6,
|
|
||||||
background: '#ffd700',
|
|
||||||
borderRadius: '50%',
|
|
||||||
width: 28,
|
|
||||||
height: 28,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
boxShadow: '0 0 12px rgba(255, 215, 0, 0.6), 0 0 0 2px rgba(7, 9, 13, 1)',
|
|
||||||
border: '2px solid rgba(0,0,0,0.5)',
|
|
||||||
zIndex: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="fa fa-star" style={{ color: '#000', fontSize: 14 }} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<span className="bd-avatar-name">{name}</span>
|
||||||
font: '700 15px \'Rajdhani\', sans-serif',
|
<span className="bd-avatar-side">{isRed ? 'Red' : 'Blue'}</span>
|
||||||
color: textColor,
|
|
||||||
letterSpacing: 1,
|
|
||||||
maxWidth: 120, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
<span style={{
|
|
||||||
font: '600 10px \'Rajdhani\', sans-serif',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 2,
|
|
||||||
color: 'rgba(255,255,255,0.3)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isRed ? 'Red' : 'Blue'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Avatar.propTypes = {
|
||||||
|
name: string,
|
||||||
|
color: string,
|
||||||
|
avatarUrl: string,
|
||||||
|
bonusPoints: string,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { useMemo } from 'react';
|
/**
|
||||||
import StatRow from './StatRow';
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
export default function BonusPoints({ game }) {
|
import { useMemo } from 'react';
|
||||||
|
import { StatRow } from './StatRow';
|
||||||
|
import { object } from 'prop-types';
|
||||||
|
|
||||||
|
export const BonusPoints = ({ game }) => {
|
||||||
const hasBonuspoints = useMemo(
|
const hasBonuspoints = useMemo(
|
||||||
() => 0 < game?.redBonusPoints
|
() => 0 < game?.redBonusPoints
|
||||||
|| 0 < game?.blueBonusPoints
|
|| 0 < game?.blueBonusPoints
|
||||||
@@ -45,95 +55,68 @@ export default function BonusPoints({ game }) {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasBonuspoints) {
|
if (!hasBonuspoints) return '';
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div className="bd-bonus">
|
||||||
padding: '16px 20px 0',
|
<div className="bd-bonus__grid">
|
||||||
borderTop: '1px solid rgba(255,255,255,0.08)',
|
<div className="bd-bonus__column bd-bonus__column--red">
|
||||||
marginTop: 16,
|
<span className="bd-bonus__heading">
|
||||||
marginBottom: 16,
|
<i className="fa fa-star" /> Red Bonus Statistics
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
|
|
||||||
{/* Red Bonus */}
|
|
||||||
<div style={{
|
|
||||||
padding: 16,
|
|
||||||
border: '1px solid rgba(173,10,5,0.2)',
|
|
||||||
borderRadius: 6,
|
|
||||||
background: 'rgba(173,10,5,0.05)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{
|
|
||||||
font: '700 12px \'Rajdhani\', sans-serif',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 2,
|
|
||||||
color: '#ffd700',
|
|
||||||
display: 'block',
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="fa fa-star" style={{ marginRight: 8 }} /> Red Bonus Statistics
|
|
||||||
</span>
|
</span>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
<div className="bd-bonus__rows">
|
||||||
<StatRow
|
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.redBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||||
icon="fa-star" label="Total Bonus Points" value={(game.redBonusPoints ?? 0).toFixed(1)}
|
{0 < game.redBonusStats?.blindHits && (
|
||||||
valueColor="#ffd700"
|
<StatRow icon="fa-bullseye" label="Blind hits" value={game.redBonusStats.blindHits} />
|
||||||
/>
|
)}
|
||||||
{0 < game.redBonusStats?.blindHits
|
{0 < game.redBonusStats?.chainBest && (
|
||||||
&& <StatRow icon="fa-bullseye" label="Blind hits" value={game.redBonusStats.blindHits} />}
|
<StatRow icon="fa-link" label="Best chain" value={game.redBonusStats.chainBest} />
|
||||||
{0 < game.redBonusStats?.chainBest
|
)}
|
||||||
&& <StatRow icon="fa-link" label="Best chain" value={game.redBonusStats.chainBest} />}
|
{0 < game.redBonusStats?.edgeMines && (
|
||||||
{0 < game.redBonusStats?.edgeMines
|
<StatRow icon="fa-border-all" label="Edge mines" value={game.redBonusStats.edgeMines} />
|
||||||
&& <StatRow icon="fa-border-all" label="Edge mines" value={game.redBonusStats.edgeMines} />}
|
)}
|
||||||
{0 < game.redBonusStats?.lastMineHits
|
{0 < game.redBonusStats?.lastMineHits && (
|
||||||
&& <StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.redBonusStats.lastMineHits} />}
|
<StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.redBonusStats.lastMineHits} />
|
||||||
{0 < game.redBonusStats?.biggestReveal
|
)}
|
||||||
&& <StatRow icon="fa-expand" label="Biggest reveal" value={game.redBonusStats.biggestReveal} />}
|
{0 < game.redBonusStats?.biggestReveal && (
|
||||||
{hasRedNoBonuses
|
<StatRow icon="fa-expand" label="Biggest reveal" value={game.redBonusStats.biggestReveal} />
|
||||||
&& <StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />}
|
)}
|
||||||
|
{hasRedNoBonuses && (
|
||||||
|
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{
|
<div className="bd-bonus__column bd-bonus__column--blue">
|
||||||
padding: 16,
|
<span className="bd-bonus__heading">
|
||||||
border: '1px solid rgba(149,207,245,0.2)',
|
<i className="fa fa-star" /> Blue Bonus Statistics
|
||||||
borderRadius: 6,
|
|
||||||
background: 'rgba(149,207,245,0.05)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span style={{
|
|
||||||
font: '700 12px \'Rajdhani\', sans-serif',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: 2,
|
|
||||||
color: '#ffd700',
|
|
||||||
display: 'block',
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i className="fa fa-star" style={{ marginRight: 8 }} /> Blue Bonus Statistics
|
|
||||||
</span>
|
</span>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
<div className="bd-bonus__rows">
|
||||||
<StatRow
|
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.blueBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||||
icon="fa-star" label="Total Bonus Points" value={(game.blueBonusPoints ?? 0).toFixed(1)}
|
{0 < game.blueBonusStats?.blindHits && (
|
||||||
valueColor="#ffd700"
|
<StatRow icon="fa-bullseye" label="Blind hits" value={game.blueBonusStats.blindHits} />
|
||||||
/>
|
)}
|
||||||
{0 < game.blueBonusStats?.blindHits
|
{0 < game.blueBonusStats?.chainBest && (
|
||||||
&& <StatRow icon="fa-bullseye" label="Blind hits" value={game.blueBonusStats.blindHits} />}
|
<StatRow icon="fa-link" label="Best chain" value={game.blueBonusStats.chainBest} />
|
||||||
{0 < game.blueBonusStats?.chainBest
|
)}
|
||||||
&& <StatRow icon="fa-link" label="Best chain" value={game.blueBonusStats.chainBest} />}
|
{0 < game.blueBonusStats?.edgeMines && (
|
||||||
{0 < game.blueBonusStats?.edgeMines
|
<StatRow icon="fa-border-all" label="Edge mines" value={game.blueBonusStats.edgeMines} />
|
||||||
&& <StatRow icon="fa-border-all" label="Edge mines" value={game.blueBonusStats.edgeMines} />}
|
)}
|
||||||
{0 < game.blueBonusStats?.lastMineHits
|
{0 < game.blueBonusStats?.lastMineHits && (
|
||||||
&& <StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.blueBonusStats.lastMineHits} />}
|
<StatRow icon="fa-hourglass-end" label="Endgame mines" value={game.blueBonusStats.lastMineHits} />
|
||||||
{0 < game.blueBonusStats?.biggestReveal
|
)}
|
||||||
&& <StatRow icon="fa-expand" label="Biggest reveal" value={game.blueBonusStats.biggestReveal} />}
|
{0 < game.blueBonusStats?.biggestReveal && (
|
||||||
{hasBlueNoBonuses
|
<StatRow icon="fa-expand" label="Biggest reveal" value={game.blueBonusStats.biggestReveal} />
|
||||||
&& <StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />}
|
)}
|
||||||
|
{hasBlueNoBonuses && (
|
||||||
|
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
BonusPoints.propTypes = {
|
||||||
|
game: object.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,33 +8,24 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { node, string } from 'prop-types';
|
||||||
|
|
||||||
export default function StatRow({ icon, label, value, valueColor }) {
|
export const StatRow = ({ icon, label, value, valueColor }) => (
|
||||||
return (
|
<div className="bd-stat-row">
|
||||||
<div style={{
|
<i className={`fa ${icon} bd-stat-row__icon`} />
|
||||||
display: 'flex', alignItems: 'center',
|
<span className="bd-stat-row__label">{label}</span>
|
||||||
gap: 10, padding: '9px 0',
|
<span
|
||||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
className="bd-stat-row__value"
|
||||||
}}
|
style={valueColor ? { '--bd-stat-value-color': valueColor } : undefined}
|
||||||
>
|
>
|
||||||
<i className={`fa ${icon}`} style={{ width: 16, color: 'rgba(149,207,245,0.4)', fontSize: 13 }} />
|
{value}
|
||||||
<span style={{
|
</span>
|
||||||
font: '500 13px \'Rajdhani\', sans-serif',
|
</div>
|
||||||
color: 'rgba(255,255,255,0.45)',
|
);
|
||||||
flex: 1,
|
|
||||||
letterSpacing: 0.5,
|
StatRow.propTypes = {
|
||||||
}}
|
icon: string.isRequired,
|
||||||
>
|
label: string.isRequired,
|
||||||
{label}
|
value: node.isRequired,
|
||||||
</span>
|
valueColor: string,
|
||||||
<span style={{
|
};
|
||||||
font: '700 13px \'Rajdhani\', sans-serif',
|
|
||||||
color: valueColor || 'rgba(255,255,255,0.75)',
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
18
assets/js/components/index.js
Normal file
18
assets/js/components/index.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { AvatarUpload } from './AvatarUpload';
|
||||||
|
export { BattleDialog } from './BattleDialog';
|
||||||
|
export { default as ContactForm } from './ContactForm';
|
||||||
|
export { default as PasskeyLogin } from './PasskeyLogin';
|
||||||
|
export { default as PasskeyManager } from './PasskeyManager';
|
||||||
|
export { default as ProfileCharts } from './ProfileCharts';
|
||||||
|
export { BonusPoints } from './battle-dialog/BonusPoints';
|
||||||
|
export { Avatar } from './battle-dialog/Avatar';
|
||||||
|
export { StatRow } from './battle-dialog/StatRow';
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import ContactForm from './components/ContactForm';
|
import { ContactForm } from '@global-components';
|
||||||
|
|
||||||
const wrapper = document.getElementById('contact-form-wrapper');
|
const wrapper = document.getElementById('contact-form-wrapper');
|
||||||
|
|
||||||
@@ -28,4 +28,3 @@ if (wrapper) {
|
|||||||
console.error('ContactForm: Missing siteKey or recaptchaFieldId in data attributes');
|
console.error('ContactForm: Missing siteKey or recaptchaFieldId in data attributes');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import React, { useRef } from 'react';
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { GameProvider } from '@mine-contexts';
|
import { GameProvider } from '@mine-contexts';
|
||||||
import { GameBoard } from '@mine-components';
|
import { GameBoard } from '@mine-components';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
@@ -34,3 +35,9 @@ const MineSeeker = ({ env, gameId, opponentName = '' }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default MineSeeker;
|
export default MineSeeker;
|
||||||
|
|
||||||
|
MineSeeker.propTypes = {
|
||||||
|
env: string.isRequired,
|
||||||
|
gameId: string,
|
||||||
|
opponentName: string,
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { func, number, string } from 'prop-types';
|
||||||
|
|
||||||
const BonusBox = ({ color, points, onClick, title }) => (
|
const BonusBox = ({ color, points, onClick, title }) => (
|
||||||
<button
|
<button
|
||||||
@@ -23,3 +24,10 @@ const BonusBox = ({ color, points, onClick, title }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default BonusBox;
|
export default BonusBox;
|
||||||
|
|
||||||
|
BonusBox.propTypes = {
|
||||||
|
color: string.isRequired,
|
||||||
|
points: number.isRequired,
|
||||||
|
onClick: func.isRequired,
|
||||||
|
title: string,
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,67 +9,12 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import { BONUS_LABELS } from '@mine-utils';
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { PlayerColumn } from '@mine-components';
|
||||||
const DIALOG_SX = {
|
import { bool, func, shape, string, number, object } from 'prop-types';
|
||||||
'& .MuiDialog-paper': {
|
|
||||||
background: '#07090d',
|
|
||||||
backgroundImage: `
|
|
||||||
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
|
||||||
`,
|
|
||||||
backgroundSize: '46px 46px',
|
|
||||||
border: '1px solid rgba(35, 111, 135, 0.4)',
|
|
||||||
borderRadius: '12px',
|
|
||||||
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
|
||||||
width: '560px',
|
|
||||||
maxWidth: '94vw',
|
|
||||||
overflow: 'hidden',
|
|
||||||
color: '#fff',
|
|
||||||
},
|
|
||||||
'& .MuiBackdrop-root': {
|
|
||||||
background: 'rgba(2, 4, 8, 0.88)',
|
|
||||||
backdropFilter: 'blur(4px)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatPlayerName = name => {
|
|
||||||
if (name && name.startsWith('anon_')) {
|
|
||||||
return 'Anonymous';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name && 10 < name.length) {
|
|
||||||
return name.substring(0, 7) + '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
return name || 'Unknown';
|
|
||||||
};
|
|
||||||
|
|
||||||
const PlayerColumn = ({ color, player }) => (
|
|
||||||
<div className={`bsd-column bsd-column--${color}`}>
|
|
||||||
<div className="bsd-column-header">
|
|
||||||
<span className="bsd-column-name">{formatPlayerName(player.name)}</span>
|
|
||||||
<span className="bsd-column-total">
|
|
||||||
<i className="fa fa-star" />
|
|
||||||
{player.bonusPoints}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ul className="bsd-stats">
|
|
||||||
{Object.entries(BONUS_LABELS).map(([key, { label, desc }]) => (
|
|
||||||
<li key={key} className="bsd-stat">
|
|
||||||
<div className="bsd-stat-text">
|
|
||||||
<span className="bsd-stat-label">{label}</span>
|
|
||||||
<span className="bsd-stat-desc">{desc}</span>
|
|
||||||
</div>
|
|
||||||
<span className="bsd-stat-value">{player.bonusStats?.[key] ?? 0}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const BonusStatsDialog = ({ open, onClose, red, blue }) => (
|
const BonusStatsDialog = ({ open, onClose, red, blue }) => (
|
||||||
<Dialog open={open} onClose={onClose} sx={DIALOG_SX}>
|
<StyledDialog open={open} onClose={onClose}>
|
||||||
<div className="bsd">
|
<div className="bsd">
|
||||||
<div className="bsd-header">
|
<div className="bsd-header">
|
||||||
<div className="bsd-header-text">
|
<div className="bsd-header-text">
|
||||||
@@ -91,7 +36,44 @@ const BonusStatsDialog = ({ open, onClose, red, blue }) => (
|
|||||||
Bonus points are awarded alongside the main score for skillful play.
|
Bonus points are awarded alongside the main score for skillful play.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
background: '#07090d',
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '46px 46px',
|
||||||
|
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||||
|
width: '560px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default BonusStatsDialog;
|
export default BonusStatsDialog;
|
||||||
|
|
||||||
|
BonusStatsDialog.propTypes = {
|
||||||
|
open: bool.isRequired,
|
||||||
|
onClose: func.isRequired,
|
||||||
|
red: shape({
|
||||||
|
name: string,
|
||||||
|
bonusPoints: number,
|
||||||
|
bonusStats: object,
|
||||||
|
}).isRequired,
|
||||||
|
blue: shape({
|
||||||
|
name: string,
|
||||||
|
bonusPoints: number,
|
||||||
|
bonusStats: object,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { func, node, string } from 'prop-types';
|
||||||
|
|
||||||
const CAPTCHA_STORAGE_KEY = 'mineseeker_captcha_verified';
|
const CAPTCHA_STORAGE_KEY = 'mineseeker_captcha_verified';
|
||||||
const CAPTCHA_TOKEN_KEY = 'mineseeker_captcha_token';
|
const CAPTCHA_TOKEN_KEY = 'mineseeker_captcha_token';
|
||||||
@@ -87,7 +88,7 @@ const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (verified) {
|
if (verified) {
|
||||||
return <>{children}</>;
|
return <Fragment>{children}</Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -114,3 +115,9 @@ const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default CaptchaOverlay;
|
export default CaptchaOverlay;
|
||||||
|
|
||||||
|
CaptchaOverlay.propTypes = {
|
||||||
|
siteKey: string.isRequired,
|
||||||
|
onVerified: func,
|
||||||
|
children: node,
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
import { Fragment, useEffect, useState } from 'react';
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
|
import { func, number } from 'prop-types';
|
||||||
|
|
||||||
const ChallengeCountdown = ({ onAccept, onDecline, seconds = 30 }) => {
|
const ChallengeCountdown = ({ onAccept, onDecline, seconds = 30 }) => {
|
||||||
const [countdown, setCountdown] = useState(seconds);
|
const [countdown, setCountdown] = useState(seconds);
|
||||||
@@ -39,3 +40,9 @@ const ChallengeCountdown = ({ onAccept, onDecline, seconds = 30 }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ChallengeCountdown;
|
export default ChallengeCountdown;
|
||||||
|
|
||||||
|
ChallengeCountdown.propTypes = {
|
||||||
|
onAccept: func.isRequired,
|
||||||
|
onDecline: func.isRequired,
|
||||||
|
seconds: number,
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useGame } from '@mine-contexts';
|
|||||||
import { useServerCommunication } from '@mine-hooks';
|
import { useServerCommunication } from '@mine-hooks';
|
||||||
import CaptchaOverlay from './CaptchaOverlay';
|
import CaptchaOverlay from './CaptchaOverlay';
|
||||||
import GridControl from './grid/GridControl';
|
import GridControl from './grid/GridControl';
|
||||||
|
import { bool, string } from 'prop-types';
|
||||||
|
|
||||||
export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDev }) => {
|
export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDev }) => {
|
||||||
const { gridReady } = useGame();
|
const { gridReady } = useGame();
|
||||||
@@ -42,3 +43,10 @@ export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDe
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
GameBoard.propTypes = {
|
||||||
|
gameAssoc: string.isRequired,
|
||||||
|
gameInherited: bool.isRequired,
|
||||||
|
opponentName: string,
|
||||||
|
isEnvDev: bool,
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,22 +7,10 @@
|
|||||||
* file that was distributed with this source code.
|
* file that was distributed with this source code.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { BonusBox, BonusStatsDialog, Avatar } from '@mine-components';
|
||||||
|
import { formatTime } from '@global-utils/format';
|
||||||
import { useGame } from '@mine-contexts';
|
import { useGame } from '@mine-contexts';
|
||||||
import BonusBox from './BonusBox';
|
|
||||||
import BonusStatsDialog from './BonusStatsDialog';
|
|
||||||
|
|
||||||
const renderAvatar = player => {
|
|
||||||
if (!player.registered) return null;
|
|
||||||
return (
|
|
||||||
<div className="timer-avatar">
|
|
||||||
{player.avatar
|
|
||||||
? <img src={player.avatar} alt={player.name} className="timer-avatar__img" />
|
|
||||||
: <span className="timer-avatar__initials">{player.name.slice(0, 2).toUpperCase()}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GameTimer = () => {
|
const GameTimer = () => {
|
||||||
const { overlay, connectionLost, endRef, activePlayer, red, blue } = useGame();
|
const { overlay, connectionLost, endRef, activePlayer, red, blue } = useGame();
|
||||||
@@ -85,84 +73,61 @@ const GameTimer = () => {
|
|||||||
}
|
}
|
||||||
}, [activePlayer, isRunning]);
|
}, [activePlayer, isRunning]);
|
||||||
|
|
||||||
|
const syncTimes = useCallback(() => {
|
||||||
|
let currentRedTime = pausedRedTimeRef.current;
|
||||||
|
let currentBlueTime = pausedBlueTimeRef.current;
|
||||||
|
|
||||||
|
if (!activePlayer && redStartTimeRef.current) {
|
||||||
|
currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000);
|
||||||
|
} else if (activePlayer && blueStartTimeRef.current) {
|
||||||
|
currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRedTime(currentRedTime);
|
||||||
|
setBlueTime(currentBlueTime);
|
||||||
|
}, [activePlayer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
if (timerIntervalRef.current) {
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
clearInterval(timerIntervalRef.current);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
timerIntervalRef.current = setInterval(() => {
|
timerIntervalRef.current = setInterval(syncTimes, 100);
|
||||||
let currentRedTime = pausedRedTimeRef.current;
|
|
||||||
let currentBlueTime = pausedBlueTimeRef.current;
|
|
||||||
|
|
||||||
if (!activePlayer && redStartTimeRef.current) {
|
|
||||||
currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000);
|
|
||||||
} else if (activePlayer && blueStartTimeRef.current) {
|
|
||||||
currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRedTime(currentRedTime);
|
|
||||||
setBlueTime(currentBlueTime);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (timerIntervalRef.current) {
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
clearInterval(timerIntervalRef.current);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [isRunning, activePlayer]);
|
}, [isRunning, activePlayer, syncTimes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFocus = () => {
|
const handleFocus = () => {
|
||||||
if (isRunning) {
|
if (isRunning) syncTimes();
|
||||||
let currentRedTime = pausedRedTimeRef.current;
|
|
||||||
let currentBlueTime = pausedBlueTimeRef.current;
|
|
||||||
|
|
||||||
if (!activePlayer && redStartTimeRef.current) {
|
|
||||||
currentRedTime += Math.floor((Date.now() - redStartTimeRef.current) / 1000);
|
|
||||||
} else if (activePlayer && blueStartTimeRef.current) {
|
|
||||||
currentBlueTime += Math.floor((Date.now() - blueStartTimeRef.current) / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRedTime(currentRedTime);
|
|
||||||
setBlueTime(currentBlueTime);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('focus', handleFocus);
|
window.addEventListener('focus', handleFocus);
|
||||||
return () => window.removeEventListener('focus', handleFocus);
|
return () => window.removeEventListener('focus', handleFocus);
|
||||||
}, [isRunning, activePlayer]);
|
}, [isRunning, activePlayer, syncTimes]);
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const formatTime = seconds => {
|
|
||||||
const mins = Math.floor(seconds / 60);
|
|
||||||
const secs = seconds % 60;
|
|
||||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const openBonusDialog = () => setBonusDialogOpen(true);
|
|
||||||
const closeBonusDialog = () => setBonusDialogOpen(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="game-timer-container">
|
<div className="game-timer-container">
|
||||||
<BonusBox color="red" points={red.bonusPoints ?? 0} onClick={openBonusDialog} />
|
<BonusBox color="red" points={red.bonusPoints ?? 0} onClick={() => setBonusDialogOpen(true)} />
|
||||||
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
||||||
{renderAvatar(red)}
|
<Avatar player={red} />
|
||||||
<i className={`fa ${!activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
<i className={`fa ${!activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||||
<span className="timer-display">{formatTime(redTime)}</span>
|
<span className="timer-display">{formatTime(redTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`game-timer blue-timer ${activePlayer ? 'active' : ''}`}>
|
<div className={`game-timer blue-timer ${activePlayer ? 'active' : ''}`}>
|
||||||
{renderAvatar(blue)}
|
<Avatar player={blue} />
|
||||||
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||||
<span className="timer-display">{formatTime(blueTime)}</span>
|
<span className="timer-display">{formatTime(blueTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
<BonusBox color="blue" points={blue.bonusPoints ?? 0} onClick={openBonusDialog} />
|
<BonusBox color="blue" points={blue.bonusPoints ?? 0} onClick={() => setBonusDialogOpen(true)} />
|
||||||
<BonusStatsDialog open={bonusDialogOpen} onClose={closeBonusDialog} red={red} blue={blue} />
|
<BonusStatsDialog open={bonusDialogOpen} onClose={() => setBonusDialogOpen(false)} red={red} blue={blue} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,37 +8,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { formatSince } from '@global-utils/format';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
import { useLobbyDataProvider } from '@mine-hooks';
|
import { useLobbyDataProvider } from '@mine-hooks';
|
||||||
|
import { bool, func, string } from 'prop-types';
|
||||||
const DIALOG_SX = {
|
|
||||||
'& .MuiDialog-paper': {
|
|
||||||
background: '#07090d',
|
|
||||||
backgroundImage: `
|
|
||||||
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
|
||||||
`,
|
|
||||||
backgroundSize: '46px 46px',
|
|
||||||
border: '1px solid rgba(35, 111, 135, 0.4)',
|
|
||||||
borderRadius: '12px',
|
|
||||||
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
|
||||||
width: '500px',
|
|
||||||
maxWidth: '94vw',
|
|
||||||
overflow: 'hidden',
|
|
||||||
color: '#fff',
|
|
||||||
},
|
|
||||||
'& .MuiBackdrop-root': {
|
|
||||||
background: 'rgba(2, 4, 8, 0.88)',
|
|
||||||
backdropFilter: 'blur(4px)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatSince = isoStr => {
|
|
||||||
const diff = Math.floor((Date.now() - new Date(isoStr)) / 60000);
|
|
||||||
if (1 > diff) return 'just now';
|
|
||||||
if (1 === diff) return '1 min ago';
|
|
||||||
return `${diff} min ago`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc, isEnvDev = false }) => {
|
const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc, isEnvDev = false }) => {
|
||||||
const [players, setPlayers] = useState([]);
|
const [players, setPlayers] = useState([]);
|
||||||
@@ -171,10 +145,9 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc, isEnvDev = false
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<StyledDialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={0 < waitingCountdown ? undefined : onClose}
|
onClose={0 < waitingCountdown ? undefined : onClose}
|
||||||
sx={DIALOG_SX}
|
|
||||||
>
|
>
|
||||||
<div className="opd">
|
<div className="opd">
|
||||||
<div className="opd-header">
|
<div className="opd-header">
|
||||||
@@ -285,8 +258,37 @@ const OnlinePlayersDialog = ({ open, onClose, currentGameAssoc, isEnvDev = false
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</StyledDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
background: '#07090d',
|
||||||
|
backgroundImage: `
|
||||||
|
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||||
|
`,
|
||||||
|
backgroundSize: '46px 46px',
|
||||||
|
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||||
|
width: '500px',
|
||||||
|
maxWidth: '94vw',
|
||||||
|
overflow: 'hidden',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'& .MuiBackdrop-root': {
|
||||||
|
background: 'rgba(2, 4, 8, 0.88)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default OnlinePlayersDialog;
|
export default OnlinePlayersDialog;
|
||||||
|
|
||||||
|
OnlinePlayersDialog.propTypes = {
|
||||||
|
open: bool.isRequired,
|
||||||
|
onClose: func.isRequired,
|
||||||
|
currentGameAssoc: string,
|
||||||
|
isEnvDev: bool,
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { OnlinePlayersDialog } from '@mine-components';
|
import { OnlinePlayersDialog } from '@mine-components';
|
||||||
|
import { bool, string } from 'prop-types';
|
||||||
|
|
||||||
const WaitingOverlayContent = ({ shareUrl, currentGameAssoc, opponentName = '', inviteOnly = false }) => {
|
const WaitingOverlayContent = ({ shareUrl, currentGameAssoc, opponentName = '', inviteOnly = false }) => {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
@@ -94,3 +95,10 @@ const ShareLinkBox = ({ url }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default WaitingOverlayContent;
|
export default WaitingOverlayContent;
|
||||||
|
|
||||||
|
WaitingOverlayContent.propTypes = {
|
||||||
|
shareUrl: string.isRequired,
|
||||||
|
currentGameAssoc: string,
|
||||||
|
opponentName: string,
|
||||||
|
inviteOnly: bool,
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import GridField from './GridField';
|
|||||||
import UserControl from '../user/UserControl';
|
import UserControl from '../user/UserControl';
|
||||||
import GameTimer from '../GameTimer';
|
import GameTimer from '../GameTimer';
|
||||||
import { BOMB_SYMBOLS, bombRadius } from '@mine-utils';
|
import { BOMB_SYMBOLS, bombRadius } from '@mine-utils';
|
||||||
|
import { func, string } from 'prop-types';
|
||||||
|
|
||||||
const GridControl = ({ gameAssoc, onClick, resign }) => {
|
const GridControl = ({ gameAssoc, onClick, resign }) => {
|
||||||
const {
|
const {
|
||||||
@@ -61,10 +62,11 @@ const GridControl = ({ gameAssoc, onClick, resign }) => {
|
|||||||
<div className={`game-overlay${overlay ? '' : ' hide'}`}>
|
<div className={`game-overlay${overlay ? '' : ' hide'}`}>
|
||||||
<div className="game-overlay-window">
|
<div className="game-overlay-window">
|
||||||
<h1>{overlayTitle}</h1>
|
<h1>{overlayTitle}</h1>
|
||||||
{'string' === typeof overlaySubTitle ? (
|
{'string' === typeof overlaySubTitle && (
|
||||||
<h2>{overlaySubTitle}</h2>
|
<h2>{overlaySubTitle}</h2>
|
||||||
) : (
|
)}
|
||||||
overlaySubTitle
|
{'string' !== typeof overlaySubTitle && (
|
||||||
|
<Fragment>{overlaySubTitle}</Fragment>
|
||||||
)}
|
)}
|
||||||
{gameAssoc && endRef.current && (
|
{gameAssoc && endRef.current && (
|
||||||
<div className="game-overlay-actions">
|
<div className="game-overlay-actions">
|
||||||
@@ -113,3 +115,9 @@ const GridControl = ({ gameAssoc, onClick, resign }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default GridControl;
|
export default GridControl;
|
||||||
|
|
||||||
|
GridControl.propTypes = {
|
||||||
|
gameAssoc: string,
|
||||||
|
onClick: func.isRequired,
|
||||||
|
resign: func.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import React, { memo, useMemo } from 'react';
|
import React, { memo, useMemo } from 'react';
|
||||||
import { IMAGES } from '@mine-utils';
|
import { IMAGES } from '@mine-utils';
|
||||||
|
import { func, shape, bool, number, string } from 'prop-types';
|
||||||
|
|
||||||
const bombSrc = area => {
|
const bombSrc = area => {
|
||||||
if (null === area) return null;
|
if (null === area) return null;
|
||||||
@@ -75,3 +76,16 @@ const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default GridField;
|
export default GridField;
|
||||||
|
|
||||||
|
GridField.propTypes = {
|
||||||
|
cell: shape({
|
||||||
|
currentImage: string,
|
||||||
|
currentObj: string,
|
||||||
|
active: bool,
|
||||||
|
lastClickedRed: bool,
|
||||||
|
lastClickedBlue: bool,
|
||||||
|
bombTargetArea: number,
|
||||||
|
}).isRequired,
|
||||||
|
onClick: func.isRequired,
|
||||||
|
onMouseEnter: func.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,3 +16,7 @@ export { default as GridControl } from './grid/GridControl';
|
|||||||
export { default as GridField } from './grid/GridField';
|
export { default as GridField } from './grid/GridField';
|
||||||
export { default as User } from './user/User';
|
export { default as User } from './user/User';
|
||||||
export { default as UserControl } from './user/UserControl';
|
export { default as UserControl } from './user/UserControl';
|
||||||
|
export { default as BonusBox } from './BonusBox';
|
||||||
|
export { default as BonusStatsDialog } from './BonusStatsDialog';
|
||||||
|
export { Avatar } from './timer/Avatar';
|
||||||
|
export { PlayerColumn } from './profile/PlayerColumn';
|
||||||
|
|||||||
52
assets/js/mine-seeker/components/profile/PlayerColumn.jsx
Normal file
52
assets/js/mine-seeker/components/profile/PlayerColumn.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { object, string } from 'prop-types';
|
||||||
|
import { BONUS_LABELS } from '@mine-utils';
|
||||||
|
|
||||||
|
const formatPlayerName = name => {
|
||||||
|
if (name && name.startsWith('anon_')) {
|
||||||
|
return 'Anonymous';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name && 10 < name.length) {
|
||||||
|
return name.substring(0, 7) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return name || 'Unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PlayerColumn = ({ color, player }) => (
|
||||||
|
<div className={`bsd-column bsd-column--${color}`}>
|
||||||
|
<div className="bsd-column-header">
|
||||||
|
<span className="bsd-column-name">{formatPlayerName(player.name)}</span>
|
||||||
|
<span className="bsd-column-total">
|
||||||
|
<i className="fa fa-star" />
|
||||||
|
{player.bonusPoints}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul className="bsd-stats">
|
||||||
|
{Object.entries(BONUS_LABELS).map(([key, { label, desc }]) => (
|
||||||
|
<li key={key} className="bsd-stat">
|
||||||
|
<div className="bsd-stat-text">
|
||||||
|
<span className="bsd-stat-label">{label}</span>
|
||||||
|
<span className="bsd-stat-desc">{desc}</span>
|
||||||
|
</div>
|
||||||
|
<span className="bsd-stat-value">{player.bonusStats?.[key] ?? 0}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
PlayerColumn.propTypes = {
|
||||||
|
color: string.isRequired,
|
||||||
|
player: object.isRequired,
|
||||||
|
};
|
||||||
28
assets/js/mine-seeker/components/timer/Avatar.jsx
Normal file
28
assets/js/mine-seeker/components/timer/Avatar.jsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { object } from 'prop-types';
|
||||||
|
|
||||||
|
export const Avatar = ({ player }) => {
|
||||||
|
if (!player.registered) return '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="timer-avatar">
|
||||||
|
{player.avatar
|
||||||
|
? <img src={player.avatar} alt={player.name} className="timer-avatar__img" />
|
||||||
|
: <span className="timer-avatar__initials">{player.name.slice(0, 2).toUpperCase()}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Avatar.propTypes = {
|
||||||
|
player: object.isRequired,
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { IMAGES } from '@mine-utils';
|
import { IMAGES } from '@mine-utils';
|
||||||
|
import { bool, func, number, string } from 'prop-types';
|
||||||
|
|
||||||
const User = memo(function User(
|
const User = memo(function User(
|
||||||
{
|
{
|
||||||
@@ -52,3 +53,15 @@ const User = memo(function User(
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
|
||||||
|
User.propTypes = {
|
||||||
|
color: string.isRequired,
|
||||||
|
webPlayer: string,
|
||||||
|
name: string,
|
||||||
|
desc: string,
|
||||||
|
active: bool,
|
||||||
|
mines: number,
|
||||||
|
haveBomb: bool,
|
||||||
|
enabledBomb: bool,
|
||||||
|
onClickBombSelector: func.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import React, { Fragment, useState } from 'react';
|
|||||||
import { useGame } from '@mine-contexts';
|
import { useGame } from '@mine-contexts';
|
||||||
import User from './User';
|
import User from './User';
|
||||||
import BonusStatsDialog from '../BonusStatsDialog';
|
import BonusStatsDialog from '../BonusStatsDialog';
|
||||||
|
import { func } from 'prop-types';
|
||||||
|
|
||||||
const UserControl = ({ resign }) => {
|
const UserControl = ({ resign }) => {
|
||||||
const { webPlayer, activePlayer, foundMines, red, blue, onBombToggle } = useGame();
|
const { webPlayer, activePlayer, foundMines, red, blue, onBombToggle } = useGame();
|
||||||
@@ -69,3 +70,7 @@ const UserControl = ({ resign }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default UserControl;
|
export default UserControl;
|
||||||
|
|
||||||
|
UserControl.propTypes = {
|
||||||
|
resign: func.isRequired,
|
||||||
|
};
|
||||||
|
|||||||
@@ -112,3 +112,21 @@ export const useLobbyDataProvider = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default useGameDataProvider;
|
export default useGameDataProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile Data Provider Hook
|
||||||
|
* Centralized API communication layer for profile-related mutations
|
||||||
|
*/
|
||||||
|
export const useProfileDataProvider = () => {
|
||||||
|
const uploadAvatarMutation = useMutation({
|
||||||
|
mutationFn: ({ uploadUrl, file }) => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('avatar', file);
|
||||||
|
return fetch(uploadUrl, { method: 'POST', body: fd })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => { if (data.error) throw new Error(data.error); return data; });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { uploadAvatarMutation };
|
||||||
|
};
|
||||||
|
|||||||
@@ -10,9 +10,8 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useGame } from '@mine-contexts';
|
import { useGame } from '@mine-contexts';
|
||||||
import { DESC, IMAGES } from '@mine-utils';
|
import { DESC, IMAGES } from '@mine-utils';
|
||||||
import useStepTimer from './useStepTimer';
|
|
||||||
import useGameDataProvider from './useGameDataProvider';
|
|
||||||
import { ChallengeCountdown, WaitingOverlayContent } from '@mine-components';
|
import { ChallengeCountdown, WaitingOverlayContent } from '@mine-components';
|
||||||
|
import { useGameDataProvider, useStepTimer } from '@mine-hooks';
|
||||||
|
|
||||||
const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev) => {
|
const useServerCommunication = (gameAssoc, gameInherited, opponentName, isEnvDev) => {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -10,24 +10,18 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
|
||||||
const useStepTimer = () => {
|
const useStepTimer = () => {
|
||||||
// Record when the current turn started (timestamp)
|
|
||||||
const turnStartTimeRef = useRef(null);
|
const turnStartTimeRef = useRef(null);
|
||||||
// Flag to track if we've already recorded a turn start
|
|
||||||
const turnStartedRef = useRef(false);
|
const turnStartedRef = useRef(false);
|
||||||
|
|
||||||
const getStepElapsed = (currentActivePlayer, isGameRunning) => {
|
const getStepElapsed = (currentActivePlayer, isGameRunning) => {
|
||||||
// If game not running, return 0
|
|
||||||
if (!isGameRunning) return 0;
|
if (!isGameRunning) return 0;
|
||||||
|
|
||||||
// Only initialize the turn timer ONCE per call to getStepElapsed
|
|
||||||
// This prevents resetting on multiple calls
|
|
||||||
if (!turnStartedRef.current) {
|
if (!turnStartedRef.current) {
|
||||||
turnStartTimeRef.current = Date.now();
|
turnStartTimeRef.current = Date.now();
|
||||||
turnStartedRef.current = true;
|
turnStartedRef.current = true;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// After initialization, just calculate elapsed time
|
|
||||||
if (turnStartTimeRef.current) {
|
if (turnStartTimeRef.current) {
|
||||||
return Math.floor((Date.now() - turnStartTimeRef.current) / 1000);
|
return Math.floor((Date.now() - turnStartTimeRef.current) / 1000);
|
||||||
}
|
}
|
||||||
@@ -40,7 +34,6 @@ const useStepTimer = () => {
|
|||||||
turnStartedRef.current = false;
|
turnStartedRef.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call this when we know a turn has actually changed (from server response)
|
|
||||||
const startNewTurn = () => {
|
const startNewTurn = () => {
|
||||||
turnStartTimeRef.current = Date.now();
|
turnStartTimeRef.current = Date.now();
|
||||||
turnStartedRef.current = true;
|
turnStartedRef.current = true;
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import PasskeyManager from './components/PasskeyManager';
|
import { PasskeyLogin, PasskeyManager } from '@global-components';
|
||||||
import PasskeyLogin from './components/PasskeyLogin';
|
|
||||||
|
|
||||||
const passkeyManagerRoot = document.getElementById('passkey-manager-root');
|
const passkeyManagerRoot = document.getElementById('passkey-manager-root');
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import ProfileCharts from './components/ProfileCharts';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import BattleDialog from './components/BattleDialog';
|
import { AvatarUpload, BattleDialog, ProfileCharts } from '@global-components';
|
||||||
import AvatarUpload from './components/AvatarUpload';
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const avatarRoot = document.getElementById('profile-avatar-root');
|
const avatarRoot = document.getElementById('profile-avatar-root');
|
||||||
if (avatarRoot) {
|
if (avatarRoot) {
|
||||||
const { uploadUrl, thumbUrl, initials } = avatarRoot.dataset;
|
const { uploadUrl, thumbUrl, initials } = avatarRoot.dataset;
|
||||||
createRoot(avatarRoot).render(
|
createRoot(avatarRoot).render(
|
||||||
<AvatarUpload
|
<QueryClientProvider client={queryClient}>
|
||||||
uploadUrl={uploadUrl}
|
<AvatarUpload
|
||||||
initialThumbUrl={thumbUrl || null}
|
uploadUrl={uploadUrl}
|
||||||
initials={initials}
|
initialThumbUrl={thumbUrl || null}
|
||||||
/>,
|
initials={initials}
|
||||||
|
/>
|
||||||
|
</QueryClientProvider>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +48,8 @@ if (list && loadMoreBtn) {
|
|||||||
const batchSize = parseInt(list.dataset.batchSize, 10) || 5;
|
const batchSize = parseInt(list.dataset.batchSize, 10) || 5;
|
||||||
loadMoreBtn.addEventListener('click', () => {
|
loadMoreBtn.addEventListener('click', () => {
|
||||||
const hidden = list.querySelectorAll('.profile-game--hidden');
|
const hidden = list.querySelectorAll('.profile-game--hidden');
|
||||||
Array.from(hidden).slice(0, batchSize).forEach((el) => el.classList.remove('profile-game--hidden'));
|
Array.from(hidden).slice(0, batchSize).forEach(el => el.classList.remove('profile-game--hidden'));
|
||||||
if (list.querySelectorAll('.profile-game--hidden').length === 0) {
|
if (0 === list.querySelectorAll('.profile-game--hidden').length) {
|
||||||
loadMoreBtn.remove();
|
loadMoreBtn.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -47,10 +59,10 @@ const filterInput = document.querySelector('[data-filter]');
|
|||||||
if (list && filterInput) {
|
if (list && filterInput) {
|
||||||
filterInput.addEventListener('input', () => {
|
filterInput.addEventListener('input', () => {
|
||||||
const term = filterInput.value.trim().toLowerCase();
|
const term = filterInput.value.trim().toLowerCase();
|
||||||
list.classList.toggle('is-filtering', term.length > 0);
|
list.classList.toggle('is-filtering', 0 < term.length);
|
||||||
list.querySelectorAll('.profile-game').forEach((card) => {
|
list.querySelectorAll('.profile-game').forEach(card => {
|
||||||
const opp = card.querySelector('.profile-game__opponent')?.textContent.trim().toLowerCase() ?? '';
|
const opp = card.querySelector('.profile-game__opponent')?.textContent.trim().toLowerCase() ?? '';
|
||||||
card.classList.toggle('profile-game--filtered-out', term.length > 0 && !opp.includes(term));
|
card.classList.toggle('profile-game--filtered-out', 0 < term.length && !opp.includes(term));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
44
assets/js/utils/format.js
Normal file
44
assets/js/utils/format.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the SplendidBear Websites' projects.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 @ www.splendidbear.org
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Formats a duration in seconds as MM:SS. */
|
||||||
|
export const formatTime = seconds => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = seconds % 60;
|
||||||
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the difference between two 'YYYY-MM-DD HH:mm' strings as a
|
||||||
|
* human-readable duration (e.g. "1h 4m 23s", "4m 23s", "23s").
|
||||||
|
* Returns null when the inputs are missing or the diff is not positive.
|
||||||
|
*/
|
||||||
|
export const formatDuration = (from, to) => {
|
||||||
|
if (!from || !to) return null;
|
||||||
|
const diffMs = new Date(to.replace(' ', 'T')) - new Date(from.replace(' ', 'T'));
|
||||||
|
if (isNaN(diffMs) || 0 >= diffMs) return null;
|
||||||
|
const totalSec = Math.floor(diffMs / 1000);
|
||||||
|
const h = Math.floor(totalSec / 3600);
|
||||||
|
const m = Math.floor((totalSec % 3600) / 60);
|
||||||
|
const s = totalSec % 60;
|
||||||
|
if (0 < h) return `${h}h ${m}m ${s}s`;
|
||||||
|
if (0 < m) return `${m}m ${s}s`;
|
||||||
|
return `${s}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats an ISO timestamp as a "X min ago" string (minute resolution).
|
||||||
|
* Returns 'just now' for differences under one minute.
|
||||||
|
*/
|
||||||
|
export const formatSince = isoStr => {
|
||||||
|
const diff = Math.floor((Date.now() - new Date(isoStr)) / 60000);
|
||||||
|
if (1 > diff) return 'just now';
|
||||||
|
if (1 === diff) return '1 min ago';
|
||||||
|
return `${diff} min ago`;
|
||||||
|
};
|
||||||
@@ -17,6 +17,8 @@ export default defineConfig({
|
|||||||
'@mine-contexts': resolve(__dirname, './assets/js/mine-seeker/contexts'),
|
'@mine-contexts': resolve(__dirname, './assets/js/mine-seeker/contexts'),
|
||||||
'@mine-hooks': resolve(__dirname, './assets/js/mine-seeker/hooks'),
|
'@mine-hooks': resolve(__dirname, './assets/js/mine-seeker/hooks'),
|
||||||
'@mine-utils': resolve(__dirname, './assets/js/mine-seeker/utils'),
|
'@mine-utils': resolve(__dirname, './assets/js/mine-seeker/utils'),
|
||||||
|
'@global-utils': resolve(__dirname, './assets/js/utils'),
|
||||||
|
'@global-components': resolve(__dirname, './assets/js/components'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user