fix: resolve all eslint errors (set-state-in-effect, nested components, no-explicit-any)

- Wrap synchronous setState calls in useEffect with startTransition to avoid cascading renders
- Convert nested SortIcon components to renderSortIcon helper functions
- Replace all `any` types with proper interfaces (OverviewData, TrendPoint, RankItem)
- Remove unused formatTokens import in logs page
- Add no-any rule to CLAUDE.md
This commit is contained in:
2026-04-07 15:19:10 +08:00
parent 8b91aa3e97
commit 20a3d399d9
7 changed files with 62 additions and 35 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState, useCallback, useRef } from "react";
import { useEffect, useState, useCallback, useRef, startTransition } from "react";
import { createPortal } from "react-dom";
import { motion } from "motion/react";
import { Users, Calendar, Hash, Zap, ArrowUpDown, ArrowDown, ArrowUp, HelpCircle } from "lucide-react";
@@ -22,7 +22,7 @@ function RatioTooltip({ text }: { text: string }) {
const iconRef = useRef<HTMLSpanElement>(null);
const [mounted, setMounted] = useState(false);
useEffect(() => { setMounted(true); }, []);
useEffect(() => { startTransition(() => setMounted(true)); }, []);
function handleEnter() {
if (iconRef.current) {
@@ -84,11 +84,11 @@ export default function AggregationPage() {
const [sortAsc, setSortAsc] = useState(false);
const fetchData = useCallback(async () => {
setLoading(true);
startTransition(() => setLoading(true));
const { start, end } = getEffectiveRange();
const res = await fetch(buildQuery("/api/aggregation", { start, end }));
setData(await res.json());
setLoading(false);
const json = await res.json();
startTransition(() => { setData(json); setLoading(false); });
}, [getEffectiveRange]);
useEffect(() => { fetchData(); }, [fetchData]);
@@ -112,12 +112,12 @@ export default function AggregationPage() {
else { setSortKey(key); setSortAsc(false); }
}
function SortIcon({ col }: { col: SortKey }) {
const renderSortIcon = (col: SortKey) => {
if (sortKey !== col) return <ArrowUpDown className="h-3 w-3" style={{ color: "var(--text-muted)", opacity: 0.5 }} />;
return sortAsc
? <ArrowUp className="h-3 w-3" style={{ color: "var(--accent)" }} />
: <ArrowDown className="h-3 w-3" style={{ color: "var(--accent)" }} />;
}
};
const sortHeaders: { key: SortKey; label: string }[] = [
{ key: "calls", label: t("th.calls") },
@@ -165,14 +165,14 @@ export default function AggregationPage() {
{sortHeaders.map(h => (
<th key={h.key} className="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider cursor-pointer select-none transition-colors"
style={{ color: "var(--text-muted)" }} onClick={() => handleSort(h.key)}>
<span className="inline-flex items-center gap-1">{h.label} <SortIcon col={h.key} /></span>
<span className="inline-flex items-center gap-1">{h.label} {renderSortIcon(h.key)}</span>
</th>
))}
<th className="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider cursor-pointer select-none transition-colors"
style={{ color: "var(--text-muted)" }} onClick={() => handleSort("ratio")}>
<span className="inline-flex items-center gap-1">
{t("agg.ratio")}
<SortIcon col="ratio" />
{renderSortIcon("ratio")}
<RatioTooltip text={t("agg.ratioTip")} />
</span>
</th>