refactor: major architecture improvements

Security:
- Remove exposed NocoDB token from client code
- Add .env.example for environment variables

Shared Components:
- Carousel: touch/keyboard navigation, accessibility
- ChartCard: reusable chart container
- EmptyState: for no-data scenarios
- FilterControls: collapsible filter panel with reset button
- StatCard: metric display with change indicator
- ToggleSwitch: accessible radio-style toggle

Architecture:
- Create src/config/chartConfig.js for shared chart options
- Extract ChartJS registration to single location
- Reduce code duplication in Dashboard and Comparison

UX Improvements:
- Add empty state when filters return no data
- Add Reset Filters button to filter controls
- Add skeleton loader CSS utilities
- Improve focus states for accessibility
- Use shared components in Dashboard and Comparison
This commit is contained in:
fahed
2026-02-02 13:50:23 +03:00
parent 24fa601aec
commit 8a3b6a8d2e
13 changed files with 590 additions and 314 deletions

View File

@@ -0,0 +1,78 @@
import React, { useRef, useCallback } from 'react';
function Carousel({
children,
activeIndex,
setActiveIndex,
labels = [],
showLabels = true,
className = ''
}) {
const touchStart = useRef(null);
const itemCount = React.Children.count(children);
const handleTouchStart = useCallback((e) => {
touchStart.current = e.touches[0].clientX;
}, []);
const handleTouchEnd = useCallback((e) => {
if (!touchStart.current) return;
const diff = touchStart.current - e.changedTouches[0].clientX;
if (Math.abs(diff) > 50) {
if (diff > 0 && activeIndex < itemCount - 1) {
setActiveIndex(activeIndex + 1);
} else if (diff < 0 && activeIndex > 0) {
setActiveIndex(activeIndex - 1);
}
}
touchStart.current = null;
}, [activeIndex, setActiveIndex, itemCount]);
const handleKeyDown = useCallback((e) => {
if (e.key === 'ArrowLeft' && activeIndex > 0) {
setActiveIndex(activeIndex - 1);
} else if (e.key === 'ArrowRight' && activeIndex < itemCount - 1) {
setActiveIndex(activeIndex + 1);
}
}, [activeIndex, setActiveIndex, itemCount]);
return (
<div className={`carousel ${className}`} onKeyDown={handleKeyDown} tabIndex={0}>
<div className="carousel-container">
<div className="carousel-viewport">
<div
className="carousel-track"
style={{ transform: `translateX(-${activeIndex * 100}%)` }}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
{React.Children.map(children, (child, i) => (
<div className="carousel-slide" key={i}>
{child}
</div>
))}
</div>
</div>
</div>
<div className={`carousel-dots ${showLabels ? 'labeled' : ''}`} role="tablist">
{Array.from({ length: itemCount }).map((_, i) => (
<button
key={i}
className={`carousel-dot ${activeIndex === i ? 'active' : ''}`}
onClick={() => setActiveIndex(i)}
role="tab"
aria-selected={activeIndex === i}
aria-label={labels[i] || `Slide ${i + 1}`}
>
{showLabels && labels[i] && (
<span className="dot-label">{labels[i]}</span>
)}
</button>
))}
</div>
</div>
);
}
export default Carousel;