Complete TypeScript migration
- Convert all shared components to TypeScript (.jsx → .tsx) - Carousel, ChartCard, EmptyState, FilterControls, StatCard, ToggleSwitch - Add proper TypeScript interfaces for all component props - Delete unused dataService.legacy.ts (archived Google Sheets code) - Build passes successfully
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
import React, { useRef, useCallback, useState } from 'react';
|
||||
import React, { useRef, useCallback, useState, ReactNode, KeyboardEvent, TouchEvent } from 'react';
|
||||
|
||||
interface CarouselProps {
|
||||
children: ReactNode;
|
||||
activeIndex: number;
|
||||
setActiveIndex: (index: number) => void;
|
||||
labels?: string[];
|
||||
showLabels?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function Carousel({
|
||||
children,
|
||||
@@ -7,10 +16,10 @@ function Carousel({
|
||||
labels = [],
|
||||
showLabels = true,
|
||||
className = ''
|
||||
}) {
|
||||
const touchStartX = useRef(null);
|
||||
const touchStartY = useRef(null);
|
||||
const trackRef = useRef(null);
|
||||
}: CarouselProps) {
|
||||
const touchStartX = useRef<number | null>(null);
|
||||
const touchStartY = useRef<number | null>(null);
|
||||
const trackRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragOffset, setDragOffset] = useState(0);
|
||||
const itemCount = React.Children.count(children);
|
||||
@@ -19,20 +28,20 @@ function Carousel({
|
||||
const SWIPE_THRESHOLD = 50;
|
||||
const VELOCITY_THRESHOLD = 0.3;
|
||||
|
||||
const handleTouchStart = useCallback((e) => {
|
||||
const handleTouchStart = useCallback((e: TouchEvent<HTMLDivElement>) => {
|
||||
touchStartX.current = e.touches[0].clientX;
|
||||
touchStartY.current = e.touches[0].clientY;
|
||||
setIsDragging(true);
|
||||
setDragOffset(0);
|
||||
}, []);
|
||||
|
||||
const handleTouchMove = useCallback((e) => {
|
||||
const handleTouchMove = useCallback((e: TouchEvent<HTMLDivElement>) => {
|
||||
if (!touchStartX.current || !isDragging) return;
|
||||
|
||||
const currentX = e.touches[0].clientX;
|
||||
const currentY = e.touches[0].clientY;
|
||||
const diffX = currentX - touchStartX.current;
|
||||
const diffY = currentY - touchStartY.current;
|
||||
const diffY = currentY - (touchStartY.current || 0);
|
||||
|
||||
// Only handle horizontal swipes
|
||||
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||||
@@ -46,7 +55,7 @@ function Carousel({
|
||||
}
|
||||
}, [isDragging, activeIndex, itemCount]);
|
||||
|
||||
const handleTouchEnd = useCallback((e) => {
|
||||
const handleTouchEnd = useCallback((e: TouchEvent<HTMLDivElement>) => {
|
||||
if (!touchStartX.current || !isDragging) return;
|
||||
|
||||
const endX = e.changedTouches[0].clientX;
|
||||
@@ -69,7 +78,7 @@ function Carousel({
|
||||
setDragOffset(0);
|
||||
}, [isDragging, activeIndex, setActiveIndex, itemCount]);
|
||||
|
||||
const handleKeyDown = useCallback((e) => {
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'ArrowLeft' && activeIndex > 0) {
|
||||
setActiveIndex(activeIndex - 1);
|
||||
} else if (e.key === 'ArrowRight' && activeIndex < itemCount - 1) {
|
||||
@@ -1,4 +1,13 @@
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
interface ChartCardProps {
|
||||
title?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
headerRight?: ReactNode;
|
||||
fullWidth?: boolean;
|
||||
halfWidth?: boolean;
|
||||
}
|
||||
|
||||
function ChartCard({
|
||||
title,
|
||||
@@ -7,7 +16,7 @@ function ChartCard({
|
||||
headerRight = null,
|
||||
fullWidth = false,
|
||||
halfWidth = false
|
||||
}) {
|
||||
}: ChartCardProps) {
|
||||
const sizeClass = fullWidth ? 'full-width' : halfWidth ? 'half-width' : '';
|
||||
|
||||
return (
|
||||
@@ -1,5 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon?: string;
|
||||
title?: string;
|
||||
message?: string;
|
||||
action?: (() => void) | null;
|
||||
actionLabel?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function EmptyState({
|
||||
icon = '📊',
|
||||
title,
|
||||
@@ -7,7 +16,7 @@ function EmptyState({
|
||||
action = null,
|
||||
actionLabel = 'Try Again',
|
||||
className = ''
|
||||
}) {
|
||||
}: EmptyStateProps) {
|
||||
return (
|
||||
<div className={`empty-state ${className}`}>
|
||||
<div className="empty-state-icon" role="img" aria-hidden="true">
|
||||
@@ -1,13 +1,35 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, ReactNode, KeyboardEvent } from 'react';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
|
||||
function FilterControls({
|
||||
interface FilterControlsProps {
|
||||
children: ReactNode;
|
||||
title?: string;
|
||||
defaultExpanded?: boolean;
|
||||
onReset?: (() => void) | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface FilterGroupProps {
|
||||
label?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface FilterRowProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface FilterControlsComponent extends React.FC<FilterControlsProps> {
|
||||
Group: React.FC<FilterGroupProps>;
|
||||
Row: React.FC<FilterRowProps>;
|
||||
}
|
||||
|
||||
const FilterControls: FilterControlsComponent = ({
|
||||
children,
|
||||
title,
|
||||
defaultExpanded = true,
|
||||
onReset = null,
|
||||
className = ''
|
||||
}) {
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const displayTitle = title || t('filters.title');
|
||||
|
||||
@@ -36,6 +58,13 @@ function FilterControls({
|
||||
setExpanded(!expanded);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleExpanded();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`controls ${expanded ? 'expanded' : 'collapsed'} ${className}`}>
|
||||
<div
|
||||
@@ -44,12 +73,7 @@ function FilterControls({
|
||||
role="button"
|
||||
aria-expanded={expanded}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleExpanded();
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<h3>{displayTitle}</h3>
|
||||
<div className="controls-header-actions">
|
||||
@@ -85,20 +109,20 @@ function FilterControls({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function FilterGroup({ label, children }) {
|
||||
const FilterGroup: React.FC<FilterGroupProps> = ({ label, children }) => {
|
||||
return (
|
||||
<div className="control-group">
|
||||
{label && <label>{label}</label>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function FilterRow({ children }) {
|
||||
const FilterRow: React.FC<FilterRowProps> = ({ children }) => {
|
||||
return <div className="control-row">{children}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
FilterControls.Group = FilterGroup;
|
||||
FilterControls.Row = FilterRow;
|
||||
@@ -1,6 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
function StatCard({ title, value, change = null, changeLabel = 'YoY', subtitle = null }) {
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
change?: number | null;
|
||||
changeLabel?: string;
|
||||
subtitle?: string | null;
|
||||
}
|
||||
|
||||
function StatCard({ title, value, change = null, changeLabel = 'YoY', subtitle = null }: StatCardProps) {
|
||||
const isPositive = change !== null && change >= 0;
|
||||
|
||||
return (
|
||||
@@ -1,6 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
function ToggleSwitch({ options, value, onChange, className = '' }) {
|
||||
interface ToggleOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ToggleSwitchProps {
|
||||
options: ToggleOption[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function ToggleSwitch({ options, value, onChange, className = '' }: ToggleSwitchProps) {
|
||||
return (
|
||||
<div className={`toggle-switch ${className}`} role="radiogroup">
|
||||
{options.map((option) => (
|
||||
Reference in New Issue
Block a user