Skip to content

Commit c2ca0f4

Browse files
fix(web): resolve all 7 ESLint errors for CI pipeline
- Extract useTheme hook and ThemeContext to separate files (react-refresh/only-export-components) - Add async/await with cleanup in CardPage and ProfilePage effects (react-hooks/set-state-in-effect) - Remove redundant mounted state in ProfilePage - Replace 'any' with vi.mocked() and PublicProfile type in tests (no-explicit-any) - Revert out-of-scope platforms.ts change
1 parent 777b9be commit c2ca0f4

8 files changed

Lines changed: 82 additions & 53 deletions

File tree

apps/web/src/components/Navbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Link } from 'react-router-dom';
2-
import { useTheme } from '../lib/theme';
2+
import { useTheme } from '../lib/useTheme';
33
import './Navbar.css';
44

55
export default function Navbar() {

apps/web/src/lib/ThemeContext.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createContext } from 'react';
2+
3+
type Theme = 'light' | 'dark';
4+
5+
export interface ThemeContextValue {
6+
theme: Theme;
7+
toggleTheme: () => void;
8+
}
9+
10+
export const ThemeContext = createContext<ThemeContextValue | null>(null);

apps/web/src/lib/theme.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
1+
import { useEffect, useState, type ReactNode } from 'react';
2+
import { ThemeContext } from './ThemeContext';
3+
import type { ThemeContextValue } from './ThemeContext';
24

35
type Theme = 'light' | 'dark';
46

5-
interface ThemeContextValue {
6-
theme: Theme;
7-
toggleTheme: () => void;
8-
}
9-
10-
const ThemeContext = createContext<ThemeContextValue | null>(null);
11-
127
function getInitialTheme(): Theme {
138
if (typeof window === 'undefined') return 'dark';
149

@@ -33,15 +28,11 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
3328

3429
const toggleTheme = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark'));
3530

31+
const value: ThemeContextValue = { theme, toggleTheme };
32+
3633
return (
37-
<ThemeContext.Provider value={{ theme, toggleTheme }}>
34+
<ThemeContext.Provider value={value}>
3835
{children}
3936
</ThemeContext.Provider>
4037
);
4138
}
42-
43-
export function useTheme(): ThemeContextValue {
44-
const ctx = useContext(ThemeContext);
45-
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
46-
return ctx;
47-
}

apps/web/src/lib/useTheme.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useContext } from 'react';
2+
import { ThemeContext } from './ThemeContext';
3+
import type { ThemeContextValue } from './ThemeContext';
4+
5+
export function useTheme(): ThemeContextValue {
6+
const ctx = useContext(ThemeContext);
7+
if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
8+
return ctx;
9+
}

apps/web/src/pages/CardPage.tsx

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,31 @@ export default function CardPage() {
2424

2525
useEffect(() => {
2626
if (!id) return;
27-
setLoading(true);
28-
apiFetch<PublicCard>(`/api/u/card/${id}`)
29-
.then((data) => {
30-
setCard(data);
31-
setError(null);
32-
})
33-
.catch(() => {
34-
setCard(null);
35-
setError('Card not found');
36-
})
37-
.finally(() => setLoading(false));
27+
28+
let cancelled = false;
29+
30+
const fetchCard = async () => {
31+
try {
32+
const data = await apiFetch<PublicCard>(`/api/u/card/${id}`);
33+
if (!cancelled) {
34+
setCard(data);
35+
setError(null);
36+
}
37+
} catch {
38+
if (!cancelled) {
39+
setCard(null);
40+
setError('Card not found');
41+
}
42+
} finally {
43+
if (!cancelled) setLoading(false);
44+
}
45+
};
46+
47+
fetchCard();
48+
49+
return () => {
50+
cancelled = true;
51+
};
3852
}, [id]);
3953

4054
// Update document title

apps/web/src/pages/ProfilePage.test.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
33
import { MemoryRouter, Routes, Route } from 'react-router-dom';
44
import ProfilePage from './ProfilePage';
55
import { apiFetch } from '../lib/api';
6+
import type { PublicProfile } from '../shared';
67

78
vi.mock('../lib/api', () => ({
89
apiFetch: vi.fn(),
@@ -24,14 +25,14 @@ describe('ProfilePage', () => {
2425
};
2526

2627
it('renders loading state initially', () => {
27-
(apiFetch as any).mockImplementation(() => new Promise(() => {}));
28+
vi.mocked(apiFetch).mockImplementation(() => new Promise(() => {}));
2829
const { container } = renderWithRouter('testuser');
2930

3031
expect(container.querySelector('.loading-card')).toBeInTheDocument();
3132
});
3233

3334
it('renders profile data successfully', async () => {
34-
const mockProfile = {
35+
const mockProfile: PublicProfile = {
3536
id: '123',
3637
userId: '456',
3738
username: 'testuser',
@@ -55,7 +56,7 @@ describe('ProfilePage', () => {
5556
updatedAt: new Date().toISOString(),
5657
};
5758

58-
(apiFetch as any).mockResolvedValue(mockProfile);
59+
vi.mocked(apiFetch).mockResolvedValue(mockProfile);
5960

6061
renderWithRouter('testuser');
6162

@@ -70,7 +71,7 @@ describe('ProfilePage', () => {
7071
});
7172

7273
it('renders 404 flow when user is not found', async () => {
73-
(apiFetch as any).mockRejectedValue(new Error('User not found'));
74+
vi.mocked(apiFetch).mockRejectedValue(new Error('User not found'));
7475

7576
renderWithRouter('unknownuser');
7677

apps/web/src/pages/ProfilePage.tsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,36 @@ export default function ProfilePage() {
1818
const [profile, setProfile] = useState<PublicProfile | null>(null);
1919
const [error, setError] = useState<string | null>(null);
2020
const [loading, setLoading] = useState(true);
21-
const [mounted, setMounted] = useState(false);
2221
const [copyMessage, setCopyMessage] = useState('');
2322
const [copyStatus, setCopyStatus] = useState<'success' | 'error'>('success');
2423

25-
useEffect(() => {
26-
setMounted(true);
27-
}, []);
28-
2924
useEffect(() => {
3025
if (!username) return;
31-
setLoading(true);
32-
apiFetch<PublicProfile>(`/api/u/${username}?source=web`)
33-
.then((data) => {
34-
setProfile(data);
35-
setError(null);
36-
})
37-
.catch(() => {
38-
setProfile(null);
39-
setError('User not found');
40-
})
41-
.finally(() => setLoading(false));
26+
27+
let cancelled = false;
28+
29+
const fetchProfile = async () => {
30+
try {
31+
const data = await apiFetch<PublicProfile>(`/api/u/${username}?source=web`);
32+
if (!cancelled) {
33+
setProfile(data);
34+
setError(null);
35+
}
36+
} catch {
37+
if (!cancelled) {
38+
setProfile(null);
39+
setError('User not found');
40+
}
41+
} finally {
42+
if (!cancelled) setLoading(false);
43+
}
44+
};
45+
46+
fetchProfile();
47+
48+
return () => {
49+
cancelled = true;
50+
};
4251
}, [username]);
4352

4453
async function copyProfileUrl() {
@@ -107,7 +116,7 @@ export default function ProfilePage() {
107116
className="bg-gradient"
108117
style={{ '--accent': profile.accentColor || '#6366f1' } as React.CSSProperties}
109118
/>
110-
<main className={`profile-container ${mounted ? 'loaded' : ''}`}>
119+
<main className="profile-container loaded">
111120
<div
112121
className="profile-card glass"
113122
style={{ '--accent': profile.accentColor } as React.CSSProperties}

packages/shared/src/platforms.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,3 @@ export function getDeepLinkUrl(platformId: string, username: string): string | n
292292
if (!platform?.deepLinkPattern) return null;
293293
return platform.deepLinkPattern.replace(/{username}/g, username);
294294
}
295-
296-
/** Check if a platform ID is supported */
297-
export function isSupportedPlatform(id: string): boolean {
298-
return id in PLATFORMS;
299-
}

0 commit comments

Comments
 (0)