feat: implement label management with sidebar, hacker theme, and Docker support

- Add label selection on chart with visual highlight (size 2x, color change)
- Implement keyboard delete handler (Delete/Backspace keys)
- Add comprehensive label management sidebar with:
  - Collapsible label annotations section
  - Search by timestamp
  - Filter by type (Break Up, Break Down, All)
  - Individual delete buttons
  - Count display
  - Click to select/highlight on chart
- Transform UI with hacker theme:
  - Matrix green (#00ff41) on dark background (#0a0e0a)
  - Monospace font (JetBrains Mono)
  - Glow effects on button hover and active states
  - Custom scrollbar styling
  - Terminal-inspired aesthetic
- Add Docker deployment:
  - Multi-stage Dockerfile with standalone output
  - docker-compose.yml with volume persistence
  - Non-root user (nextjs) for security
  - Health check endpoint integration
- Tailwind and CSS enhancements:
  - Custom colors (matrix, matrixDim, neonRed, etc.)
  - Glow box shadows and animations
  - Selection and scrollbar styling
This commit is contained in:
Marko Djordjevic 2026-02-12 15:12:59 +01:00
parent 74b84073a9
commit a1fa86fe55
14 changed files with 509 additions and 42 deletions

View file

@ -29,6 +29,8 @@ interface CandleChartProps {
activeTool: string | null;
onAnnotationChange?: () => void;
selectedColor: string;
selectedLabelId?: number | null;
onLabelSelect?: (id: number) => void;
}
export interface CandleChartHandle {
@ -36,7 +38,7 @@ export interface CandleChartHandle {
}
const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
({ activeTool, onAnnotationChange, selectedColor }, ref) => {
({ activeTool, onAnnotationChange, selectedColor, selectedLabelId, onLabelSelect }, ref) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
@ -159,17 +161,23 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
);
const markers = markerAnnotations
.map((annotation) => ({
time: annotation.timestamp as Time,
position: annotation.label_type === 'break_up' ? ('belowBar' as const) : ('aboveBar' as const),
color: annotation.label_type === 'break_up' ? '#22c55e' : '#ef4444',
shape: annotation.label_type === 'break_up' ? ('arrowUp' as const) : ('arrowDown' as const),
text: annotation.label_type === 'break_up' ? 'Break Up' : 'Break Down',
}))
.map((annotation) => {
const isSelected = annotation.id === selectedLabelId;
return {
time: annotation.timestamp as Time,
position: annotation.label_type === 'break_up' ? ('belowBar' as const) : ('aboveBar' as const),
color: isSelected
? (annotation.label_type === 'break_up' ? '#00ff41' : '#ff0040')
: (annotation.label_type === 'break_up' ? '#22c55e' : '#ef4444'),
shape: annotation.label_type === 'break_up' ? ('arrowUp' as const) : ('arrowDown' as const),
text: annotation.label_type === 'break_up' ? 'Break Up' : 'Break Down',
size: isSelected ? 2 : 1,
};
})
.sort((a, b) => (a.time as number) - (b.time as number));
seriesRef.current.setMarkers(markers);
}, [annotations]);
}, [annotations, selectedLabelId]);
// Handle chart clicks for annotation
useEffect(() => {
@ -244,6 +252,23 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
}
}
}
// Select/deselect label markers by clicking them
if (!activeTool || activeTool === 'break_up' || activeTool === 'break_down') {
const timestamp = typeof time === 'string' ? Date.parse(time) / 1000 : (time as number);
// Find annotation at this timestamp (within tolerance)
const tolerance = 60; // 60 seconds tolerance
const annotation = annotations.find(
(a) =>
(a.label_type === 'break_up' || a.label_type === 'break_down') &&
Math.abs(a.timestamp - timestamp) < tolerance
);
if (annotation) {
onLabelSelect?.(annotation.id === selectedLabelId ? -1 : annotation.id);
}
}
};
chartRef.current.subscribeClick(handleClick);