Skip to content

Commit d105d64

Browse files
committed
refactor: fix react-hooks lint errors
- use-media-query: rewrite with useSyncExternalStore (drops effect-setState) - typewriter-text: derive reduceMotion text in render, skip effect - mascot: move Math.random sparkle rotate to creation time - mascot/hero: drop unused 'priority' prop
1 parent 81b9ec2 commit d105d64

4 files changed

Lines changed: 40 additions & 36 deletions

File tree

components/site/hero.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function Hero() {
7171
>
7272
<motion.div
7373
className="flex flex-col gap-6 text-center lg:text-left"
74-
style={reduceMotion ? undefined : { y: textParallaxY }}
74+
style={reduceMotion || !isDesktop ? undefined : { y: textParallaxY }}
7575
>
7676
<motion.div
7777
variants={itemFromLeft}
@@ -100,7 +100,7 @@ export function Hero() {
100100
variants={itemFromRight}
101101
className="flex justify-center lg:hidden"
102102
>
103-
<Mascot size={220} priority />
103+
<Mascot size={220} />
104104
</motion.div>
105105

106106
<motion.p
@@ -128,8 +128,8 @@ export function Hero() {
128128
variants={itemFromRight}
129129
className="hidden justify-end lg:flex"
130130
>
131-
<motion.div style={reduceMotion ? undefined : { y: mascotParallaxY }}>
132-
<Mascot size={420} priority />
131+
<motion.div style={reduceMotion || !isDesktop ? undefined : { y: mascotParallaxY }}>
132+
<Mascot size={420} />
133133
</motion.div>
134134
</motion.div>
135135
</motion.div>

components/site/mascot.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import { useRef, useState } from "react";
44
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
55
import { cn } from "@/lib/cn";
6+
import { useMediaQuery } from "@/lib/use-media-query";
67

78
interface MascotProps {
89
className?: string;
910
size?: number;
10-
priority?: boolean;
1111
}
1212

1313
interface Sparkle {
@@ -16,14 +16,17 @@ interface Sparkle {
1616
startDistance: number;
1717
endDistance: number;
1818
scale: number;
19+
rotate: number;
1920
}
2021

2122
const SPARKLE_COUNT = 6;
2223
const SPARKLE_LIFE = 900;
2324

24-
export function Mascot({ className, size = 360, priority: _priority = false }: MascotProps) {
25+
export function Mascot({ className, size = 360 }: MascotProps) {
2526
const videoRef = useRef<HTMLVideoElement>(null);
2627
const reduceMotion = useReducedMotion();
28+
const isDesktop = useMediaQuery("(min-width: 1024px)");
29+
const idleLoops = isDesktop && !reduceMotion;
2730
const [sparkles, setSparkles] = useState<Sparkle[]>([]);
2831
const idCounter = useRef(0);
2932

@@ -47,6 +50,7 @@ export function Mascot({ className, size = 360, priority: _priority = false }: M
4750
startDistance: startRadius,
4851
endDistance: startRadius + size * 0.18 + Math.random() * (size * 0.1),
4952
scale: 0.9 + Math.random() * 0.6,
53+
rotate: (Math.random() - 0.5) * 60,
5054
};
5155
});
5256
setSparkles((prev) => [...prev, ...newOnes]);
@@ -59,29 +63,27 @@ export function Mascot({ className, size = 360, priority: _priority = false }: M
5963
<motion.div
6064
className={cn("group relative shrink-0", className)}
6165
style={{ width: size, height: size }}
62-
animate={
63-
reduceMotion ? { scale: 1 } : { scale: [1, 1.06, 1] }
64-
}
66+
animate={idleLoops ? { scale: [1, 1.06, 1] } : { scale: 1 }}
6567
transition={
66-
reduceMotion
67-
? { duration: 0 }
68-
: { duration: 3.8, repeat: Infinity, ease: "easeInOut" }
68+
idleLoops
69+
? { duration: 3.8, repeat: Infinity, ease: "easeInOut" }
70+
: { duration: 0 }
6971
}
7072
>
71-
{/* Idle halo — always pulses softly */}
73+
{/* Idle halo — pulses softly on desktop, static on mobile */}
7274
<motion.span
7375
aria-hidden
7476
className="pointer-events-none absolute inset-0 rounded-full bg-[var(--color-primary)] blur-3xl"
7577
initial={{ opacity: 0.06, scale: 1 }}
7678
animate={
77-
reduceMotion
78-
? { opacity: 0.06, scale: 1 }
79-
: { opacity: [0.04, 0.14, 0.04], scale: [1, 1.32, 1] }
79+
idleLoops
80+
? { opacity: [0.04, 0.14, 0.04], scale: [1, 1.32, 1] }
81+
: { opacity: 0.06, scale: 1 }
8082
}
8183
transition={
82-
reduceMotion
83-
? { duration: 0 }
84-
: { duration: 3.8, repeat: Infinity, ease: "easeInOut" }
84+
idleLoops
85+
? { duration: 3.8, repeat: Infinity, ease: "easeInOut" }
86+
: { duration: 0 }
8587
}
8688
/>
8789
{/* Hover halo — gentle bloom on top */}
@@ -143,7 +145,7 @@ export function Mascot({ className, size = 360, priority: _priority = false }: M
143145
y: endY,
144146
scale: [0, sparkle.scale, 0],
145147
opacity: [0, 1, 0],
146-
rotate: (Math.random() - 0.5) * 60,
148+
rotate: sparkle.rotate,
147149
}}
148150
transition={{ duration: SPARKLE_LIFE / 1000, ease: "easeOut" }}
149151
>

components/site/typewriter-text.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,7 @@ export function TypewriterText({
3232
const [mode, setMode] = useState<Mode>("typing");
3333

3434
useEffect(() => {
35-
if (!inView) return;
36-
if (reduceMotion) {
37-
setDisplayed(texts[0] ?? "");
38-
return;
39-
}
35+
if (!inView || reduceMotion) return;
4036
const currentText = texts[textIdx];
4137
if (!currentText) return;
4238

@@ -81,9 +77,11 @@ export function TypewriterText({
8177
pauseAfterErase,
8278
]);
8379

80+
const visibleText = reduceMotion ? (texts[0] ?? "") : displayed;
81+
8482
return (
8583
<span ref={ref} className={className} aria-label={texts[0]}>
86-
<span aria-hidden>{displayed}</span>
84+
<span aria-hidden>{visibleText}</span>
8785
{cursor && (
8886
<span
8987
aria-hidden

lib/use-media-query.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
"use client";
22

3-
import { useEffect, useState } from "react";
3+
import { useCallback, useSyncExternalStore } from "react";
44

55
export function useMediaQuery(query: string) {
6-
const [matches, setMatches] = useState(false);
6+
const subscribe = useCallback(
7+
(notify: () => void) => {
8+
const mq = window.matchMedia(query);
9+
mq.addEventListener("change", notify);
10+
return () => mq.removeEventListener("change", notify);
11+
},
12+
[query],
13+
);
714

8-
useEffect(() => {
9-
const mq = window.matchMedia(query);
10-
setMatches(mq.matches);
11-
const handler = (event: MediaQueryListEvent) => setMatches(event.matches);
12-
mq.addEventListener("change", handler);
13-
return () => mq.removeEventListener("change", handler);
14-
}, [query]);
15+
const getSnapshot = useCallback(
16+
() => window.matchMedia(query).matches,
17+
[query],
18+
);
1519

16-
return matches;
20+
return useSyncExternalStore(subscribe, getSnapshot, () => false);
1721
}

0 commit comments

Comments
 (0)