chg: dev: massive refactor on front-end for unification and readiness #8
This commit is contained in:
@@ -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 (
|
||||
<div
|
||||
className={`profile-avatar${loading ? ' profile-avatar--loading' : ''}`}
|
||||
className={`profile-avatar${isPending ? ' profile-avatar--loading' : ''}`}
|
||||
title="Click to change profile picture"
|
||||
onClick={handleClick}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
{thumbUrl
|
||||
? <img src={thumbUrl} alt={initials} className="profile-avatar__img" />
|
||||
@@ -65,7 +62,13 @@ export default function AvatarUpload({ uploadUrl, initialThumbUrl, initials }) {
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{error && <div className="profile-avatar__error">{error}</div>}
|
||||
{errorMessage && <div className="profile-avatar__error">{errorMessage}</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 { 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 <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;
|
||||
@@ -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 (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<Dialog open={open} onClose={() => setOpen(false)} sx={DIALOG_SX}>
|
||||
<StyledDialog open={open} onClose={() => setOpen(false)}>
|
||||
<div className="bd">
|
||||
<div className="bd-header">
|
||||
<div className="bd-header-left">
|
||||
@@ -122,7 +97,7 @@ export default function BattleDialog({ games }) {
|
||||
<i className="fa fa-crosshairs" /> Match Details
|
||||
</h2>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<div className="bd-header-actions">
|
||||
{canContinue ? (
|
||||
<a
|
||||
className="bd-continue"
|
||||
@@ -160,33 +135,19 @@ export default function BattleDialog({ games }) {
|
||||
<span className="bd-vs-score__sep">:</span>
|
||||
<span className="bd-vs-score__blue">{game.bluePoints ?? '—'}</span>
|
||||
</div>
|
||||
<div className="bd-vs-score" style={{ marginBottom: 8 }}>
|
||||
<span style={{
|
||||
font: '700 13px \'Rajdhani\', sans-serif',
|
||||
color: '#f67d52',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-star" style={{ fontSize: 11 }} /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
||||
<div className="bd-vs-score bd-bonus-score">
|
||||
<span className="bd-bonus-score__red">
|
||||
<i className="fa fa-star" /> {(game.redBonusPoints ?? 0).toFixed(1)}
|
||||
</span>
|
||||
<span className="bd-vs-score__sep">:</span>
|
||||
<span style={{
|
||||
font: '700 13px \'Rajdhani\', sans-serif',
|
||||
color: '#95cff5',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" style={{ fontSize: 11 }} />
|
||||
<span className="bd-bonus-score__blue">
|
||||
{(game.blueBonusPoints ?? 0).toFixed(1)} <i className="fa fa-star" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="bd-vs-label">VS</div>
|
||||
<div
|
||||
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}
|
||||
</div>
|
||||
@@ -226,7 +187,33 @@ export default function BattleDialog({ games }) {
|
||||
game={game}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</StyledDialog>
|
||||
</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 { 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;
|
||||
|
||||
@@ -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;
|
||||
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 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 }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={addModalOpen} onClose={closeAddModal} sx={DIALOG_SX}>
|
||||
<StyledDialog open={addModalOpen} onClose={closeAddModal}>
|
||||
<DialogTitle>Add New Passkey</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
@@ -344,9 +346,9 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
||||
Continue
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</StyledDialog>
|
||||
|
||||
<Dialog open={renameModalOpen} onClose={closeRenameModal} sx={DIALOG_SX}>
|
||||
<StyledDialog open={renameModalOpen} onClose={closeRenameModal}>
|
||||
<DialogTitle>Rename Passkey</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
@@ -374,9 +376,9 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
||||
Rename
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</StyledDialog>
|
||||
|
||||
<Dialog open={deleteModalOpen} onClose={closeDeleteModal} sx={DIALOG_SX}>
|
||||
<StyledDialog open={deleteModalOpen} onClose={closeDeleteModal}>
|
||||
<DialogTitle>Delete Passkey</DialogTitle>
|
||||
<DialogContent>
|
||||
<p>
|
||||
@@ -402,9 +404,25 @@ const PasskeyManager = ({ credentials, apiRoutes }) => {
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</StyledDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 { 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 }) {
|
||||
</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.
|
||||
*/
|
||||
|
||||
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 (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, position: 'relative' }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{
|
||||
width: 72, height: 72, borderRadius: '50%',
|
||||
background: avatarUrl ? 'transparent' : gradient,
|
||||
border: `2px solid ${border}`,
|
||||
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 className="bd-avatar-wrap" style={cssVars}>
|
||||
<div className="bd-avatar-ring-wrap">
|
||||
<div className="bd-avatar-ring">
|
||||
{avatarUrl
|
||||
? <img src={avatarUrl} alt={name} className="bd-avatar-img" />
|
||||
: initials}
|
||||
</div>
|
||||
{0 < bonusPoints && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
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 className="bd-avatar-bonus">
|
||||
<i className="fa fa-star" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span style={{
|
||||
font: '700 15px \'Rajdhani\', sans-serif',
|
||||
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>
|
||||
<span className="bd-avatar-name">{name}</span>
|
||||
<span className="bd-avatar-side">{isRed ? 'Red' : 'Blue'}</span>
|
||||
</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(
|
||||
() => 0 < game?.redBonusPoints
|
||||
|| 0 < game?.blueBonusPoints
|
||||
@@ -45,95 +55,68 @@ export default function BonusPoints({ game }) {
|
||||
],
|
||||
);
|
||||
|
||||
if (!hasBonuspoints) {
|
||||
return '';
|
||||
}
|
||||
if (!hasBonuspoints) return '';
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '16px 20px 0',
|
||||
borderTop: '1px solid rgba(255,255,255,0.08)',
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<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
|
||||
<div className="bd-bonus">
|
||||
<div className="bd-bonus__grid">
|
||||
<div className="bd-bonus__column bd-bonus__column--red">
|
||||
<span className="bd-bonus__heading">
|
||||
<i className="fa fa-star" /> Red Bonus Statistics
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<StatRow
|
||||
icon="fa-star" label="Total Bonus Points" value={(game.redBonusPoints ?? 0).toFixed(1)}
|
||||
valueColor="#ffd700"
|
||||
/>
|
||||
{0 < game.redBonusStats?.blindHits
|
||||
&& <StatRow icon="fa-bullseye" label="Blind hits" value={game.redBonusStats.blindHits} />}
|
||||
{0 < game.redBonusStats?.chainBest
|
||||
&& <StatRow icon="fa-link" label="Best chain" value={game.redBonusStats.chainBest} />}
|
||||
{0 < game.redBonusStats?.edgeMines
|
||||
&& <StatRow icon="fa-border-all" label="Edge mines" value={game.redBonusStats.edgeMines} />}
|
||||
{0 < 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} />}
|
||||
{hasRedNoBonuses
|
||||
&& <StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />}
|
||||
<div className="bd-bonus__rows">
|
||||
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.redBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||
{0 < game.redBonusStats?.blindHits && (
|
||||
<StatRow icon="fa-bullseye" label="Blind hits" value={game.redBonusStats.blindHits} />
|
||||
)}
|
||||
{0 < game.redBonusStats?.chainBest && (
|
||||
<StatRow icon="fa-link" label="Best chain" value={game.redBonusStats.chainBest} />
|
||||
)}
|
||||
{0 < game.redBonusStats?.edgeMines && (
|
||||
<StatRow icon="fa-border-all" label="Edge mines" value={game.redBonusStats.edgeMines} />
|
||||
)}
|
||||
{0 < 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} />
|
||||
)}
|
||||
{hasRedNoBonuses && (
|
||||
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: 16,
|
||||
border: '1px solid rgba(149,207,245,0.2)',
|
||||
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
|
||||
<div className="bd-bonus__column bd-bonus__column--blue">
|
||||
<span className="bd-bonus__heading">
|
||||
<i className="fa fa-star" /> Blue Bonus Statistics
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<StatRow
|
||||
icon="fa-star" label="Total Bonus Points" value={(game.blueBonusPoints ?? 0).toFixed(1)}
|
||||
valueColor="#ffd700"
|
||||
/>
|
||||
{0 < game.blueBonusStats?.blindHits
|
||||
&& <StatRow icon="fa-bullseye" label="Blind hits" value={game.blueBonusStats.blindHits} />}
|
||||
{0 < game.blueBonusStats?.chainBest
|
||||
&& <StatRow icon="fa-link" label="Best chain" value={game.blueBonusStats.chainBest} />}
|
||||
{0 < game.blueBonusStats?.edgeMines
|
||||
&& <StatRow icon="fa-border-all" label="Edge mines" value={game.blueBonusStats.edgeMines} />}
|
||||
{0 < 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} />}
|
||||
{hasBlueNoBonuses
|
||||
&& <StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />}
|
||||
<div className="bd-bonus__rows">
|
||||
<StatRow icon="fa-star" label="Total Bonus Points" value={(game.blueBonusPoints ?? 0).toFixed(1)} valueColor="#ffd700" />
|
||||
{0 < game.blueBonusStats?.blindHits && (
|
||||
<StatRow icon="fa-bullseye" label="Blind hits" value={game.blueBonusStats.blindHits} />
|
||||
)}
|
||||
{0 < game.blueBonusStats?.chainBest && (
|
||||
<StatRow icon="fa-link" label="Best chain" value={game.blueBonusStats.chainBest} />
|
||||
)}
|
||||
{0 < game.blueBonusStats?.edgeMines && (
|
||||
<StatRow icon="fa-border-all" label="Edge mines" value={game.blueBonusStats.edgeMines} />
|
||||
)}
|
||||
{0 < 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} />
|
||||
)}
|
||||
{hasBlueNoBonuses && (
|
||||
<StatRow icon="fa-minus-circle" label="Status" value="No bonuses" valueColor="rgba(255,255,255,0.3)" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
BonusPoints.propTypes = {
|
||||
game: object.isRequired,
|
||||
};
|
||||
|
||||
@@ -8,33 +8,24 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { node, string } from 'prop-types';
|
||||
|
||||
export default function StatRow({ icon, label, value, valueColor }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center',
|
||||
gap: 10, padding: '9px 0',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
}}
|
||||
export const StatRow = ({ icon, label, value, valueColor }) => (
|
||||
<div className="bd-stat-row">
|
||||
<i className={`fa ${icon} bd-stat-row__icon`} />
|
||||
<span className="bd-stat-row__label">{label}</span>
|
||||
<span
|
||||
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 }} />
|
||||
<span style={{
|
||||
font: '500 13px \'Rajdhani\', sans-serif',
|
||||
color: 'rgba(255,255,255,0.45)',
|
||||
flex: 1,
|
||||
letterSpacing: 0.5,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span style={{
|
||||
font: '700 13px \'Rajdhani\', sans-serif',
|
||||
color: valueColor || 'rgba(255,255,255,0.75)',
|
||||
letterSpacing: 0.5,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
StatRow.propTypes = {
|
||||
icon: string.isRequired,
|
||||
label: string.isRequired,
|
||||
value: node.isRequired,
|
||||
valueColor: string,
|
||||
};
|
||||
|
||||
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';
|
||||
Reference in New Issue
Block a user