diff --git a/assets/css/homepage/_animations.scss b/assets/css/homepage/_animations.scss index 7f42902..195bcbe 100644 --- a/assets/css/homepage/_animations.scss +++ b/assets/css/homepage/_animations.scss @@ -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 { from { opacity: 0; transform: scale(0.94); } to { opacity: 1; transform: scale(1); } diff --git a/assets/css/homepage/_auth-bar.scss b/assets/css/homepage/_auth-bar.scss index 1217c93..50b4da3 100644 --- a/assets/css/homepage/_auth-bar.scss +++ b/assets/css/homepage/_auth-bar.scss @@ -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 { padding: 20px; diff --git a/assets/css/homepage/_auth.scss b/assets/css/homepage/_auth.scss index 4e3f90a..a895dd9 100644 --- a/assets/css/homepage/_auth.scss +++ b/assets/css/homepage/_auth.scss @@ -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 { display: flex; flex-direction: column; diff --git a/assets/css/homepage/_battle-dialog.scss b/assets/css/homepage/_battle-dialog.scss new file mode 100644 index 0000000..929c66f --- /dev/null +++ b/assets/css/homepage/_battle-dialog.scss @@ -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); +} diff --git a/assets/css/homepage/_content.scss b/assets/css/homepage/_content.scss index d5b82d6..d01ebfe 100644 --- a/assets/css/homepage/_content.scss +++ b/assets/css/homepage/_content.scss @@ -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 { color: rgba(255, 255, 255, 0.85); max-width: 900px; diff --git a/assets/css/homepage/_cta.scss b/assets/css/homepage/_cta.scss index f2d0d77..3736da7 100644 --- a/assets/css/homepage/_cta.scss +++ b/assets/css/homepage/_cta.scss @@ -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 { position: relative; display: inline-block; diff --git a/assets/css/homepage/_features.scss b/assets/css/homepage/_features.scss index d74278f..1bb9c85 100644 --- a/assets/css/homepage/_features.scss +++ b/assets/css/homepage/_features.scss @@ -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 { width: 100%; padding: 80px 40px; diff --git a/assets/css/homepage/_footer.scss b/assets/css/homepage/_footer.scss index 5f23bc1..a414d16 100644 --- a/assets/css/homepage/_footer.scss +++ b/assets/css/homepage/_footer.scss @@ -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 { background: #040608; border-top: 1px solid rgba(35, 111, 135, 0.12); diff --git a/assets/css/homepage/_header.scss b/assets/css/homepage/_header.scss index a4226bd..d91cfe5 100644 --- a/assets/css/homepage/_header.scss +++ b/assets/css/homepage/_header.scss @@ -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 { position: relative; width: 100%; diff --git a/assets/css/homepage/_hero-compact.scss b/assets/css/homepage/_hero-compact.scss index ea71fd7..b08ff55 100644 --- a/assets/css/homepage/_hero-compact.scss +++ b/assets/css/homepage/_hero-compact.scss @@ -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 { min-height: unset; padding: 36px 60px 48px; diff --git a/assets/css/homepage/_hero.scss b/assets/css/homepage/_hero.scss index 942e6b8..29ea81e 100644 --- a/assets/css/homepage/_hero.scss +++ b/assets/css/homepage/_hero.scss @@ -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 { position: relative; z-index: 2; diff --git a/assets/css/homepage/_reset.scss b/assets/css/homepage/_reset.scss index 9c43d4e..72cce14 100644 --- a/assets/css/homepage/_reset.scss +++ b/assets/css/homepage/_reset.scss @@ -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; padding: 0; diff --git a/assets/css/homepage/_responsive.scss b/assets/css/homepage/_responsive.scss index d32b5a7..4fe5ddf 100644 --- a/assets/css/homepage/_responsive.scss +++ b/assets/css/homepage/_responsive.scss @@ -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) { .hero h1 { font-size: 44px; diff --git a/assets/css/homepage/_tech.scss b/assets/css/homepage/_tech.scss index fdd23ed..cf99d2a 100644 --- a/assets/css/homepage/_tech.scss +++ b/assets/css/homepage/_tech.scss @@ -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 { background: #07090d; } diff --git a/assets/css/mineseeker/_back-button.scss b/assets/css/mineseeker/_back-button.scss index 6a213c2..2981122 100644 --- a/assets/css/mineseeker/_back-button.scss +++ b/assets/css/mineseeker/_back-button.scss @@ -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 { display: inline-block; position: fixed; diff --git a/assets/css/mineseeker/_base.scss b/assets/css/mineseeker/_base.scss index d8a12ad..40c8a30 100644 --- a/assets/css/mineseeker/_base.scss +++ b/assets/css/mineseeker/_base.scss @@ -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 { height: 100%; padding: 0; diff --git a/assets/css/mineseeker/_bomb.scss b/assets/css/mineseeker/_bomb.scss index 86c0329..04906af 100644 --- a/assets/css/mineseeker/_bomb.scss +++ b/assets/css/mineseeker/_bomb.scss @@ -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 { 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%); diff --git a/assets/css/mineseeker/_grid.scss b/assets/css/mineseeker/_grid.scss index 07bb4a3..5da85cf 100644 --- a/assets/css/mineseeker/_grid.scss +++ b/assets/css/mineseeker/_grid.scss @@ -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 { display: flex; flex-direction: row; diff --git a/assets/css/mineseeker/_mine-counter.scss b/assets/css/mineseeker/_mine-counter.scss index d0368cf..fa387e9 100644 --- a/assets/css/mineseeker/_mine-counter.scss +++ b/assets/css/mineseeker/_mine-counter.scss @@ -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 { 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%); diff --git a/assets/css/mineseeker/_overlay.scss b/assets/css/mineseeker/_overlay.scss index a6dfa9a..bd6b435 100644 --- a/assets/css/mineseeker/_overlay.scss +++ b/assets/css/mineseeker/_overlay.scss @@ -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 { background: rgba(255, 255, 255, 0.2); display: flex; diff --git a/assets/css/mineseeker/_responsive.scss b/assets/css/mineseeker/_responsive.scss index 17b72a5..55b5d48 100644 --- a/assets/css/mineseeker/_responsive.scss +++ b/assets/css/mineseeker/_responsive.scss @@ -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) { #mine-wrapper .game-wrapper .users { visibility: hidden; diff --git a/assets/css/mineseeker/_timer.scss b/assets/css/mineseeker/_timer.scss index 9fac241..e68ab8a 100644 --- a/assets/css/mineseeker/_timer.scss +++ b/assets/css/mineseeker/_timer.scss @@ -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 { display: flex; gap: 12px; diff --git a/assets/css/mineseeker/_users.scss b/assets/css/mineseeker/_users.scss index 98db70a..e1cb153 100644 --- a/assets/css/mineseeker/_users.scss +++ b/assets/css/mineseeker/_users.scss @@ -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 { width: 180px; padding: 0 10px 0 0; diff --git a/assets/css/mineseeker/_waiting-dialog.scss b/assets/css/mineseeker/_waiting-dialog.scss index 3f6d868..a0c6b3a 100644 --- a/assets/css/mineseeker/_waiting-dialog.scss +++ b/assets/css/mineseeker/_waiting-dialog.scss @@ -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 { background: #07090d !important; background-image: linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px), diff --git a/assets/css/passkey.scss b/assets/css/passkey.scss index ea29656..610e6aa 100644 --- a/assets/css/passkey.scss +++ b/assets/css/passkey.scss @@ -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"; .twofa-status { diff --git a/assets/css/style.homepage.scss b/assets/css/style.homepage.scss index b607650..400d10c 100644 --- a/assets/css/style.homepage.scss +++ b/assets/css/style.homepage.scss @@ -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/animations'; @use 'homepage/header'; @@ -12,4 +21,5 @@ @use 'homepage/tech'; @use 'homepage/footer'; @use 'homepage/profile'; +@use 'homepage/battle-dialog'; @use 'homepage/responsive'; diff --git a/assets/css/style.layout.scss b/assets/css/style.layout.scss index e038be3..cc6980f 100644 --- a/assets/css/style.layout.scss +++ b/assets/css/style.layout.scss @@ -1,7 +1,7 @@ /*!* * 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 * file that was distributed with this source code. diff --git a/assets/css/style.mineseeker.scss b/assets/css/style.mineseeker.scss index 34d835b..8b38048 100644 --- a/assets/css/style.mineseeker.scss +++ b/assets/css/style.mineseeker.scss @@ -1,7 +1,7 @@ /*!* * 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 * file that was distributed with this source code. diff --git a/assets/css/style.scss b/assets/css/style.scss index 9542f17..b0ed6e7 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -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 { position: fixed; top: 0; diff --git a/assets/js/components/AvatarUpload.jsx b/assets/js/components/AvatarUpload.jsx index bcec227..ce1fa8a 100644 --- a/assets/js/components/AvatarUpload.jsx +++ b/assets/js/components/AvatarUpload.jsx @@ -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 }) { - const [thumbUrl, setThumbUrl] = useState(initialThumbUrl || null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); +import React, { useMemo, useRef } from 'react'; +import { string } from 'prop-types'; +import { useProfileDataProvider } from '@mine-hooks/useGameDataProvider'; + +export const AvatarUpload = ({ uploadUrl, initialThumbUrl, initials }) => { const inputRef = useRef(null); + const [thumbUrl, setThumbUrl] = React.useState(initialThumbUrl || null); + const { uploadAvatarMutation: { isPending, error, mutate } } = useProfileDataProvider(); - function handleClick() { - inputRef.current?.click(); - } - - function handleChange(e) { + const handleChange = e => { const file = e.target.files?.[0]; if (!file) return; - const fd = new FormData(); - fd.append('avatar', file); - - setLoading(true); - setError(null); - - fetch(uploadUrl, { method: 'POST', body: fd }) - .then(r => r.json()) - .then(data => { - if (data.error) { - setError(data.error); - return; - } + mutate({ uploadUrl, file }, { + onSuccess: data => { setThumbUrl(data.thumbUrl); const navImg = document.querySelector('.hero-auth-avatar:not(.hero-auth-avatar--initials)'); const navInitials = document.querySelector('.hero-auth-avatar.hero-auth-avatar--initials'); + if (navImg) { navImg.src = data.thumbUrl; } else if (navInitials) { @@ -40,16 +36,17 @@ export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) { img.className = 'hero-auth-avatar'; navInitials.replaceWith(img); } - }) - .catch(() => setError('Upload failed. Please try again.')) - .finally(() => setLoading(false)); - } + }, + }); + }; + + const errorMessage = useMemo(() => error?.message ?? null, [error]); return (
inputRef.current?.click()} > {thumbUrl ? {initials} @@ -65,7 +62,13 @@ export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) { style={{ display: 'none' }} onChange={handleChange} /> - {error &&
{error}
} + {errorMessage &&
{errorMessage}
}
); } + +AvatarUpload.propTypes = { + uploadUrl: string.isRequired, + initialThumbUrl: string, + initials: string.isRequired, +}; diff --git a/assets/js/components/BattleDialog.jsx b/assets/js/components/BattleDialog.jsx index f14ee7f..3785839 100644 --- a/assets/js/components/BattleDialog.jsx +++ b/assets/js/components/BattleDialog.jsx @@ -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 { array } from 'prop-types'; +import { formatDuration } from '@global-utils/format'; import Dialog from '@mui/material/Dialog'; -import { createTheme, ThemeProvider } from '@mui/material/styles'; -import Avatar from './battle-dialog/Avatar'; -import StatRow from './battle-dialog/StatRow'; -import BonusPoints from './battle-dialog/BonusPoints'; +import { createTheme, styled, ThemeProvider } from '@mui/material/styles'; +import { Avatar, BonusPoints, StatRow } from '@global-components'; 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 = { win: { label: 'Victory', @@ -53,7 +40,7 @@ const RESULT_META = { }, }; -export default function BattleDialog({ games }) { +export const BattleDialog = ({ games }) => { const [open, setOpen] = useState(false); const [game, setGame] = useState(null); const [copied, setCopied] = useState(false); @@ -73,7 +60,7 @@ export default function BattleDialog({ games }) { }, [games]); if (!game) { - return ; + return ; } const meta = RESULT_META[game.result] ?? RESULT_META.draw; @@ -86,18 +73,6 @@ export default function BattleDialog({ games }) { const canContinue = !resign && 26 > maxPoints; 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 pointDiff = Math.abs((game.redPoints ?? 0) - (game.bluePoints ?? 0)); const winnerColor = (game.redPoints ?? 0) > (game.bluePoints ?? 0) ? '#f67d52' @@ -113,7 +88,7 @@ export default function BattleDialog({ games }) { return ( - setOpen(false)} sx={DIALOG_SX}> + setOpen(false)}>
@@ -122,7 +97,7 @@ export default function BattleDialog({ games }) { Match Details
-
+
); -} +}; + +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)', + }, +}); diff --git a/assets/js/components/ContactForm.jsx b/assets/js/components/ContactForm.jsx index a07040e..1e3f5ef 100644 --- a/assets/js/components/ContactForm.jsx +++ b/assets/js/components/ContactForm.jsx @@ -8,6 +8,7 @@ */ import { useEffect, useRef } from 'react'; +import { string } from 'prop-types'; /** * ContactForm Component @@ -80,4 +81,9 @@ const ContactForm = ({ siteKey, recaptchaFieldId }) => { return null; }; +ContactForm.propTypes = { + siteKey: string.isRequired, + recaptchaFieldId: string.isRequired, +}; + export default ContactForm; diff --git a/assets/js/components/PasskeyLogin.jsx b/assets/js/components/PasskeyLogin.jsx index 81725cd..616280c 100644 --- a/assets/js/components/PasskeyLogin.jsx +++ b/assets/js/components/PasskeyLogin.jsx @@ -8,6 +8,7 @@ */ import React, { useState, useCallback } from 'react'; +import { shape, string } from 'prop-types'; const base64ToArrayBuffer = base64 => { const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/'))); @@ -107,4 +108,11 @@ const PasskeyLogin = ({ apiRoutes }) => { ); }; -export default PasskeyLogin; \ No newline at end of file +export default PasskeyLogin; + +PasskeyLogin.propTypes = { + apiRoutes: shape({ + authenticationBegin: string.isRequired, + authenticationComplete: string.isRequired, + }).isRequired, +}; diff --git a/assets/js/components/PasskeyManager.jsx b/assets/js/components/PasskeyManager.jsx index b460ae1..781118c 100644 --- a/assets/js/components/PasskeyManager.jsx +++ b/assets/js/components/PasskeyManager.jsx @@ -9,13 +9,15 @@ import React, { Fragment, useCallback, useEffect, useState } from 'react'; import Dialog from '@mui/material/Dialog'; +import { styled } from '@mui/material/styles'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; import Button from '@mui/material/Button'; import TextField from '@mui/material/TextField'; +import { arrayOf, shape, string, bool } from 'prop-types'; -const DIALOG_SX = { +const StyledDialog = styled(Dialog)({ '& .MuiDialog-paper': { background: '#0a0e14', color: '#e0e0e0', @@ -47,7 +49,7 @@ const DIALOG_SX = { background: 'rgba(2, 4, 8, 0.88)', backdropFilter: 'blur(4px)', }, -}; +}); const base64ToArrayBuffer = base64 => { const binary = atob(base64.replace(/([-_])/g, m => ('-' === m ? '+' : '/'))); @@ -314,7 +316,7 @@ const PasskeyManager = ({ credentials, apiRoutes }) => { )} - + Add New Passkey { Continue - + - + Rename Passkey { Rename - + - + Delete Passkey

@@ -402,9 +404,25 @@ const PasskeyManager = ({ credentials, apiRoutes }) => { Delete -

+ ); }; 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, +}; diff --git a/assets/js/components/ProfileCharts.jsx b/assets/js/components/ProfileCharts.jsx index 3e6e2be..ffe9df4 100644 --- a/assets/js/components/ProfileCharts.jsx +++ b/assets/js/components/ProfileCharts.jsx @@ -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 { BarChart } from '@mui/x-charts/BarChart'; import { LineChart } from '@mui/x-charts/LineChart'; import { PieChart } from '@mui/x-charts/PieChart'; import { createTheme, ThemeProvider } from '@mui/material/styles'; +import { shape, arrayOf, number, string } from 'prop-types'; const darkTheme = createTheme({ palette: { @@ -136,3 +146,20 @@ export default function ProfileCharts({ chartData }) { ); } + +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, +}; diff --git a/assets/js/components/battle-dialog/Avatar.jsx b/assets/js/components/battle-dialog/Avatar.jsx index 9f62eeb..f8e521c 100644 --- a/assets/js/components/battle-dialog/Avatar.jsx +++ b/assets/js/components/battle-dialog/Avatar.jsx @@ -7,92 +7,48 @@ * 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 initials = (name || '?').slice(0, 2).toUpperCase(); + const initials = useMemo(() => (name || '?').slice(0, 2).toUpperCase(), [name]); - const gradient = isRed - ? '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%)'; - const glow = isRed - ? '0 0 0 3px rgba(173,10,5,0.2), 0 0 28px rgba(173,10,5,0.35)' - : '0 0 0 3px rgba(35,111,135,0.2), 0 0 28px rgba(35,111,135,0.35)'; - const border = isRed - ? 'rgba(173,10,5,0.5)' - : 'rgba(35,111,135,0.5)'; - const textColor = isRed ? '#f67d52' : '#95cff5'; + const cssVars = isRed ? { + '--bd-avatar-gradient': 'linear-gradient(135deg, rgba(173,10,5,0.6) 0%, rgba(246,125,82,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)', + '--bd-avatar-border': 'rgba(173,10,5,0.5)', + '--bd-avatar-color': '#f67d52', + } : { + '--bd-avatar-gradient': '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(35,111,135,0.2), 0 0 28px rgba(35,111,135,0.35)', + '--bd-avatar-border': 'rgba(35,111,135,0.5)', + '--bd-avatar-color': '#95cff5', + }; return ( -
-
-
- {avatarUrl ? ( - {name} - ) : ( - initials - )} +
+
+
+ {avatarUrl + ? {name} + : initials}
{0 < bonusPoints && ( -
- +
+
)}
- - {name} - - - {isRed ? 'Red' : 'Blue'} - + {name} + {isRed ? 'Red' : 'Blue'}
); -} +}; + +Avatar.propTypes = { + name: string, + color: string, + avatarUrl: string, + bonusPoints: string, +}; diff --git a/assets/js/components/battle-dialog/BonusPoints.jsx b/assets/js/components/battle-dialog/BonusPoints.jsx index 5b2d162..0d986a6 100644 --- a/assets/js/components/battle-dialog/BonusPoints.jsx +++ b/assets/js/components/battle-dialog/BonusPoints.jsx @@ -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( () => 0 < game?.redBonusPoints || 0 < game?.blueBonusPoints @@ -45,95 +55,68 @@ export default function BonusPoints({ game }) { ], ); - if (!hasBonuspoints) { - return ''; - } + if (!hasBonuspoints) return ''; return ( -
-
- {/* Red Bonus */} -
- - Red Bonus Statistics +
+
+
+ + Red Bonus Statistics -
- - {0 < game.redBonusStats?.blindHits - && } - {0 < game.redBonusStats?.chainBest - && } - {0 < game.redBonusStats?.edgeMines - && } - {0 < game.redBonusStats?.lastMineHits - && } - {0 < game.redBonusStats?.biggestReveal - && } - {hasRedNoBonuses - && } +
+ + {0 < game.redBonusStats?.blindHits && ( + + )} + {0 < game.redBonusStats?.chainBest && ( + + )} + {0 < game.redBonusStats?.edgeMines && ( + + )} + {0 < game.redBonusStats?.lastMineHits && ( + + )} + {0 < game.redBonusStats?.biggestReveal && ( + + )} + {hasRedNoBonuses && ( + + )}
-
- - Blue Bonus Statistics +
+ + Blue Bonus Statistics -
- - {0 < game.blueBonusStats?.blindHits - && } - {0 < game.blueBonusStats?.chainBest - && } - {0 < game.blueBonusStats?.edgeMines - && } - {0 < game.blueBonusStats?.lastMineHits - && } - {0 < game.blueBonusStats?.biggestReveal - && } - {hasBlueNoBonuses - && } +
+ + {0 < game.blueBonusStats?.blindHits && ( + + )} + {0 < game.blueBonusStats?.chainBest && ( + + )} + {0 < game.blueBonusStats?.edgeMines && ( + + )} + {0 < game.blueBonusStats?.lastMineHits && ( + + )} + {0 < game.blueBonusStats?.biggestReveal && ( + + )} + {hasBlueNoBonuses && ( + + )}
); -} +}; + +BonusPoints.propTypes = { + game: object.isRequired, +}; diff --git a/assets/js/components/battle-dialog/StatRow.jsx b/assets/js/components/battle-dialog/StatRow.jsx index 5aba0f4..2c64b4d 100644 --- a/assets/js/components/battle-dialog/StatRow.jsx +++ b/assets/js/components/battle-dialog/StatRow.jsx @@ -8,33 +8,24 @@ */ import React from 'react'; +import { node, string } from 'prop-types'; -export default function StatRow({ icon, label, value, valueColor }) { - return ( -
( +
+ + {label} + - - - {label} - - - {value} - -
- ); -} + {value} + +
+); + +StatRow.propTypes = { + icon: string.isRequired, + label: string.isRequired, + value: node.isRequired, + valueColor: string, +}; diff --git a/assets/js/components/index.js b/assets/js/components/index.js new file mode 100644 index 0000000..be912b1 --- /dev/null +++ b/assets/js/components/index.js @@ -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'; diff --git a/assets/js/contact.jsx b/assets/js/contact.jsx index d36d074..e864dcd 100644 --- a/assets/js/contact.jsx +++ b/assets/js/contact.jsx @@ -9,7 +9,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import ContactForm from './components/ContactForm'; +import { ContactForm } from '@global-components'; const wrapper = document.getElementById('contact-form-wrapper'); @@ -28,4 +28,3 @@ if (wrapper) { console.error('ContactForm: Missing siteKey or recaptchaFieldId in data attributes'); } } - diff --git a/assets/js/mine-seeker/MineSeeker.jsx b/assets/js/mine-seeker/MineSeeker.jsx index 7401ce7..e32b89d 100644 --- a/assets/js/mine-seeker/MineSeeker.jsx +++ b/assets/js/mine-seeker/MineSeeker.jsx @@ -11,6 +11,7 @@ import React, { useRef } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { GameProvider } from '@mine-contexts'; import { GameBoard } from '@mine-components'; +import { string } from 'prop-types'; const queryClient = new QueryClient(); @@ -34,3 +35,9 @@ const MineSeeker = ({ env, gameId, opponentName = '' }) => { }; export default MineSeeker; + +MineSeeker.propTypes = { + env: string.isRequired, + gameId: string, + opponentName: string, +}; diff --git a/assets/js/mine-seeker/components/BonusBox.jsx b/assets/js/mine-seeker/components/BonusBox.jsx index 1683fd6..700b197 100644 --- a/assets/js/mine-seeker/components/BonusBox.jsx +++ b/assets/js/mine-seeker/components/BonusBox.jsx @@ -8,6 +8,7 @@ */ import React from 'react'; +import { func, number, string } from 'prop-types'; const BonusBox = ({ color, points, onClick, title }) => (