Mobile UX enhancements (desktop unchanged)

- Added mobile-only CSS improvements at end of file
- Enhanced touch targets (min 44px for accessibility)
- Improved carousel with real-time drag feedback and rubber band effect
- Better visual feedback on touch interactions
- Bottom nav active indicator
- Improved stat/chart cards spacing on mobile
- Period banner stacks vertically on mobile
- Table scroll hint with fade gradient
- Extra small screen (≤375px) optimizations
- All changes scoped to @media (max-width: 768px)
This commit is contained in:
fahed
2026-02-03 15:13:34 +03:00
parent 0e5d285680
commit 480885a8e6
5 changed files with 1548 additions and 1653 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,7 @@ function Carousel({
const [dragOffset, setDragOffset] = useState(0); const [dragOffset, setDragOffset] = useState(0);
const itemCount = React.Children.count(children); const itemCount = React.Children.count(children);
// Threshold for swipe detection
const SWIPE_THRESHOLD = 50; const SWIPE_THRESHOLD = 50;
const VELOCITY_THRESHOLD = 0.3;
const handleTouchStart = useCallback((e) => { const handleTouchStart = useCallback((e) => {
touchStartX.current = e.touches[0].clientX; touchStartX.current = e.touches[0].clientX;
@@ -32,15 +30,14 @@ function Carousel({
const currentX = e.touches[0].clientX; const currentX = e.touches[0].clientX;
const currentY = e.touches[0].clientY; const currentY = e.touches[0].clientY;
const diffX = currentX - touchStartX.current; const diffX = currentX - touchStartX.current;
const diffY = currentY - touchStartY.current; const diffY = Math.abs(currentY - touchStartY.current);
// Only handle horizontal swipes // Only handle horizontal swipes (prevent vertical scroll interference)
if (Math.abs(diffX) > Math.abs(diffY)) { if (Math.abs(diffX) > diffY) {
e.preventDefault(); // Add resistance at edges (rubber band effect)
// Add resistance at edges
let offset = diffX; let offset = diffX;
if ((activeIndex === 0 && diffX > 0) || (activeIndex === itemCount - 1 && diffX < 0)) { if ((activeIndex === 0 && diffX > 0) || (activeIndex === itemCount - 1 && diffX < 0)) {
offset = diffX * 0.3; // Rubber band effect offset = diffX * 0.25;
} }
setDragOffset(offset); setDragOffset(offset);
} }
@@ -51,10 +48,9 @@ function Carousel({
const endX = e.changedTouches[0].clientX; const endX = e.changedTouches[0].clientX;
const diff = touchStartX.current - endX; const diff = touchStartX.current - endX;
const velocity = Math.abs(diff) / 200; // Rough velocity calc
// Determine if we should change slide // Change slide if swipe exceeds threshold
if (Math.abs(diff) > SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) { if (Math.abs(diff) > SWIPE_THRESHOLD) {
if (diff > 0 && activeIndex < itemCount - 1) { if (diff > 0 && activeIndex < itemCount - 1) {
setActiveIndex(activeIndex + 1); setActiveIndex(activeIndex + 1);
} else if (diff < 0 && activeIndex > 0) { } else if (diff < 0 && activeIndex > 0) {
@@ -77,9 +73,11 @@ function Carousel({
} }
}, [activeIndex, setActiveIndex, itemCount]); }, [activeIndex, setActiveIndex, itemCount]);
// Calculate transform // Calculate transform with drag offset
const baseTransform = -(activeIndex * 100); const baseTransform = -(activeIndex * 100);
const dragPercentage = trackRef.current ? (dragOffset / trackRef.current.offsetWidth) * 100 : 0; const dragPercentage = trackRef.current
? (dragOffset / trackRef.current.offsetWidth) * 100
: 0;
const transform = baseTransform + dragPercentage; const transform = baseTransform + dragPercentage;
return ( return (
@@ -97,7 +95,7 @@ function Carousel({
className="carousel-track" className="carousel-track"
style={{ style={{
transform: `translateX(${transform}%)`, transform: `translateX(${transform}%)`,
transition: isDragging ? 'none' : 'transform 400ms cubic-bezier(0.25, 0.46, 0.45, 0.94)' transition: isDragging ? 'none' : undefined
}} }}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove} onTouchMove={handleTouchMove}
@@ -109,7 +107,6 @@ function Carousel({
key={i} key={i}
role="tabpanel" role="tabpanel"
aria-hidden={activeIndex !== i} aria-hidden={activeIndex !== i}
aria-label={labels[i] || `Slide ${i + 1}`}
> >
{child} {child}
</div> </div>
@@ -127,7 +124,6 @@ function Carousel({
role="tab" role="tab"
aria-selected={activeIndex === i} aria-selected={activeIndex === i}
aria-label={labels[i] || `Slide ${i + 1}`} aria-label={labels[i] || `Slide ${i + 1}`}
aria-controls={`slide-${i}`}
> >
{showLabels && labels[i] && ( {showLabels && labels[i] && (
<span className="dot-label">{labels[i]}</span> <span className="dot-label">{labels[i]}</span>

View File

@@ -2,29 +2,18 @@ import React from 'react';
function EmptyState({ function EmptyState({
icon = '📊', icon = '📊',
title, title = 'No data found',
message, message = 'Try adjusting your filters',
action = null, action,
actionLabel = 'Try Again', actionLabel = 'Reset Filters'
className = ''
}) { }) {
return ( return (
<div className={`empty-state ${className}`}> <div className="empty-state">
<div className="empty-state-icon" role="img" aria-hidden="true"> <div className="empty-state-icon">{icon}</div>
{icon}
</div>
{title && (
<h3 className="empty-state-title">{title}</h3> <h3 className="empty-state-title">{title}</h3>
)}
{message && (
<p className="empty-state-message">{message}</p> <p className="empty-state-message">{message}</p>
)}
{action && ( {action && (
<button <button className="empty-state-action" onClick={action}>
className="empty-state-action"
onClick={action}
type="button"
>
{actionLabel} {actionLabel}
</button> </button>
)} )}

View File

@@ -1,86 +1,33 @@
import React, { useState, useEffect } from 'react'; import React, { useState } from 'react';
import { useLanguage } from '../../contexts/LanguageContext';
function FilterControls({ function FilterControls({
children, children,
title, title = 'Filters',
defaultExpanded = true, defaultExpanded = true,
onReset = null, onReset = null,
className = '' className = ''
}) { }) {
const { t } = useLanguage(); const [expanded, setExpanded] = useState(defaultExpanded);
const displayTitle = title || t('filters.title');
// Start collapsed on mobile
const [expanded, setExpanded] = useState(() => {
if (typeof window !== 'undefined') {
return window.innerWidth > 768 ? defaultExpanded : false;
}
return defaultExpanded;
});
// Handle resize
useEffect(() => {
const handleResize = () => {
// Auto-expand on desktop, keep user preference on mobile
if (window.innerWidth > 768) {
setExpanded(true);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
const toggleExpanded = () => {
setExpanded(!expanded);
};
return ( return (
<div className={`controls ${expanded ? 'expanded' : 'collapsed'} ${className}`}> <div className={`controls ${expanded ? 'expanded' : 'collapsed'} ${className}`}>
<div <div className="controls-header" onClick={() => setExpanded(!expanded)}>
className="controls-header" <h3>{title}</h3>
onClick={toggleExpanded}
role="button"
aria-expanded={expanded}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleExpanded();
}
}}
>
<h3>{displayTitle}</h3>
<div className="controls-header-actions"> <div className="controls-header-actions">
{onReset && expanded && ( {onReset && expanded && (
<button <button
className="controls-reset" className="controls-reset"
onClick={(e) => { onClick={(e) => { e.stopPropagation(); onReset(); }}
e.stopPropagation();
onReset();
}}
aria-label={t('filters.reset') || 'Reset filters'}
> >
{t('filters.reset') || 'Reset'} Reset
</button> </button>
)} )}
<button <button className="controls-toggle">
className="controls-toggle" {expanded ? '▲ Hide' : '▼ Show'}
aria-hidden="true"
>
{expanded ? '▲' : '▼'}
</button> </button>
</div> </div>
</div> </div>
<div className="controls-body">
<div
className="controls-body"
style={{
display: expanded ? 'block' : 'none',
animation: expanded ? 'fadeIn 200ms ease' : 'none'
}}
>
{children} {children}
</div> </div>
</div> </div>
@@ -90,7 +37,7 @@ function FilterControls({
function FilterGroup({ label, children }) { function FilterGroup({ label, children }) {
return ( return (
<div className="control-group"> <div className="control-group">
{label && <label>{label}</label>} <label>{label}</label>
{children} {children}
</div> </div>
); );

View File

@@ -1,20 +1,15 @@
import React from 'react'; import React from 'react';
function StatCard({ title, value, change = null, changeLabel = 'YoY', subtitle = null }) { function StatCard({ title, value, change = null, changeLabel = 'YoY' }) {
const isPositive = change !== null && change >= 0; const isPositive = change !== null && change >= 0;
return ( return (
<div className="stat-card"> <div className="stat-card">
<h3>{title}</h3> <h3>{title}</h3>
<div className="stat-value">{value}</div> <div className="stat-value">{value}</div>
{subtitle && (
<div className="stat-subtitle">{subtitle}</div>
)}
{change !== null && ( {change !== null && (
<div className={`stat-change ${isPositive ? 'positive' : 'negative'}`}> <div className={`stat-change ${isPositive ? 'positive' : 'negative'}`}>
<span className="stat-change-arrow">{isPositive ? '↑' : '↓'}</span> {isPositive ? '↑' : '↓'} {Math.abs(change).toFixed(1)}% {changeLabel}
<span className="stat-change-value">{Math.abs(change).toFixed(1)}%</span>
<span className="stat-change-label">{changeLabel}</span>
</div> </div>
)} )}
</div> </div>