Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions src/main/frontend/components/cards/band-activity/ActivityBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* ActivityBar - Progress bar showing spot count.
*
* Single Responsibility: Displays activity level as a visual progress bar.
*/

import React, { useMemo } from 'react';
import './BandModeActivityContent.css';

/** Maximum spots for full bar (100%) */
const MAX_SPOTS_FOR_FULL_BAR = 100;

interface Props {
/** Number of spots in current window */
spotCount: number;

/** Maximum spots for scaling (defaults to 100) */
maxSpots?: number;

/** Duration of the window in minutes */
windowMinutes?: number;
}

/**
* Renders a progress bar showing activity level.
*/
export function ActivityBar({ spotCount, maxSpots = MAX_SPOTS_FOR_FULL_BAR, windowMinutes = 15 }: Props) {
// Calculate percentage (capped at 100%)
const percentage = Math.min(100, (spotCount / maxSpots) * 100);

// Memoize style object to satisfy react-perf/jsx-no-new-object-as-prop
const fillStyle = useMemo(() => ({ width: `${percentage}%` }), [percentage]);

// Format spot count for display
const formattedCount = spotCount >= 1000 ? `${(spotCount / 1000).toFixed(1)}k` : spotCount.toString();

return (
<div className="activity-bar-container">
<div className="activity-bar-label">
<span className="activity-bar-title">Activity</span>
<span className="activity-bar-value">
{formattedCount} spots ({windowMinutes} min)
</span>
</div>
<div
className="activity-bar"
role="progressbar"
aria-valuenow={percentage}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`Activity level: ${Math.round(percentage)}%`}
>
<div className="activity-bar-fill" style={fillStyle} />
</div>
</div>
);
}

export default ActivityBar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
/**
* Styles for Band Mode Activity Card components.
*
* Uses CSS variables from global.css for consistent theming.
*/

/* ============================================
Activity Bar
============================================ */

.activity-bar-container {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 4px;
}

.activity-bar-label {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 0.75rem;
}

.activity-bar-title {
color: var(--color-text-secondary);
font-weight: 500;
}

.activity-bar-value {
color: var(--color-text-primary);
font-weight: 600;
font-variant-numeric: tabular-nums;
}

.activity-bar {
height: 8px;
background: var(--color-surface-secondary);
border-radius: 4px;
overflow: hidden;
}

.activity-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--hotness-warm-color), var(--hotness-hot-color));
border-radius: 4px;
transition: width 0.3s ease-out;
}

/* ============================================
Trend Indicator
============================================ */

.trend-indicator {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
margin-bottom: 4px;
}

.trend-label {
color: var(--color-text-secondary);
font-weight: 500;
}

.trend-value {
display: flex;
align-items: center;
gap: 4px;
font-weight: 600;
}

.trend-icon {
flex-shrink: 0;
}

.trend-icon--up {
color: var(--hotness-hot-color);
}

.trend-icon--down {
color: var(--color-error);
}

.trend-icon--flat {
color: var(--color-text-secondary);
}

.trend-positive .trend-percentage {
color: var(--hotness-hot-color);
}

.trend-negative .trend-percentage {
color: var(--color-error);
}

.trend-flat .trend-percentage {
color: var(--color-text-secondary);
}

/* ============================================
DX Reach Display
============================================ */

.dx-reach-container {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
margin-bottom: 4px;
flex-wrap: wrap;
}

.dx-reach-icon {
color: var(--color-text-secondary);
flex-shrink: 0;
}

.dx-reach-label {
color: var(--color-text-secondary);
font-weight: 500;
}

.dx-reach-value {
color: var(--color-text-primary);
font-weight: 600;
font-variant-numeric: tabular-nums;
}

.dx-reach-path {
color: var(--color-text-secondary);
font-weight: 400;
margin-left: 4px;
}

.dx-reach--empty .dx-reach-value {
color: var(--color-text-secondary);
font-style: italic;
font-weight: 400;
}

/* ============================================
Path Status Grid
============================================ */

.path-grid-container {
margin-bottom: 4px;
}

.path-grid-label {
display: block;
color: var(--color-text-secondary);
font-size: 0.75rem;
font-weight: 500;
margin-bottom: 2px;
}

.path-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}

.path-item {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
padding: 3px 4px;
border-radius: 4px;
font-size: 0.65rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
}

.path-item--active {
background: rgba(16, 185, 129, 0.15);
color: var(--hotness-hot-color);
}

.path-item--inactive {
background: var(--color-surface-secondary);
color: var(--color-text-secondary);
}

.path-display {
letter-spacing: -0.02em;
}

.path-status-icon {
flex-shrink: 0;
}

.path-status--active {
color: var(--hotness-hot-color);
}

.path-status--inactive {
color: var(--color-text-tertiary);
opacity: 0.5;
}

/* ============================================
Condition Badge
============================================ */

.condition-badge-container {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
}

.condition-badge-label {
color: var(--color-text-secondary);
font-weight: 500;
}

.condition-badge {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
}

.condition-badge--good {
background: rgba(16, 185, 129, 0.15);
color: var(--hotness-hot-color);
}

.condition-badge--fair {
background: rgba(245, 158, 11, 0.15);
color: var(--hotness-warm-color);
}

.condition-badge--poor {
background: rgba(239, 68, 68, 0.15);
color: var(--color-error);
}

.condition-badge--unknown {
background: var(--color-surface-secondary);
color: var(--color-text-secondary);
}

/* ============================================
Placeholders
============================================ */

.placeholder-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px 8px;
text-align: center;
min-height: 80px;
}

.placeholder-icon {
color: var(--color-text-tertiary);
margin-bottom: 8px;
}

.placeholder-text {
color: var(--color-text-secondary);
font-size: 0.8rem;
font-weight: 500;
}

.placeholder-hint {
color: var(--color-text-tertiary);
font-size: 0.7rem;
margin-top: 4px;
}

.placeholder--coming-soon .placeholder-icon {
color: var(--hotness-warm-color);
opacity: 0.6;
}

.placeholder--coming-soon .placeholder-text {
color: var(--hotness-warm-color);
}

/* ============================================
BandModeActivityContent (Composed)
============================================ */

.band-mode-activity-content {
display: flex;
flex-direction: column;
}

/* Compact layout - no dividers, minimal gaps */

/* ============================================
Dark Mode Adjustments
============================================ */

@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .activity-bar {
background: rgba(255, 255, 255, 0.1);
}

:root:not([data-theme='light']) .path-item--inactive {
background: rgba(255, 255, 255, 0.05);
}
}

:root[data-theme='dark'] .activity-bar {
background: rgba(255, 255, 255, 0.1);
}

:root[data-theme='dark'] .path-item--inactive {
background: rgba(255, 255, 255, 0.05);
}

/* ============================================
Reduced Motion
============================================ */

@media (prefers-reduced-motion: reduce) {
.activity-bar-fill {
transition: none;
}
}
Loading