Skip to content

Commit d532e16

Browse files
committed
fix(VisitorCounter): resolve hydration mismatch, lint errors, fake count
1 parent 76191e1 commit d532e16

1 file changed

Lines changed: 34 additions & 68 deletions

File tree

app/components/ui/VisitorCounter.tsx

Lines changed: 34 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12,115 +12,81 @@ import { GithubUsername } from '@/app/utils/config';
1212
import { Users } from 'lucide-react';
1313
import { useEffect, useRef, useState, useCallback } from 'react';
1414

15+
const SESSION_KEY = `visitor_${GithubUsername}`;
16+
17+
const getSessionData = (): { count: number } | null => {
18+
try {
19+
const data = sessionStorage.getItem(SESSION_KEY);
20+
return data ? JSON.parse(data) : null;
21+
} catch { return null; }
22+
};
23+
24+
const setSessionData = (count: number): void => {
25+
try {
26+
sessionStorage.setItem(SESSION_KEY, JSON.stringify({ count }));
27+
} catch { }
28+
};
29+
1530
const VisitorCounter = () => {
16-
const [count, setCount] = useState<number>(0);
31+
const [count, setCount] = useState<number | null>(null);
1732
const [isUpdating, setIsUpdating] = useState(false);
1833
const hasRun = useRef(false);
19-
2034
const counterId = GithubUsername;
21-
const SESSION_KEY = `visitor_${counterId}`;
22-
23-
/**
24-
* Session storage operations - faster than cookies
25-
*/
26-
const getSessionData = useCallback((): { count: number; fetched: boolean } | null => {
27-
if (typeof window === 'undefined') return null;
28-
try {
29-
const data = sessionStorage.getItem(SESSION_KEY);
30-
return data ? JSON.parse(data) : null;
31-
} catch {
32-
return null;
33-
}
34-
}, [SESSION_KEY]);
35-
36-
const setSessionData = useCallback((count: number, fetched: boolean = true): void => {
37-
if (typeof window === 'undefined') return;
38-
try {
39-
sessionStorage.setItem(SESSION_KEY, JSON.stringify({ count, fetched }));
40-
} catch {
41-
// Ignore errors
42-
}
43-
}, [SESSION_KEY]);
4435

4536
/**
4637
* Fast count fetch - API auto-increments, so we handle it properly
38+
* Note: This API is not my own and may break at any time. I am dependent on it.
4739
*/
4840
const fetchCount = useCallback(async (): Promise<void> => {
41+
// Set updating state inside async function
42+
setIsUpdating(true);
4943
try {
50-
const sessionData = getSessionData();
51-
52-
// If we already fetched this session, don't fetch again
53-
if (sessionData?.fetched) {
54-
setCount(sessionData.count);
55-
setIsUpdating(false);
56-
return;
57-
}
58-
59-
// Fetch with timeout
6044
const controller = new AbortController();
61-
const timeoutId = setTimeout(() => controller.abort(), 2000);
45+
const timeoutId = setTimeout(() => controller.abort(), 3000);
6246

63-
// Note: This API is not my own and may break at any time. I am dependent on it.
6447
const response = await fetch(`https://counterpro.vercel.app/api/count/id/${counterId}`, {
6548
signal: controller.signal,
6649
headers: { 'Accept': 'application/json' }
6750
});
68-
6951
clearTimeout(timeoutId);
7052

7153
if (response.ok) {
7254
const data = await response.json();
7355
const apiCount = data?.count || 0;
74-
7556
// API auto-incremented, so this is our new count
7657
setCount(apiCount);
77-
setSessionData(apiCount, true);
78-
} else {
79-
// Fallback to cached or estimated
80-
const fallbackCount = sessionData?.count || Math.floor(Math.random() * 100) + 50;
81-
setCount(fallbackCount);
58+
setSessionData(apiCount);
8259
}
8360
} catch {
84-
// Use cached data or show estimated count
85-
const sessionData = getSessionData();
86-
const fallbackCount = sessionData?.count || Math.floor(Math.random() * 100) + 50;
87-
setCount(fallbackCount);
61+
// API failed — keep showing cached value already set from sessionStorage
8862
} finally {
8963
setIsUpdating(false);
9064
}
91-
}, [counterId, getSessionData, setSessionData]);
65+
}, [counterId]);
9266

9367
useEffect(() => {
9468
if (hasRun.current) return;
9569
hasRun.current = true;
9670

97-
setTimeout(() => {
98-
// IMMEDIATE: Show cached count from session
99-
const sessionData = getSessionData();
100-
if (sessionData) {
101-
setCount(sessionData.count);
102-
103-
// If we already fetched this session, don't fetch again
104-
if (sessionData.fetched) return;
105-
} else {
106-
// Show estimated count for new sessions
107-
setCount(Math.floor(Math.random() * 100) + 50);
108-
}
71+
const cached = getSessionData();
72+
if (cached) {
73+
// Already visited this session — show cached count, skip API call
74+
setTimeout(() => setCount(cached.count), 0);
75+
return;
76+
}
10977

110-
// BACKGROUND: Fetch real count if not already done this session
111-
setIsUpdating(true);
112-
fetchCount();
113-
}, 0);
114-
}, [counterId, fetchCount, getSessionData]);
78+
const id = setTimeout(fetchCount, 0);
79+
return () => clearTimeout(id);
80+
}, [fetchCount]);
11581

11682
return (
11783
<div
11884
className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-300 transition-all duration-200 scale-[0.95]"
119-
aria-label={`${count.toLocaleString()} total visitors`}
85+
aria-label={count !== null ? `${count.toLocaleString()} total visitors` : 'Loading visitor count'}
12086
>
12187
<Users className={`text-cyan-400 w-4 h-4 shrink-0 ${isUpdating ? 'animate-pulse' : ''}`} />
12288
<span className="tabular-nums">
123-
{count.toLocaleString()} visitors
89+
{count !== null ? `${count.toLocaleString()} visitors` : '... visitors'}
12490
</span>
12591
</div>
12692
);

0 commit comments

Comments
 (0)