From 6d17615750f1b7a1abda94b1ab20d25844eaccff Mon Sep 17 00:00:00 2001 From: Marco Pasqualetti Date: Thu, 29 Jan 2026 13:49:04 +0100 Subject: [PATCH] feat(frontend): adds `typescript-eslint` --- apps/frontend/src/api/user.ts | 15 +- apps/frontend/src/axios-override.ts | 21 +- .../src/components/Card/SvgInline.tsx | 15 +- .../Home/LanguagesLayoutSection.tsx | 2 +- .../src/components/Home/NumericSection.tsx | 8 +- .../frontend/src/components/Home/Progress.tsx | 12 +- .../src/components/Home/StatsRankSection.tsx | 2 +- .../src/components/Home/TextSection.tsx | 14 +- .../components/Home/WakatimeLayoutSection.tsx | 2 +- apps/frontend/src/constants.ts | 6 +- apps/frontend/src/dotenv-browser-stub.ts | 2 +- apps/frontend/src/globals.d.ts | 14 +- apps/frontend/src/mock-http.ts | 4 +- apps/frontend/src/pages/App/AppTrends.tsx | 14 +- apps/frontend/src/pages/Home/Home.tsx | 72 +++---- .../src/pages/Home/stages/Customize.tsx | 8 +- .../src/pages/Home/stages/Display.tsx | 20 +- apps/frontend/src/pages/Home/stages/Login.tsx | 76 +++---- .../src/pages/Home/stages/SelectCard.tsx | 4 +- apps/frontend/src/pages/Home/stages/Theme.tsx | 7 +- .../src/redux/selectors/userSelectors.ts | 9 +- apps/frontend/src/redux/slices/user.ts | 12 +- apps/frontend/src/wakatime-override.ts | 7 +- apps/frontend/tsconfig.json | 3 +- apps/frontend/vite.config.ts | 10 +- eslint.config.js | 43 +++- package.json | 3 +- pnpm-lock.yaml | 187 ++++++++++++++++++ 28 files changed, 434 insertions(+), 158 deletions(-) diff --git a/apps/frontend/src/api/user.ts b/apps/frontend/src/api/user.ts index 1eda2ae8..4fe73bb2 100644 --- a/apps/frontend/src/api/user.ts +++ b/apps/frontend/src/api/user.ts @@ -6,10 +6,12 @@ const authenticate = async ( code: string, privateAccess: boolean, userKey: string, -) => { +): Promise => { try { const fullUrl = `https://${HOST}/api/authenticate?code=${code}&private_access=${privateAccess}&user_key=${userKey}`; - const result = await axios.post(fullUrl); + const result = await axios.post<{ userId: string; needDowngrade: boolean }>( + fullUrl, + ); const { userId, needDowngrade } = result.data; if (needDowngrade) { console.info( @@ -24,12 +26,17 @@ const authenticate = async ( } }; +interface UserMetaDataResponse { + token: string; + privateAccess: string; +} + const getUserMetadata = async ( userKey: string, -): Promise => { +): Promise => { try { const fullUrl = `https://${HOST}/api/user-access?user_key=${userKey}`; - const result = await axios.get(fullUrl); + const result = await axios.get(fullUrl); return result.data; } catch (error) { console.error(error); diff --git a/apps/frontend/src/axios-override.ts b/apps/frontend/src/axios-override.ts index d98d095f..5282e76e 100644 --- a/apps/frontend/src/axios-override.ts +++ b/apps/frontend/src/axios-override.ts @@ -29,7 +29,7 @@ axios.get = cachedAxios.get.bind(cachedAxios); axios.post = cachedAxios.post.bind(cachedAxios); export function clearAxiosCache(): void { - cachedAxios.storage.clear?.(); + void cachedAxios.storage.clear?.(); } function createMockResponse( @@ -54,7 +54,7 @@ function createMockResponse( } // store shouldMock outside React context so the interceptor can access it -let shouldMock: boolean = false; +let shouldMock = false; export function setShouldMock(newShouldMock: boolean): void { shouldMock = newShouldMock; @@ -68,7 +68,17 @@ axios.defaults.adapter = async (config) => { return defaultAdapter(config); } - const params = config.data ? JSON.parse(config.data) : {}; + interface Params { + query?: string; + variables?: { + login: string; + repo: string; + gistName: string; + }; + } + const params = ( + config.data ? JSON.parse(config.data as string) : {} + ) as Params; if ( config.url === "https://api.github.com/graphql" && @@ -103,8 +113,9 @@ axios.defaults.adapter = async (config) => { if ( config.url === "https://api.github.com/graphql" && params.query?.includes("fragment RepoInfo on Repository {") && - params.variables?.login === "anuraghazra" && - params.variables?.repo === "github-readme-stats" + params.variables && + params.variables.login === "anuraghazra" && + params.variables.repo === "github-readme-stats" ) { return createMockResponse(repository, config); } diff --git a/apps/frontend/src/components/Card/SvgInline.tsx b/apps/frontend/src/components/Card/SvgInline.tsx index 3b1231ea..3e106b69 100644 --- a/apps/frontend/src/components/Card/SvgInline.tsx +++ b/apps/frontend/src/components/Card/SvgInline.tsx @@ -21,6 +21,9 @@ interface SvgInlineProps { forceLoading?: boolean; } +/** + * + */ export function SvgInline(props: SvgInlineProps): JSX.Element { const { url, @@ -30,7 +33,7 @@ export function SvgInline(props: SvgInlineProps): JSX.Element { forceLoading = false, } = props; - const [svg, setSvg] = useState(null); + const [svg, setSvg] = useState(null); const [loaded, setLoaded] = useState(false); const containerRef = useRef(null); const userToken = useUserToken(); @@ -49,7 +52,7 @@ export function SvgInline(props: SvgInlineProps): JSX.Element { setLoaded(false); - let body; + let body: string; let status; if (isAuthenticated && (!userToken || userToken === "placeholderPAT")) { @@ -58,7 +61,7 @@ export function SvgInline(props: SvgInlineProps): JSX.Element { } if (stage === 4 && !isAuthenticated) { - let res = await axios.get(url); + const res = await axios.get(url); body = res.data; status = res.status; } else { @@ -67,8 +70,10 @@ export function SvgInline(props: SvgInlineProps): JSX.Element { url, }); const res = createMockResponse(); + // will be solved by npm package + // eslint-disable-next-line @typescript-eslint/no-unsafe-call await router(req, res); - body = res._getBody(); + body = res._getBody() as string; status = res._getStatusCode(); } @@ -83,7 +88,7 @@ export function SvgInline(props: SvgInlineProps): JSX.Element { setSvg(body); setLoaded(true); }; - loadSvg(); + void loadSvg(); return () => { isCurrent = false; diff --git a/apps/frontend/src/components/Home/LanguagesLayoutSection.tsx b/apps/frontend/src/components/Home/LanguagesLayoutSection.tsx index 169e9760..be1d57f2 100644 --- a/apps/frontend/src/components/Home/LanguagesLayoutSection.tsx +++ b/apps/frontend/src/components/Home/LanguagesLayoutSection.tsx @@ -46,7 +46,7 @@ const options: Array = [ ]; interface LanguagesLayoutSectionProps { - selectedLanguageLayoutOption: SelectOption; + selectedLanguageLayoutOption: SelectOption | undefined; onLanguageLayoutOptionChange: (option: SelectOption) => void; } diff --git a/apps/frontend/src/components/Home/NumericSection.tsx b/apps/frontend/src/components/Home/NumericSection.tsx index be42dd5c..b0e1bce4 100644 --- a/apps/frontend/src/components/Home/NumericSection.tsx +++ b/apps/frontend/src/components/Home/NumericSection.tsx @@ -38,7 +38,7 @@ export function NumericSection({ return undefined; } - debounceTimeout.current = setTimeout(() => { + debounceTimeout.current = window.setTimeout(() => { const maybeNumber = internalValue && parseInt(internalValue, 10); if (typeof maybeNumber !== "number" || Number.isNaN(maybeNumber)) { onValueChange(undefined); @@ -50,7 +50,7 @@ export function NumericSection({ return () => { clearTimeout(debounceTimeout.current as number); }; - }, [internalValue]); + }, [internalValue, onValueChange, value]); useEffect(() => { setInternalValue(value?.toString()); @@ -63,7 +63,9 @@ export function NumericSection({ type="number" className="border border-gray-300 rounded px-2 py-1 mt-2 w-1/4" value={internalValue ?? ""} - onChange={(e) => setInternalValue(e.target.value)} + onChange={(e) => { + setInternalValue(e.target.value); + }} min={min} max={max} step={step} diff --git a/apps/frontend/src/components/Home/Progress.tsx b/apps/frontend/src/components/Home/Progress.tsx index e85f3457..f296c333 100644 --- a/apps/frontend/src/components/Home/Progress.tsx +++ b/apps/frontend/src/components/Home/Progress.tsx @@ -75,11 +75,13 @@ export function ProgressBar({ return ( = index} isActive={currItemIndex === index} - onClick={() => onItemClick(index)} + onClick={() => { + onItemClick(index); + }} /> ); })} @@ -89,9 +91,9 @@ export function ProgressBar({ "text-gray-400 cursor-not-allowed": rightDisabled, "text-gray-700 cursor-pointer": !rightDisabled, })} - onClick={() => - onItemClick(Math.min(currItemIndex + 1, items.length - 1)) - } + onClick={() => { + onItemClick(Math.min(currItemIndex + 1, items.length - 1)); + }} /> ); diff --git a/apps/frontend/src/components/Home/StatsRankSection.tsx b/apps/frontend/src/components/Home/StatsRankSection.tsx index 464d5b03..acb06db0 100644 --- a/apps/frontend/src/components/Home/StatsRankSection.tsx +++ b/apps/frontend/src/components/Home/StatsRankSection.tsx @@ -20,7 +20,7 @@ const options: Array = [ ]; interface StatsRankSectionProps { - selectedOption: SelectOption; + selectedOption: SelectOption | undefined; onOptionChange: (option: SelectOption) => void; } diff --git a/apps/frontend/src/components/Home/TextSection.tsx b/apps/frontend/src/components/Home/TextSection.tsx index 2692c753..8a04caa8 100644 --- a/apps/frontend/src/components/Home/TextSection.tsx +++ b/apps/frontend/src/components/Home/TextSection.tsx @@ -35,17 +35,19 @@ export function TextSection({ useEffect(() => { // Debounce onValueChange if (debounceTimeout.current) { - clearTimeout(debounceTimeout.current); + window.clearTimeout(debounceTimeout.current); } if (internalValue === value) { return undefined; } - debounceTimeout.current = setTimeout(() => { + debounceTimeout.current = window.setTimeout(() => { onValueChange(internalValue); }, 700); // return cleanup function: - return () => clearTimeout(debounceTimeout.current as number); - }, [internalValue]); + return () => { + window.clearTimeout(debounceTimeout.current as number); + }; + }, [internalValue, onValueChange, value]); return (
@@ -57,7 +59,9 @@ export function TextSection({ { "cursor-not-allowed": disabled }, )} value={internalValue} - onChange={(e) => setInternalValue(e.target.value)} + onChange={(e) => { + setInternalValue(e.target.value); + }} disabled={disabled} placeholder={placeholder} onPaste={onPaste} diff --git a/apps/frontend/src/components/Home/WakatimeLayoutSection.tsx b/apps/frontend/src/components/Home/WakatimeLayoutSection.tsx index 2b46437f..33c29e8b 100644 --- a/apps/frontend/src/components/Home/WakatimeLayoutSection.tsx +++ b/apps/frontend/src/components/Home/WakatimeLayoutSection.tsx @@ -28,7 +28,7 @@ const options: Array = [ ]; interface WakatimeLayoutSectionProps { - selectedOption: SelectOption; + selectedOption: SelectOption | undefined; onOptionChange: (option: SelectOption) => void; } diff --git a/apps/frontend/src/constants.ts b/apps/frontend/src/constants.ts index 30858d88..822be026 100644 --- a/apps/frontend/src/constants.ts +++ b/apps/frontend/src/constants.ts @@ -1,6 +1,6 @@ -const PROD = false; +const PROD = false as boolean; -export const USE_LOGGER = true; +export const USE_LOGGER = true as boolean; export const CLIENT_ID = "Ov23lilAc5biyyRY0K1u"; @@ -23,4 +23,4 @@ window.process = { FETCH_MULTI_PAGE_STARS: "10", PAT_1: "placeholderPAT", // so the backend's retryer.js sees there is 1 PAT and sets `RETRIES` accordingly }, -}; +} as (typeof window)["process"]; diff --git a/apps/frontend/src/dotenv-browser-stub.ts b/apps/frontend/src/dotenv-browser-stub.ts index 7f814acd..5abc676a 100644 --- a/apps/frontend/src/dotenv-browser-stub.ts +++ b/apps/frontend/src/dotenv-browser-stub.ts @@ -1,4 +1,4 @@ // Safe browser stub for dotenv -export function config(): {} { +export function config(): { parsed: Record } { return { parsed: {} }; } diff --git a/apps/frontend/src/globals.d.ts b/apps/frontend/src/globals.d.ts index 9ec6c3f3..29d02c17 100644 --- a/apps/frontend/src/globals.d.ts +++ b/apps/frontend/src/globals.d.ts @@ -1,12 +1,14 @@ declare global { - interface Window { - process: { - env: { - FETCH_MULTI_PAGE_STARS: string | undefined; - PAT_1: string | undefined; - }; + interface CustomProcess { + env: { + FETCH_MULTI_PAGE_STARS: string | undefined; + PAT_1: string | undefined; }; } + + interface Window { + process?: CustomProcess; + } } export {}; diff --git a/apps/frontend/src/mock-http.ts b/apps/frontend/src/mock-http.ts index 2368c09a..d62af77f 100644 --- a/apps/frontend/src/mock-http.ts +++ b/apps/frontend/src/mock-http.ts @@ -52,9 +52,9 @@ interface CreateMockResponseResult { } export function createMockResponse(): CreateMockResponseResult { - let statusCode = 200; + const statusCode = 200; const headers: HeaderMap = {}; - let chunks: Array = []; + const chunks: Array = []; const res: CreateMockResponseResult = { statusCode, diff --git a/apps/frontend/src/pages/App/AppTrends.tsx b/apps/frontend/src/pages/App/AppTrends.tsx index f1d0917f..8ad29e26 100644 --- a/apps/frontend/src/pages/App/AppTrends.tsx +++ b/apps/frontend/src/pages/App/AppTrends.tsx @@ -25,12 +25,16 @@ const toMessage = ( if (typeof input === "string") { return input; } - if ("reason" in input && input.reason?.message) { - return input.reason.message; + type MaybeErrorReason = { message: string } | null; + const reason = ("reason" in input ? input.reason : null) as MaybeErrorReason; + if (reason?.message === "string") { + return reason.message as string; } + if ("message" in input && input.message) { return input.message; } + try { return JSON.stringify(input); } catch { @@ -80,7 +84,7 @@ export function AppTrends() { if (isAuthenticated && stage === 0) { setStage(1); } - }, [isAuthenticated]); + }, [isAuthenticated, stage]); useEffect(() => { async function getPrivateAccess() { @@ -93,8 +97,8 @@ export function AppTrends() { } } } - getPrivateAccess(); - }, [userKey]); + void getPrivateAccess(); + }, [dispatch, userKey]); return (
diff --git a/apps/frontend/src/pages/Home/Home.tsx b/apps/frontend/src/pages/Home/Home.tsx index e6a9408e..f0495a3f 100644 --- a/apps/frontend/src/pages/Home/Home.tsx +++ b/apps/frontend/src/pages/Home/Home.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import type { JSX } from "react"; import { useDispatch } from "react-redux"; import BounceLoader from "react-spinners/BounceLoader"; @@ -6,7 +6,7 @@ import axios from "axios"; import { v4 as uuidv4 } from "uuid"; import { authenticate } from "../../api/user"; -import { login as _login } from "../../redux/slices/user"; +import { login } from "../../redux/slices/user"; import { HOST, DEMO_USER, @@ -46,12 +46,8 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element { const dispatch = useDispatch(); - const login = (newUserId: string, userKey: string) => { - dispatch(_login({ userId: newUserId, userKey })); - }; - // for stage two - const [selectedUserId, setSelectedUserId] = useState(userId); + const [selectedUserId, setSelectedUserId] = useState(userId); const [repo, setRepo] = useState(DEMO_REPO); const [gist, setGist] = useState(DEMO_GIST); const [wakatimeUser, setWakatimeUser] = useState(DEMO_WAKATIME_USER); @@ -230,38 +226,34 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element { // for stage five const [gistUrl, setGistUrl] = useState(""); - let guestHint; - switch (selectedCard) { - case CardType.STATS: - case CardType.TOP_LANGS: - guestHint = `username "${DEMO_USER}"`; - break; - case CardType.PIN: - guestHint = `repo "${DEMO_REPO}"`; - break; - case CardType.GIST: - guestHint = `Gist ID "${DEMO_GIST}"`; - break; - case CardType.WAKATIME: - guestHint = `WakaTime username "${DEMO_WAKATIME_USER}"`; - break; - default: - selectedCard satisfies never; - } + const guestHint = useMemo(() => { + switch (selectedCard) { + case CardType.STATS: + case CardType.TOP_LANGS: + return `username "${DEMO_USER}"`; + case CardType.PIN: + return `repo "${DEMO_REPO}"`; + case CardType.GIST: + return `Gist ID "${DEMO_GIST}"`; + case CardType.WAKATIME: + return `WakaTime username "${DEMO_WAKATIME_USER}"`; + default: + selectedCard satisfies never; + return ""; + } + }, [selectedCard]); useEffect(() => { - const fetchGistUrl = async (gistId: string) => { + async function fetchGistURL() { try { - const fullUrl = `https://api.github.com/gists/${gistId}`; - const result = await axios.get(fullUrl); - return result.data.html_url; + const fullUrl = `https://api.github.com/gists/${gist}`; + const result = await axios.get<{ html_url: string }>(fullUrl); + setGistUrl(result.data.html_url); } catch (error) { console.error(error); - return ""; } - }; - - fetchGistUrl(gist).then(setGistUrl); + } + void fetchGistURL(); }, [gist]); const contentSectionRef = useRef(null); @@ -287,7 +279,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element { if (url.includes("code=")) { const tempPrivateAccess = url.includes("private"); const newUrl = url.split("?code=", 2) as [string, string]; - const redirect = `${url.split(HOST)[0]}${HOST}/frontend`; + const redirect = `${url.split(HOST)[0] as string}${HOST}/frontend`; window.history.pushState({}, "", redirect); setIsLoading(true); const userKey = uuidv4(); @@ -296,13 +288,15 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element { tempPrivateAccess, userKey, ); - login(newUserId, userKey); + + dispatch(login({ userId: newUserId, userKey })); + setIsLoading(false); } } - redirectCode(); - }, []); + void redirectCode(); + }, [dispatch]); if (isLoading) { return ( @@ -377,7 +371,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element { )} {stage === 2 && ( { switch (selectedCard) { case CardType.STATS: @@ -445,7 +438,6 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element { return ""; } })()} - // eslint-disable-next-line consistent-return link={(() => { switch (selectedCard) { case CardType.STATS: diff --git a/apps/frontend/src/pages/Home/stages/Customize.tsx b/apps/frontend/src/pages/Home/stages/Customize.tsx index db53f8de..64f25f89 100644 --- a/apps/frontend/src/pages/Home/stages/Customize.tsx +++ b/apps/frontend/src/pages/Home/stages/Customize.tsx @@ -101,7 +101,7 @@ export function CustomizeStage({ fullSuffix, setStage, }: CustomizeStageProps): JSX.Element { - const cardType = selectedCard || CardType.STATS; + const cardType = selectedCard; const isAuthenticated = useIsAuthenticated(); return ( @@ -142,7 +142,7 @@ export function CustomizeStage({ if (newValue.endsWith("/")) { newValue = newValue.slice(0, -1); } - let parts = newValue.split("/"); + const parts = newValue.split("/"); if (parts.length > 1) { newValue = parts.slice(-1).join("/"); } @@ -186,7 +186,7 @@ export function CustomizeStage({ if (newValue.endsWith("/")) { newValue = newValue.slice(0, -1); } - let parts = newValue.split("/"); + const parts = newValue.split("/"); if (parts.length > 2) { newValue = parts.slice(-2).join("/"); } @@ -230,7 +230,7 @@ export function CustomizeStage({ if (newValue.endsWith("/")) { newValue = newValue.slice(0, -1); } - let parts = newValue.split("/"); + const parts = newValue.split("/"); if (parts.length > 1) { newValue = parts.slice(-1).join("/"); } diff --git a/apps/frontend/src/pages/Home/stages/Display.tsx b/apps/frontend/src/pages/Home/stages/Display.tsx index c99159ab..4ce8a13b 100644 --- a/apps/frontend/src/pages/Home/stages/Display.tsx +++ b/apps/frontend/src/pages/Home/stages/Display.tsx @@ -34,7 +34,7 @@ export function DisplayStage({ }; const copyMarkdown = () => { - navigator.clipboard.writeText( + void navigator.clipboard.writeText( `[![GitHub Stats](https://${HOST}/api${themeSuffix})](${link})`, ); toast.info("Copied to Clipboard!", { @@ -48,7 +48,7 @@ export function DisplayStage({ }; const copyUrl = () => { - navigator.clipboard.writeText(`https://${HOST}/api${themeSuffix}`); + void navigator.clipboard.writeText(`https://${HOST}/api${themeSuffix}`); toast.info("Copied to Clipboard!", { position: "bottom-right", autoClose: 1500, @@ -70,11 +70,19 @@ export function DisplayStage({ highlight: true, onClick: copyMarkdown, }, - { title: "Copy URL", highlight: false, onClick: copyUrl }, - { title: "Download PNG", highlight: false, onClick: downloadPNG }, - ].map((item, index) => ( + { + title: "Copy URL", + highlight: false, + onClick: copyUrl, + }, + { + title: "Download PNG", + highlight: false, + onClick: downloadPNG, + }, + ].map((item) => ( @@ -244,6 +244,7 @@ export function LoginStage({ return (
diff --git a/apps/frontend/src/pages/Home/stages/SelectCard.tsx b/apps/frontend/src/pages/Home/stages/SelectCard.tsx index 50ab00c1..aa66b3f8 100644 --- a/apps/frontend/src/pages/Home/stages/SelectCard.tsx +++ b/apps/frontend/src/pages/Home/stages/SelectCard.tsx @@ -67,10 +67,10 @@ export function SelectCardStage({ return (
- {options.map((card, index) => ( + {options.map((card) => (