Skip to content

Commit 43eb141

Browse files
committed
refactor: optimize error pages with refined animations and mobile support
- Load Failed: Add Snellen chart visual effect with optimized mouse tracking and mobile touch support - Session Expired: Implement ripple timer with pulsating squircles, breathing animation, and accessibility - Add ripple animations to index.css with reduced-motion support - Optimize for mobile with responsive scaling and typography - Improve performance with memoization, throttling, and GPU acceleration hints - Add prefers-reduced-motion support for accessibility compliance - Unify error page typography and layout patterns
1 parent 686daba commit 43eb141

3 files changed

Lines changed: 458 additions & 61 deletions

File tree

Lines changed: 199 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,230 @@
1+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
import { ArrowLeft, RotateCw, WifiOff } from "lucide-react";
23

34
import { Button } from "@/components/ui/button";
45

6+
const SNELLEN_LINES = [
7+
"C",
8+
"O U",
9+
"L D N",
10+
"T L O A D",
11+
"T H E P A G E",
12+
"R I G H T N O W",
13+
"R E T U R N T O C A R E",
14+
];
15+
16+
const BLUR_RADIUS_PX = 180;
17+
const TOUCH_UNBLUR_DURATION = 6000;
18+
const BASE_FONT_SIZE = 56;
19+
const FONT_SIZE_DECREMENT = 7;
20+
const BLUR_INTENSITY = "2px";
21+
const SCALE_DESKTOP = 1;
22+
const SCALE_MOBILE = 0.75;
23+
const MOUSE_THROTTLE_MS = 16; // ~60fps
24+
25+
// Memoized Snellen lines component to prevent unnecessary re-renders
26+
const SnellenLines = ({ isUnblurred }: { isUnblurred: boolean }) => (
27+
<div className="relative z-10 flex flex-col items-center px-6 pt-6 pb-8 font-bold text-center leading-none">
28+
{SNELLEN_LINES.map((line, index) => {
29+
const fontSize = BASE_FONT_SIZE - index * FONT_SIZE_DECREMENT;
30+
return (
31+
<p
32+
key={`snellen-${index}`}
33+
className="text-black select-none pb-3 transition-all duration-300 will-change-[filter]"
34+
style={{
35+
fontSize: `${fontSize}px`,
36+
filter: isUnblurred ? "blur(0px)" : `blur(${BLUR_INTENSITY})`,
37+
}}
38+
>
39+
{line}
40+
</p>
41+
);
42+
})}
43+
</div>
44+
);
45+
546
export default function LoadFailedErrorPage() {
47+
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
48+
const [isUnblurred, setIsUnblurred] = useState(false);
49+
const containerRef = useRef<HTMLDivElement>(null);
50+
const touchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
51+
const mouseMoveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
52+
const isTouchDeviceRef = useRef(false);
53+
54+
// Detect touch device once on mount
55+
useEffect(() => {
56+
isTouchDeviceRef.current =
57+
typeof window !== "undefined" &&
58+
("ontouchstart" in window || navigator.maxTouchPoints > 0);
59+
}, []);
60+
61+
// Cleanup on unmount
62+
useEffect(() => {
63+
return () => {
64+
if (touchTimeoutRef.current) clearTimeout(touchTimeoutRef.current);
65+
if (mouseMoveTimeoutRef.current) clearTimeout(mouseMoveTimeoutRef.current);
66+
};
67+
}, []);
68+
69+
// Throttled mouse tracking for blur mask effect (desktop only)
70+
const handleMouseMove = useCallback((e: MouseEvent) => {
71+
if (mouseMoveTimeoutRef.current || isTouchDeviceRef.current) return;
72+
73+
const rect = containerRef.current?.getBoundingClientRect();
74+
if (rect) {
75+
setMousePos({
76+
x: e.clientX - rect.left,
77+
y: e.clientY - rect.top,
78+
});
79+
80+
mouseMoveTimeoutRef.current = setTimeout(() => {
81+
mouseMoveTimeoutRef.current = null;
82+
}, MOUSE_THROTTLE_MS);
83+
}
84+
}, []);
85+
86+
const handlePointerEnter = useCallback(() => {
87+
if (!isTouchDeviceRef.current) {
88+
setIsUnblurred(true);
89+
}
90+
}, []);
91+
92+
const handlePointerLeave = useCallback(() => {
93+
if (!isTouchDeviceRef.current && !touchTimeoutRef.current) {
94+
setIsUnblurred(false);
95+
}
96+
}, []);
97+
98+
// Touch handler for mobile (trigger brief unblur)
99+
const handleTouchStart = useCallback(() => {
100+
if (touchTimeoutRef.current) {
101+
clearTimeout(touchTimeoutRef.current);
102+
}
103+
104+
setIsUnblurred(true);
105+
touchTimeoutRef.current = setTimeout(() => {
106+
setIsUnblurred(false);
107+
touchTimeoutRef.current = null;
108+
}, TOUCH_UNBLUR_DURATION);
109+
}, []);
110+
111+
// Clean up touch on touchend to allow state reset
112+
const handleTouchEnd = useCallback(() => {
113+
// Timeout continues to manage the blur
114+
}, []);
115+
116+
// Setup event listeners with passive flag for better scroll performance
117+
useEffect(() => {
118+
const container = containerRef.current;
119+
if (!container) return;
120+
121+
container.addEventListener("mousemove", handleMouseMove, { passive: true });
122+
container.addEventListener("touchstart", handleTouchStart, {
123+
passive: true,
124+
});
125+
container.addEventListener("touchend", handleTouchEnd, { passive: true });
126+
127+
return () => {
128+
container.removeEventListener("mousemove", handleMouseMove);
129+
container.removeEventListener("touchstart", handleTouchStart);
130+
container.removeEventListener("touchend", handleTouchEnd);
131+
};
132+
}, [handleMouseMove, handleTouchStart, handleTouchEnd]);
133+
134+
// Memoize mask image to prevent recalculation on every render
135+
const maskImage = useMemo(
136+
() =>
137+
`radial-gradient(circle ${BLUR_RADIUS_PX}px at ${mousePos.x}px ${mousePos.y}px, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 70%)`,
138+
[mousePos.x, mousePos.y]
139+
);
140+
6141
return (
7-
<div className="bg-background text-foreground flex min-h-screen w-full items-center justify-center px-6 py-16">
8-
<div className="w-full max-w-md text-center">
9-
<div className="bg-muted text-muted-foreground mx-auto flex size-14 items-center justify-center rounded-full">
10-
<WifiOff className="size-6" />
142+
<main className="bg-background text-foreground flex min-h-screen w-full flex-col items-center justify-center px-4 py-16">
143+
{/* Snellen Chart */}
144+
<div
145+
className="relative scale-[var(--scale-mobile)] rounded-xl border-6 border-yellow-950 transition-all duration-300 will-change-transform md:scale-[var(--scale-desktop)] md:justify-center"
146+
ref={containerRef}
147+
onPointerEnter={handlePointerEnter}
148+
onPointerLeave={handlePointerLeave}
149+
style={{
150+
"--scale-mobile": SCALE_MOBILE,
151+
"--scale-desktop": SCALE_DESKTOP,
152+
boxShadow: `
153+
hsl(57, 19%, 35%, 0.73) 0px 1px 0.8px,
154+
hsl(57, 19%, 35%, 0.67) 0px 2.3px 1.9px -0.5px,
155+
hsl(57, 19%, 35%, 0.6) 0px 4.6px 3.8px -1px,
156+
hsl(57, 19%, 35%, 0.54) 0.1px 9px 7.4px -1.5px,
157+
hsl(57, 19%, 35%, 0.47) 0.1px 16.8px 13.9px -2px,
158+
hsl(57, 19%, 35%, 0.41) 0.2px 29.3px 24.2px -2.5px,
159+
hsl(57, 19%, 35%, 0.34) 0.3px 47.6px 39.3px -3px,
160+
hsl(57, 19%, 35%, 0.28) 0.5px 73px 60.2px -3.5px,
161+
hsl(57, 19%, 35%, 0.21) 0.7px 106.8px 88.1px -4px,
162+
hsl(57, 19%, 35%, 0.15) 0.9px 150px 123.8px -4.5px
163+
`,
164+
} as React.CSSProperties}
165+
>
166+
{/* Background glow animation */}
167+
<div
168+
className={`absolute inset-0 z-0 rounded-xl bg-yellow-100 blur-3xl pointer-events-none transition-all duration-700 will-change-opacity ${
169+
!isUnblurred ? "opacity-0 scale-90" : "opacity-50 scale-100"
170+
}`}
171+
aria-hidden="true"
172+
/>
173+
174+
{/* Chart Container */}
175+
<div className="relative max-w-fit w-full bg-white rounded-xl ring-1 ring-white/60 backdrop-blur-md transition-all duration-300 hover:shadow-[inset_0_0_40px_rgba(255,255,150,0.5)] will-change-shadow">
176+
{/* Radial cursor blur (desktop only) */}
177+
<div
178+
className="absolute inset-0 z-20 pointer-events-none rounded-xl will-change-[mask-image,webkit-mask-image]"
179+
style={{
180+
WebkitMaskImage: maskImage,
181+
maskImage: maskImage,
182+
WebkitBackdropFilter: `blur(${BLUR_INTENSITY})`,
183+
backdropFilter: `blur(${BLUR_INTENSITY})`,
184+
}}
185+
aria-hidden="true"
186+
/>
187+
188+
{/* Snellen Lines */}
189+
<SnellenLines isUnblurred={isUnblurred} />
11190
</div>
191+
</div>
12192

13-
<h1 className="mt-6 text-2xl font-semibold tracking-tight">
193+
{/* Error Content */}
194+
<div className="w-full max-w-xl text-center mt-8 md:mt-16">
195+
<div className="text-muted-foreground/70 font-mono text-sm tracking-widest uppercase">
196+
Error 503
197+
</div>
198+
<h1 className="mt-4 text-4xl font-bold tracking-tight text-balance sm:text-5xl">
14199
We couldn&rsquo;t load this page
15200
</h1>
16-
<p className="text-muted-foreground mt-3 text-sm leading-6 text-balance">
201+
<p className="text-muted-foreground mt-2 text-base leading-7 text-balance">
17202
Something went wrong while reaching our servers. This is usually a
18203
temporary network issue. Please try again, or come back in a minute.
19204
</p>
20205

21-
<div className="border-border/60 bg-card text-muted-foreground mt-8 rounded-lg border px-4 py-3 text-left font-mono text-xs">
22-
<div className="flex items-center justify-between">
23-
<span className="text-foreground/80">Request ID</span>
24-
<span>req_8f3c2d1a</span>
25-
</div>
26-
<div className="mt-1.5 flex items-center justify-between">
27-
<span className="text-foreground/80">Status</span>
28-
<span>Network error</span>
29-
</div>
30-
</div>
31-
32-
<div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
33-
<Button size="lg" className="w-full sm:w-auto">
206+
<div className="mt-6 flex flex-col items-center justify-center gap-3 sm:flex-row">
207+
<Button className="w-full sm:w-auto">
34208
<RotateCw data-icon="inline-start" />
35209
Try again
36210
</Button>
37-
<Button size="lg" variant="outline" className="w-full sm:w-auto">
211+
<Button variant="outline" className="w-full sm:w-auto">
38212
<ArrowLeft data-icon="inline-start" />
39213
Go back
40214
</Button>
41215
</div>
42216

43-
<p className="text-muted-foreground mt-8 text-xs">
44-
If this keeps happening, share the request ID with{" "}
217+
<p className="text-muted-foreground mt-10 inline-flex items-center gap-1.5 text-sm">
218+
<WifiOff className="size-4" />
219+
Still having issues?{" "}
45220
<a
46221
href="#"
47222
className="text-foreground underline-offset-4 hover:underline"
48223
>
49-
support
224+
Contact support
50225
</a>
51-
.
52226
</p>
53227
</div>
54-
</div>
228+
</main>
55229
);
56230
}

0 commit comments

Comments
 (0)