ca-grow-ops-manager/frontend/src/components/ui/PullToRefresh.tsx
fullsizemalt 38f9ef5f0b
Some checks are pending
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run
style: Complete visual refactor with CSS variable tokens
- Apply Climate Monitoring design system to all 81 files
- Replace 931 hardcoded color references with CSS variables
- Consistent theming: --color-primary, --color-text-*, --color-bg-*
- Status colors: --color-error, --color-warning, --color-accent
2025-12-27 11:55:09 -08:00

100 lines
3.2 KiB
TypeScript

import { useState, useRef, useCallback, ReactNode, TouchEvent } from 'react';
import { RefreshCw } from 'lucide-react';
interface PullToRefreshProps {
onRefresh: () => Promise<void>;
children: ReactNode;
disabled?: boolean;
}
export function PullToRefresh({ onRefresh, children, disabled = false }: PullToRefreshProps) {
const [pulling, setPulling] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [pullDistance, setPullDistance] = useState(0);
const startY = useRef(0);
const containerRef = useRef<HTMLDivElement>(null);
const THRESHOLD = 80; // pixels to pull before triggering refresh
const MAX_PULL = 120;
const handleTouchStart = useCallback((e: TouchEvent) => {
if (disabled || refreshing) return;
const scrollTop = containerRef.current?.scrollTop || 0;
if (scrollTop > 0) return; // Only trigger at top
startY.current = e.touches[0].clientY;
setPulling(true);
}, [disabled, refreshing]);
const handleTouchMove = useCallback((e: TouchEvent) => {
if (!pulling || refreshing) return;
const currentY = e.touches[0].clientY;
const diff = currentY - startY.current;
if (diff > 0) {
// Add resistance to pull
const resistance = Math.min(diff * 0.5, MAX_PULL);
setPullDistance(resistance);
}
}, [pulling, refreshing]);
const handleTouchEnd = useCallback(async () => {
if (!pulling) return;
setPulling(false);
if (pullDistance >= THRESHOLD && !refreshing) {
setRefreshing(true);
setPullDistance(THRESHOLD);
try {
await onRefresh();
} finally {
setRefreshing(false);
setPullDistance(0);
}
} else {
setPullDistance(0);
}
}, [pulling, pullDistance, refreshing, onRefresh]);
const progress = Math.min(pullDistance / THRESHOLD, 1);
const rotation = progress * 180;
return (
<div
ref={containerRef}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
className="relative overflow-auto h-full"
>
{/* Pull indicator */}
<div
className="absolute left-1/2 -translate-x-1/2 z-10 flex items-center justify-center transition-transform"
style={{
top: `${Math.max(-40, pullDistance - 40)}px`,
transform: `translateX(-50%) rotate(${rotation}deg)`,
opacity: progress
}}
>
<div className={`p-2 rounded-full bg-white dark:bg-slate-800 shadow-lg ${refreshing ? 'animate-spin' : ''}`}>
<RefreshCw size={20} className="text-[var(--color-primary)]" />
</div>
</div>
{/* Content with pull transform */}
<div
style={{
transform: `translateY(${pullDistance}px)`,
transition: pulling ? 'none' : 'transform 0.2s ease-out'
}}
>
{children}
</div>
</div>
);
}