修复 lint 阻塞错误并恢复门禁
This commit is contained in:
@@ -148,9 +148,14 @@
|
||||
- 将 `tsc --noEmit` 纳入 CI 必跑;
|
||||
- 优先修复测试目录类型错误,确保类型门禁恢复。
|
||||
|
||||
### P2-4 Lint 存在阻塞错误(React hooks 新规则触发)
|
||||
### P2-4 Lint 存在阻塞错误(React hooks 新规则触发)【已完成】
|
||||
- 修复状态:✅ 已完成(2026-03-03)
|
||||
- 修复内容:
|
||||
- 修复 `GlobalUserBadge`、`RestaurantCard`、`SwipeableCard`、`PageTransition`、`useGeolocation` 的 hooks 规则 error;
|
||||
- 修复页面文案中的未转义引号(`react/no-unescaped-entities`);
|
||||
- `npm run lint` 已恢复为 0 error(仍有 warning,后续可持续清理)。
|
||||
- 证据:
|
||||
- `npm run lint`:10 error / 32 warning。
|
||||
- 修复后执行 `npm run lint`:`0 errors / 29 warnings`。
|
||||
- 代表性问题:
|
||||
- `src/components/SwipeableCard.tsx:81`(render 阶段注册副作用)
|
||||
- `src/components/GlobalUserBadge.tsx:23`(effect 内同步 setState)
|
||||
|
||||
@@ -339,7 +339,7 @@ export default function BlindboxLobbyPage() {
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.08 }}
|
||||
>
|
||||
平日蓄水,周末开奖。把所有"想做但一直没做"的事,交给命运来决定。
|
||||
平日蓄水,周末开奖。把所有"想做但一直没做"的事,交给命运来决定。
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ export default function LandingPage() {
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
别再说"随便"了。
|
||||
别再说"随便"了。
|
||||
<br />
|
||||
两个模式,覆盖你们所有的选择困难症。
|
||||
</motion.p>
|
||||
|
||||
@@ -14,19 +14,11 @@ const HIDDEN_PREFIXES = ["/profile"];
|
||||
export default function GlobalUserBadge() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [profile, setProfile] = useState<UserProfile | null>(() => getCachedProfile());
|
||||
const [showAuth, setShowAuth] = useState(false);
|
||||
const [theme, setTheme] = useState<Theme>("system");
|
||||
const [theme, setTheme] = useState<Theme>(() => getStoredTheme());
|
||||
const hidden = HIDDEN_PREFIXES.some((p) => pathname.startsWith(p));
|
||||
|
||||
useEffect(() => {
|
||||
setProfile(getCachedProfile());
|
||||
}, [pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
setTheme(getStoredTheme());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => setProfile(getCachedProfile());
|
||||
window.addEventListener("nowhatever_auth", handler);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useContext, useRef, type PropsWithChildren } from "react";
|
||||
import { useContext, useState, type PropsWithChildren } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
|
||||
@@ -11,7 +11,7 @@ import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.sha
|
||||
*/
|
||||
function FrozenRoute({ children }: PropsWithChildren) {
|
||||
const ctx = useContext(LayoutRouterContext);
|
||||
const frozen = useRef(ctx).current;
|
||||
const [frozen] = useState(ctx);
|
||||
return (
|
||||
<LayoutRouterContext.Provider value={frozen}>
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState, useEffect, useRef } from "react";
|
||||
import { useCallback, useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Star, MapPin, Clock, ExternalLink, Flame, Bookmark, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Restaurant } from "@/types";
|
||||
@@ -119,17 +119,6 @@ function ImageGallery({ images, name }: { images: string[]; name: string }) {
|
||||
|
||||
export default function RestaurantCard({ restaurant, likeCount = 0 }: RestaurantCardProps) {
|
||||
const [favorited, setFavorited] = useState(false);
|
||||
const [likeBounce, setLikeBounce] = useState(false);
|
||||
const prevLikeRef = useRef(likeCount);
|
||||
|
||||
useEffect(() => {
|
||||
if (likeCount > prevLikeRef.current) {
|
||||
setLikeBounce(true);
|
||||
const t = setTimeout(() => setLikeBounce(false), 600);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
prevLikeRef.current = likeCount;
|
||||
}, [likeCount]);
|
||||
|
||||
const images = restaurant.images?.filter(Boolean);
|
||||
const hasImage = images && images.length > 0;
|
||||
@@ -175,21 +164,18 @@ export default function RestaurantCard({ restaurant, likeCount = 0 }: Restaurant
|
||||
<AnimatePresence>
|
||||
{likeCount > 0 && (
|
||||
<motion.span
|
||||
key="like-badge"
|
||||
key={`like-badge-${likeCount}`}
|
||||
className="flex items-center gap-0.5 rounded-full bg-rose-500/90 px-2 py-0.5 text-xs font-semibold text-white shadow-sm backdrop-blur-sm"
|
||||
initial={{ opacity: 0, scale: 0.5, x: -8 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
scale: likeBounce ? [1, 1.3, 1] : 1,
|
||||
scale: [1, 1.3, 1],
|
||||
x: 0,
|
||||
}}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
transition={likeBounce
|
||||
? { scale: { duration: 0.4, ease: "easeInOut" }, default: { type: "spring", stiffness: 400, damping: 20 } }
|
||||
: { type: "spring", stiffness: 400, damping: 20 }
|
||||
}
|
||||
transition={{ scale: { duration: 0.4, ease: "easeInOut" }, default: { type: "spring", stiffness: 400, damping: 20 } }}
|
||||
>
|
||||
<Flame size={11} className={likeBounce ? "animate-pulse" : ""} />
|
||||
<Flame size={11} className="animate-pulse" />
|
||||
{likeCount} 人想去
|
||||
</motion.span>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useRef, useCallback, useEffect } from "react";
|
||||
import {
|
||||
motion,
|
||||
useMotionValue,
|
||||
@@ -64,7 +64,7 @@ export default function SwipeableCard({
|
||||
|
||||
const isSwiping = useRef(false);
|
||||
|
||||
const flyOut = (direction: SwipeDirection) => {
|
||||
const flyOut = useCallback((direction: SwipeDirection) => {
|
||||
if (isSwiping.current) return;
|
||||
isSwiping.current = true;
|
||||
const exit = getExitX();
|
||||
@@ -75,11 +75,12 @@ export default function SwipeableCard({
|
||||
damping: 40,
|
||||
onComplete: () => onSwipe(direction),
|
||||
});
|
||||
};
|
||||
}, [onSwipe, x]);
|
||||
|
||||
if (registerSwipe) {
|
||||
useEffect(() => {
|
||||
if (!registerSwipe) return;
|
||||
registerSwipe(flyOut);
|
||||
}
|
||||
}, [registerSwipe, flyOut]);
|
||||
|
||||
const handleDragEnd = (_: unknown, info: PanInfo) => {
|
||||
const offsetX = info.offset.x;
|
||||
|
||||
@@ -64,7 +64,10 @@ export function useGeolocation() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
locate();
|
||||
const timer = setTimeout(() => {
|
||||
void locate();
|
||||
}, 0);
|
||||
return () => clearTimeout(timer);
|
||||
}, [locate]);
|
||||
|
||||
return { status, coords, locationName, retry: locate };
|
||||
|
||||
Reference in New Issue
Block a user