Redesign dashboard with steampunk neon theme and live gauges.
Ambient background, 3D neon cards, brass HUD styling, glowing charts, gauge rings, and refreshed Command Deck, Fleet Roster, Forge, and Calibrate pages.
This commit is contained in:
56
server/web/src/components/Charts/GaugeRing.css
Normal file
56
server/web/src/components/Charts/GaugeRing.css
Normal file
@@ -0,0 +1,56 @@
|
||||
.gauge-ring {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gauge-ring-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.gauge-ring-track {
|
||||
fill: none;
|
||||
stroke: rgba(201, 162, 39, 0.15);
|
||||
stroke-width: 6;
|
||||
}
|
||||
|
||||
.gauge-ring-fill {
|
||||
fill: none;
|
||||
stroke-width: 6;
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dashoffset 0.8s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.gauge-ring-center {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.gauge-ring-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--neon-cyan);
|
||||
text-shadow: 0 0 12px rgba(0, 245, 255, 0.5);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.gauge-ring-label {
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--brass-light);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.gauge-ring-sub {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
66
server/web/src/components/Charts/GaugeRing.tsx
Normal file
66
server/web/src/components/Charts/GaugeRing.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import './GaugeRing.css';
|
||||
|
||||
interface GaugeRingProps {
|
||||
value: number;
|
||||
max?: number;
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export default function GaugeRing({
|
||||
value,
|
||||
max = 100,
|
||||
label,
|
||||
sublabel,
|
||||
color = 'var(--neon-cyan)',
|
||||
size = 100,
|
||||
}: GaugeRingProps) {
|
||||
const pct = Math.min(100, Math.max(0, (value / max) * 100));
|
||||
const circumference = 2 * Math.PI * 42;
|
||||
const offset = circumference - (pct / 100) * circumference;
|
||||
|
||||
return (
|
||||
<div className="gauge-ring" style={{ width: size, height: size }}>
|
||||
<svg viewBox="0 0 100 100" className="gauge-ring-svg">
|
||||
<defs>
|
||||
<linearGradient id={`gauge-grad-${label.replace(/\s/g, '')}`} x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor={color} stopOpacity="1" />
|
||||
<stop offset="100%" stopColor="var(--brass)" stopOpacity="0.8" />
|
||||
</linearGradient>
|
||||
<filter id="gauge-glow">
|
||||
<feGaussianBlur stdDeviation="2" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<circle className="gauge-ring-track" cx="50" cy="50" r="42" />
|
||||
<circle
|
||||
className="gauge-ring-fill"
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="42"
|
||||
stroke={`url(#gauge-grad-${label.replace(/\s/g, '')})`}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
filter="url(#gauge-glow)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="gauge-ring-center">
|
||||
<span className="gauge-ring-value font-tech">{formatValue(value, max)}</span>
|
||||
<span className="gauge-ring-label">{label}</span>
|
||||
{sublabel && <span className="gauge-ring-sub">{sublabel}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatValue(v: number, max: number): string {
|
||||
if (max <= 100 && max > 1) return `${v.toFixed(0)}%`;
|
||||
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
||||
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`;
|
||||
return v.toFixed(0);
|
||||
}
|
||||
74
server/web/src/components/Charts/HashrateChart.css
Normal file
74
server/web/src/components/Charts/HashrateChart.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.neon-chart-panel {
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid rgba(201, 162, 39, 0.2);
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--brass-light);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chart-live {
|
||||
font-family: var(--font-tech);
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--neon-green);
|
||||
text-shadow: 0 0 8px rgba(57, 255, 20, 0.6);
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 260px;
|
||||
color: var(--text-muted);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.chart-empty-icon {
|
||||
font-size: 2.5rem;
|
||||
color: var(--brass);
|
||||
opacity: 0.5;
|
||||
animation: pulse-glow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.neon-tooltip {
|
||||
background: linear-gradient(145deg, rgba(20, 16, 12, 0.98), rgba(8, 6, 4, 0.99));
|
||||
border: 1px solid var(--border-brass);
|
||||
border-radius: 2px;
|
||||
padding: 0.75rem 1rem;
|
||||
box-shadow: 0 0 20px rgba(0, 245, 255, 0.15), var(--shadow-panel);
|
||||
}
|
||||
|
||||
.neon-tooltip-time {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.neon-tooltip-value {
|
||||
font-family: var(--font-tech);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.neon-tooltip-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--brass-light);
|
||||
margin-top: 0.25rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import './HashrateChart.css';
|
||||
|
||||
export interface ChartPoint {
|
||||
time: string;
|
||||
@@ -19,48 +20,149 @@ interface HashrateChartProps {
|
||||
title?: string;
|
||||
color?: string;
|
||||
unit?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export default function HashrateChart({ data, title, color = '#06b6d4', unit = 'H/s' }: HashrateChartProps) {
|
||||
const GRAD_IDS = ['cyan', 'magenta', 'amber', 'green', 'purple'] as const;
|
||||
|
||||
function colorToId(color: string): string {
|
||||
if (color.includes('f5ff') || color.includes('06b6d4') || color === '#00f5ff') return 'cyan';
|
||||
if (color.includes('2da6') || color.includes('8b5cf6')) return 'magenta';
|
||||
if (color.includes('b020') || color.includes('eab308')) return 'amber';
|
||||
if (color.includes('39ff') || color.includes('22c55e')) return 'green';
|
||||
return 'purple';
|
||||
}
|
||||
|
||||
export default function HashrateChart({
|
||||
data,
|
||||
title,
|
||||
color = '#00f5ff',
|
||||
unit = 'H/s',
|
||||
height = 280,
|
||||
}: HashrateChartProps) {
|
||||
const gradId = colorToId(color);
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<div className="chart-empty card">
|
||||
<p>{title || 'Chart'} — waiting for data...</p>
|
||||
<div className="chart-empty neon-chart-panel">
|
||||
<div className="chart-empty-icon">◈</div>
|
||||
<p className="font-tech">{title || 'Telemetry'}</p>
|
||||
<span>Awaiting signal from fleet...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chart-wrap">
|
||||
{title && <h3 className="chart-title">{title}</h3>}
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<AreaChart data={data}>
|
||||
<div className="chart-wrap neon-chart-panel">
|
||||
{title && (
|
||||
<div className="chart-header">
|
||||
<h3 className="chart-title font-display">{title}</h3>
|
||||
<span className="chart-live pulse">● LIVE</span>
|
||||
</div>
|
||||
)}
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<AreaChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id={`grad-${color}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={color} stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor={color} stopOpacity={0} />
|
||||
<linearGradient id={`area-fill-${gradId}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.55} />
|
||||
<stop offset="50%" stopColor={color} stopOpacity={0.15} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<filter id={`glow-${gradId}`}>
|
||||
<feGaussianBlur stdDeviation="3" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#243049" />
|
||||
<XAxis dataKey="time" stroke="#64748b" fontSize={11} tickLine={false} />
|
||||
<YAxis stroke="#64748b" fontSize={11} tickLine={false} tickFormatter={(v) => formatShort(v, unit)} />
|
||||
<Tooltip
|
||||
contentStyle={{ background: '#111827', border: '1px solid #2a3a5c', borderRadius: 8 }}
|
||||
labelStyle={{ color: '#94a3b8' }}
|
||||
formatter={(value: number) => [formatShort(value, unit), title || 'Value']}
|
||||
<CartesianGrid strokeDasharray="4 8" stroke="rgba(201, 162, 39, 0.12)" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="rgba(196, 181, 160, 0.5)"
|
||||
fontSize={10}
|
||||
fontFamily="Orbitron, monospace"
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: 'rgba(201, 162, 39, 0.25)' }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="rgba(196, 181, 160, 0.5)"
|
||||
fontSize={10}
|
||||
fontFamily="Orbitron, monospace"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(v) => formatShort(v, unit)}
|
||||
/>
|
||||
<Tooltip
|
||||
content={(props) => (
|
||||
<NeonTooltip
|
||||
active={props.active}
|
||||
payload={props.payload as { value: number }[] | undefined}
|
||||
label={props.label as string | undefined}
|
||||
unit={unit}
|
||||
title={title}
|
||||
color={color}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={color}
|
||||
strokeWidth={2.5}
|
||||
fill={`url(#area-fill-${gradId})`}
|
||||
filter={`url(#glow-${gradId})`}
|
||||
animationDuration={800}
|
||||
animationEasing="ease-out"
|
||||
/>
|
||||
<Area type="monotone" dataKey="value" stroke={color} fill={`url(#grad-${color})`} strokeWidth={2} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NeonTooltip({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
unit,
|
||||
title,
|
||||
color,
|
||||
}: {
|
||||
active?: boolean;
|
||||
payload?: { value: number }[];
|
||||
label?: string;
|
||||
unit: string;
|
||||
title?: string;
|
||||
color: string;
|
||||
}) {
|
||||
if (!active || !payload?.length) return null;
|
||||
const v = payload[0].value;
|
||||
return (
|
||||
<div className="neon-tooltip">
|
||||
<div className="neon-tooltip-time font-tech">{label}</div>
|
||||
<div className="neon-tooltip-value" style={{ color, textShadow: `0 0 12px ${color}` }}>
|
||||
{formatFull(v, unit)}
|
||||
</div>
|
||||
<div className="neon-tooltip-label">{title || 'Reading'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatShort(v: number, unit: string): string {
|
||||
if (unit === 'H/s') {
|
||||
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
|
||||
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`;
|
||||
return `${v.toFixed(0)}`;
|
||||
}
|
||||
return `${v.toFixed(1)}${unit === '%' ? '%' : ''}`;
|
||||
return `${v.toFixed(0)}${unit === '%' ? '%' : ''}`;
|
||||
}
|
||||
|
||||
function formatFull(v: number, unit: string): string {
|
||||
if (unit === 'H/s') {
|
||||
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(3)} MH/s`;
|
||||
if (v >= 1_000) return `${(v / 1_000).toFixed(2)} KH/s`;
|
||||
return `${v.toFixed(0)} H/s`;
|
||||
}
|
||||
return `${v.toFixed(1)}${unit}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user