Hotfix for v1.3.0 production issue where PullToRefresh component was blocking all mobile scrolling. Root Cause: - CSS flex chain break (container uses h-full instead of flex-1) - Touch event handlers interfering with native scroll Fix: Temporarily disabled PullToRefresh via early return that bypasses all functionality while still rendering children. Feature will be properly fixed in v1.4. This restores normal scrolling on mobile devices across all pages. Also includes: - CHANGELOG.md updates for v1.3.1 and v1.3.2 - README.md typo fix
139 lines
5.0 KiB
TypeScript
139 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useCallback, ReactNode, TouchEvent } from "react";
|
|
import { GradientSpinner } from "./GradientSpinner";
|
|
import { RefreshCw } from "lucide-react";
|
|
|
|
interface PullToRefreshProps {
|
|
children: ReactNode;
|
|
threshold?: number;
|
|
}
|
|
|
|
export function PullToRefresh({
|
|
children,
|
|
threshold = 80,
|
|
}: PullToRefreshProps) {
|
|
// HOTFIX v1.3.2: Temporarily disabled - blocking mobile scrolling
|
|
// TODO: Fix in v1.4 - Issues: 1) h-full breaks flex layout, 2) touch handlers may interfere
|
|
// Proper fix: Change line 90 className to "relative flex-1 flex flex-col min-h-0"
|
|
return <>{children}</>;
|
|
|
|
const [pullDistance, setPullDistance] = useState(0);
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
const startY = useRef(0);
|
|
const isPulling = useRef(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const handleTouchStart = useCallback((e: TouchEvent) => {
|
|
// Only allow pull-to-refresh when scrolled to the top
|
|
const container = containerRef.current;
|
|
if (!container) return;
|
|
|
|
// Check if the main element inside is scrolled to top
|
|
const mainElement = container.querySelector("main");
|
|
if (mainElement && mainElement.scrollTop === 0) {
|
|
startY.current = e.touches[0].clientY;
|
|
isPulling.current = true;
|
|
}
|
|
}, []);
|
|
|
|
const handleTouchMove = useCallback(
|
|
(e: TouchEvent) => {
|
|
if (!isPulling.current || isRefreshing) return;
|
|
|
|
const currentY = e.touches[0].clientY;
|
|
const distance = currentY - startY.current;
|
|
|
|
// Only track downward pulls
|
|
if (distance > 0) {
|
|
// Apply resistance factor for smoother feel
|
|
const resistance = 0.5;
|
|
const adjustedDistance = distance * resistance;
|
|
|
|
setPullDistance(adjustedDistance);
|
|
|
|
// Prevent default scroll behavior when pulling
|
|
if (adjustedDistance > 10) {
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
},
|
|
[isRefreshing]
|
|
);
|
|
|
|
const handleTouchEnd = useCallback(() => {
|
|
if (!isPulling.current) return;
|
|
|
|
isPulling.current = false;
|
|
|
|
// Check if we've pulled past the threshold
|
|
if (pullDistance >= threshold) {
|
|
setIsRefreshing(true);
|
|
setPullDistance(threshold); // Lock at threshold during refresh
|
|
|
|
// Trigger full page reload after a brief delay for visual feedback
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 300);
|
|
} else {
|
|
// Reset if not past threshold
|
|
setPullDistance(0);
|
|
}
|
|
}, [pullDistance, threshold]);
|
|
|
|
// Calculate visual properties based on pull progress
|
|
const pullProgress = Math.min(pullDistance / threshold, 1);
|
|
const showIndicator = pullDistance > 0;
|
|
const shouldRelease = pullDistance >= threshold;
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
onTouchStart={handleTouchStart}
|
|
onTouchMove={handleTouchMove}
|
|
onTouchEnd={handleTouchEnd}
|
|
className="relative h-full"
|
|
style={{ touchAction: "pan-y" }}
|
|
>
|
|
{/* Pull-to-refresh indicator */}
|
|
{showIndicator && (
|
|
<div
|
|
className="absolute top-0 left-0 right-0 z-50 flex flex-col items-center justify-center pointer-events-none"
|
|
style={{
|
|
transform: `translateY(${Math.min(pullDistance, threshold + 20)}px)`,
|
|
opacity: pullProgress,
|
|
transition: isPulling.current
|
|
? "none"
|
|
: "transform 0.3s ease-out, opacity 0.3s ease-out",
|
|
willChange: "transform",
|
|
}}
|
|
>
|
|
<div className="bg-black/80 backdrop-blur-sm rounded-full p-3 shadow-lg border border-white/10">
|
|
{isRefreshing ? (
|
|
<GradientSpinner size="sm" />
|
|
) : (
|
|
<RefreshCw
|
|
className={`w-5 h-5 text-white transition-transform ${
|
|
shouldRelease ? "rotate-180" : ""
|
|
}`}
|
|
style={{
|
|
transform: `rotate(${pullDistance * 2}deg)`,
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
<p className="text-white/80 text-xs mt-2 font-medium">
|
|
{isRefreshing
|
|
? "Refreshing..."
|
|
: shouldRelease
|
|
? "Release to refresh"
|
|
: "Pull to refresh"}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|