This commit is contained in:
Tomas Dvorak
2025-10-17 17:39:11 +02:00
parent 35d0954afd
commit e9a63073e5
61 changed files with 3824 additions and 1061 deletions
+126 -10
View File
@@ -482,6 +482,10 @@ const MatchesAdminPage = () => {
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [lastX, setLastX] = useState(0);
const [lastTime, setLastTime] = useState(0);
const velocityRef = useRef(0);
const animationRef = useRef<number | null>(null);
// Color modes for past/future matches
const pastMatchBg = useColorModeValue('gray.100', 'gray.700');
@@ -499,11 +503,20 @@ const MatchesAdminPage = () => {
// Drag-to-scroll handlers
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
if (!scrollRef.current) return;
// Cancel any ongoing momentum animation
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
animationRef.current = null;
}
setIsDragging(true);
setStartX(e.pageX - scrollRef.current.offsetLeft);
setScrollLeft(scrollRef.current.scrollLeft);
setLastX(e.pageX);
setLastTime(Date.now());
velocityRef.current = 0;
scrollRef.current.style.cursor = 'grabbing';
scrollRef.current.style.userSelect = 'none';
scrollRef.current.style.scrollBehavior = 'auto'; // Disable smooth scroll during drag
};
const handleMouseLeave = () => {
@@ -519,6 +532,24 @@ const MatchesAdminPage = () => {
if (scrollRef.current) {
scrollRef.current.style.cursor = 'grab';
scrollRef.current.style.userSelect = 'auto';
scrollRef.current.style.scrollBehavior = 'smooth';
// Apply momentum scrolling
const velocity = velocityRef.current;
if (Math.abs(velocity) > 0.5) {
const applyMomentum = () => {
if (!scrollRef.current) return;
velocityRef.current *= 0.95; // Deceleration factor
scrollRef.current.scrollLeft -= velocityRef.current;
if (Math.abs(velocityRef.current) > 0.5) {
animationRef.current = requestAnimationFrame(applyMomentum);
} else {
animationRef.current = null;
}
};
animationRef.current = requestAnimationFrame(applyMomentum);
}
}
};
@@ -526,8 +557,77 @@ const MatchesAdminPage = () => {
if (!isDragging || !scrollRef.current) return;
e.preventDefault();
const x = e.pageX - scrollRef.current.offsetLeft;
const walk = (x - startX) * 2; // Scroll speed multiplier
const walk = (x - startX) * 1.5; // Scroll speed multiplier (reduced for smoother feel)
scrollRef.current.scrollLeft = scrollLeft - walk;
// Calculate velocity for momentum
const now = Date.now();
const timeDelta = now - lastTime;
if (timeDelta > 0) {
const currentX = e.pageX;
const distance = currentX - lastX;
velocityRef.current = distance / timeDelta * 16; // Normalize to ~60fps
setLastX(currentX);
setLastTime(now);
}
};
// Touch handlers for mobile
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
if (!scrollRef.current) return;
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
animationRef.current = null;
}
const touch = e.touches[0];
setIsDragging(true);
setStartX(touch.pageX - scrollRef.current.offsetLeft);
setScrollLeft(scrollRef.current.scrollLeft);
setLastX(touch.pageX);
setLastTime(Date.now());
velocityRef.current = 0;
if (scrollRef.current) scrollRef.current.style.scrollBehavior = 'auto';
};
const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
if (!isDragging || !scrollRef.current) return;
const touch = e.touches[0];
const x = touch.pageX - scrollRef.current.offsetLeft;
const walk = (x - startX) * 1.5;
scrollRef.current.scrollLeft = scrollLeft - walk;
const now = Date.now();
const timeDelta = now - lastTime;
if (timeDelta > 0) {
const currentX = touch.pageX;
const distance = currentX - lastX;
velocityRef.current = distance / timeDelta * 16;
setLastX(currentX);
setLastTime(now);
}
};
const handleTouchEnd = () => {
setIsDragging(false);
if (scrollRef.current) {
scrollRef.current.style.scrollBehavior = 'smooth';
const velocity = velocityRef.current;
if (Math.abs(velocity) > 0.5) {
const applyMomentum = () => {
if (!scrollRef.current) return;
velocityRef.current *= 0.95;
scrollRef.current.scrollLeft -= velocityRef.current;
if (Math.abs(velocityRef.current) > 0.5) {
animationRef.current = requestAnimationFrame(applyMomentum);
} else {
animationRef.current = null;
}
};
animationRef.current = requestAnimationFrame(applyMomentum);
}
}
};
// Utility to check if match is in the past
@@ -551,7 +651,13 @@ const MatchesAdminPage = () => {
updateScrollShadow();
const onResize = () => updateScrollShadow();
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
// Cleanup momentum animation on unmount
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, []);
const headerBg = useColorModeValue('brand.primary', 'gray.700');
@@ -656,8 +762,8 @@ const MatchesAdminPage = () => {
</WrapItem>
</Wrap>
{showScrollHint && (
<Text fontSize="xs" color="blue.600" fontWeight="600" mb={2}>
💡 Tip: Tabulku můžete posouvat tažením myší nebo touchem
<Text fontSize="xs" color="blue.600" fontWeight="600" mb={2} display="flex" alignItems="center" gap={1}>
💡 Tip: Tabulku můžete plynule posouvat tažením myší nebo prstem
</Text>
)}
<Box
@@ -676,23 +782,33 @@ const MatchesAdminPage = () => {
onMouseLeave={handleMouseLeave}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onScroll={(e) => {
updateScrollShadow();
if ((e.currentTarget as HTMLDivElement).scrollLeft > 0 && showScrollHint) setShowScrollHint(false);
}}
sx={{
WebkitOverflowScrolling: 'touch',
scrollBehavior: 'smooth',
'th, td': { whiteSpace: 'nowrap' },
'::-webkit-scrollbar': { height: '12px' },
'::-webkit-scrollbar': { height: '14px' },
'::-webkit-scrollbar-thumb': {
background: '#3182ce',
borderRadius: '8px',
'&:hover': { background: '#2c5aa0' }
borderRadius: '10px',
border: '3px solid transparent',
backgroundClip: 'content-box',
transition: 'background 0.2s ease',
'&:hover': { background: '#2c5aa0', backgroundClip: 'content-box' },
'&:active': { background: '#2a4e8a', backgroundClip: 'content-box' }
},
'::-webkit-scrollbar-track': {
background: '#e2e8f0',
borderRadius: '8px',
margin: '0 4px'
background: useColorModeValue('#f7fafc', '#2d3748'),
borderRadius: '10px',
margin: '0 8px',
border: '1px solid',
borderColor: useColorModeValue('#e2e8f0', '#4a5568')
},
}}
>