feat(frontend): adds typescript-eslint

This commit is contained in:
Marco Pasqualetti
2026-01-31 02:27:04 +01:00
parent 24907643ac
commit 6d17615750
28 changed files with 434 additions and 158 deletions
+11 -4
View File
@@ -6,10 +6,12 @@ const authenticate = async (
code: string,
privateAccess: boolean,
userKey: string,
) => {
): Promise<string> => {
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<null | { token: string; privateAccess: string }> => {
): Promise<null | UserMetaDataResponse> => {
try {
const fullUrl = `https://${HOST}/api/user-access?user_key=${userKey}`;
const result = await axios.get(fullUrl);
const result = await axios.get<UserMetaDataResponse>(fullUrl);
return result.data;
} catch (error) {
console.error(error);
+16 -5
View File
@@ -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<TData, TConfig>(
@@ -54,7 +54,7 @@ function createMockResponse<TData, TConfig>(
}
// 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);
}
@@ -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<string | null>(null);
const [loaded, setLoaded] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(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<string>(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;
@@ -46,7 +46,7 @@ const options: Array<SelectOption> = [
];
interface LanguagesLayoutSectionProps {
selectedLanguageLayoutOption: SelectOption;
selectedLanguageLayoutOption: SelectOption | undefined;
onLanguageLayoutOptionChange: (option: SelectOption) => void;
}
@@ -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}
@@ -75,11 +75,13 @@ export function ProgressBar({
return (
<ProgressSection
num={index}
key={index}
key={item} // each step should have a unique name
item={item}
passed={currItemIndex >= 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));
}}
/>
</div>
);
@@ -20,7 +20,7 @@ const options: Array<SelectOption> = [
];
interface StatsRankSectionProps {
selectedOption: SelectOption;
selectedOption: SelectOption | undefined;
onOptionChange: (option: SelectOption) => void;
}
@@ -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 (
<Section title={title}>
@@ -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}
@@ -28,7 +28,7 @@ const options: Array<SelectOption> = [
];
interface WakatimeLayoutSectionProps {
selectedOption: SelectOption;
selectedOption: SelectOption | undefined;
onOptionChange: (option: SelectOption) => void;
}
+3 -3
View File
@@ -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"];
+1 -1
View File
@@ -1,4 +1,4 @@
// Safe browser stub for dotenv
export function config(): {} {
export function config(): { parsed: Record<string, never> } {
return { parsed: {} };
}
+8 -6
View File
@@ -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 {};
+2 -2
View File
@@ -52,9 +52,9 @@ interface CreateMockResponseResult {
}
export function createMockResponse(): CreateMockResponseResult {
let statusCode = 200;
const statusCode = 200;
const headers: HeaderMap = {};
let chunks: Array<unknown> = [];
const chunks: Array<unknown> = [];
const res: CreateMockResponseResult = {
statusCode,
+9 -5
View File
@@ -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 (
<div className="min-h-screen flex flex-col">
+32 -40
View File
@@ -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<string>(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<HTMLDivElement | null>(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 && (
<CustomizeStage
selectedCard={selectedCard || CardType.STATS}
selectedCard={selectedCard}
selectedStatsRank={selectedStatsRank}
setSelectedStatsRank={setSelectedStatsRank}
selectedLanguagesLayout={selectedLanguagesLayout}
@@ -428,7 +422,6 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element {
)}
{stage === 4 && (
<DisplayStage
// eslint-disable-next-line consistent-return
filename={(() => {
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:
@@ -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("/");
}
@@ -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) => (
<Button
key={index}
key={item.title}
className={clsx("m-4 w-60 flex justify-center", {
"bg-blue-500 hover:bg-blue-600 text-white": item.highlight,
"bg-white hover:bg-gray-100 text-black": !item.highlight,
+40 -36
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { JSX, RefObject } from "react";
import clsx from "clsx";
import { useDispatch } from "react-redux";
@@ -16,7 +16,7 @@ import {
HOST,
} from "../../../constants";
import { FaGithub as GithubIcon } from "react-icons/fa";
import { logout as _logout } from "../../../redux/slices/user";
import { logout } from "../../../redux/slices/user";
import {
useIsAuthenticated,
usePrivateAccess,
@@ -25,6 +25,30 @@ import {
} from "../../../redux/selectors/userSelectors";
import { deleteAccount } from "../../../api/user";
function useOutsideAlerter(
ref: RefObject<HTMLElement | null>,
action: () => void,
) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) {
action();
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [action, ref]);
}
interface LoginStageProps {
onContinueAsGuestClick: () => void;
}
@@ -33,16 +57,16 @@ export function LoginStage({
onContinueAsGuestClick,
}: LoginStageProps): JSX.Element {
const userId = useUserId();
const userKey = useUserKey();
const userKey = useUserKey() as string;
const privateAccess = usePrivateAccess();
const isAuthenticated = useIsAuthenticated();
const dispatch = useDispatch();
const [deleteModal, setDeleteModal] = useState(false);
const logout = () => {
dispatch(_logout({ userKey: null }));
};
const handleLogout = useCallback(() => {
dispatch(logout({ userKey: null }));
}, [dispatch]);
const openDeleteModal = () => {
setDeleteModal(true);
@@ -52,37 +76,13 @@ export function LoginStage({
setDeleteModal(false);
};
function useOutsideAlerter(
ref: RefObject<HTMLElement | null>,
action: () => void,
) {
useEffect(() => {
/**
* Alert if clicked on outside of element
*/
function handleClickOutside(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) {
action();
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef, closeDeleteModal);
const deleteAccountHandler = async () => {
const success = await deleteAccount(userId as string, userKey as string);
const handleAccountDelete = async () => {
const success = await deleteAccount(userId as string, userKey);
if (success) {
logout();
handleLogout();
window.location.href = `https://github.com/settings/connections/applications/${CLIENT_ID}`;
}
};
@@ -171,7 +171,7 @@ export function LoginStage({
<div className="mt-6 flex items-center gap-4">
<Button
className="h-12 flex justify-center items-center w-[320px] text-black border border-black bg-white hover:bg-gray-100"
onClick={logout}
onClick={handleLogout}
>
<span className="xl:text-lg">Log Out</span>
</Button>
@@ -244,6 +244,7 @@ export function LoginStage({
return (
<div
// eslint-disable-next-line react/no-array-index-key
key={index}
style={{
left: `${x}%`,
@@ -282,13 +283,16 @@ export function LoginStage({
<div className="flex flex-wrap">
<Button
className="bg-blue-500 hover:bg-blue-600 text-white rounded-[0.25rem]"
onClick={() => setDeleteModal(false)}
onClick={() => {
setDeleteModal(false);
}}
>
Cancel
</Button>
<Button
className="bg-gray-200 hover:bg-gray-300 ml-auto rounded-[0.25rem] text-red-600 border-2"
onClick={deleteAccountHandler}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={handleAccountDelete}
>
Delete Account
</Button>
@@ -67,10 +67,10 @@ export function SelectCardStage({
return (
<div className="w-full flex flex-wrap">
{options.map((card, index) => (
{options.map((card) => (
<button
className="p-2 lg:p-4"
key={index}
key={card.cardType}
type="button"
onClick={() => {
onCardTypeChange(card.cardType);
@@ -18,7 +18,8 @@ export function ThemeStage({
return (
<>
<div className="flex flex-wrap">
{Object.keys(themes)
{/* Needed until themes is proper types */}
{Object.keys(themes as Record<string, string>)
.filter(
(myTheme) =>
![
@@ -30,10 +31,10 @@ export function ThemeStage({
"holi",
].includes(myTheme),
)
.map((myTheme, index) => (
.map((myTheme) => (
<button
className="p-2 lg:p-4"
key={index}
key={myTheme}
type="button"
onClick={() => {
onThemeChange(myTheme);
@@ -2,14 +2,11 @@ import { useSelector } from "react-redux";
import type { StoreState } from "../store";
export const useUserId = <
TUserName extends string | undefined,
TOutput = TUserName extends string ? string : string | null,
>(
export const useUserId = <TUserName extends string | undefined>(
fallbackUsername?: TUserName,
): TOutput => {
): TUserName => {
const storeValue = useSelector((state: StoreState) => state.user.userId);
return (storeValue || fallbackUsername || null) as TOutput;
return (storeValue || fallbackUsername || null) as TUserName;
};
export const useIsAuthenticated = (): boolean => {
+10 -2
View File
@@ -14,9 +14,17 @@ export interface UserState {
privateAccess: string | null;
}
function getFromLocalStorage(key: string): string | null {
const storageValue = localStorage.getItem(key);
if (!storageValue) {
return null;
}
return (JSON.parse(storageValue) as string) || null;
}
const initialState: UserState = {
userId: JSON.parse(localStorage.getItem("userId") as string) || null,
userKey: JSON.parse(localStorage.getItem("userKey") as string) || null,
userId: getFromLocalStorage("userId"),
userKey: getFromLocalStorage("userKey"),
token: null,
privateAccess: null,
};
+3 -4
View File
@@ -2,23 +2,22 @@ import axios from "axios";
import { HOST } from "./constants";
// See https://github.com/stats-organization/github-stats-extended/pull/27#discussion_r2712184285
// eslint-disable-next-line no-unused-vars
const fetchWakatimeStats = async ({
username,
api_domain: _,
}: {
username: string;
api_domain: string;
}) => {
}): Promise<unknown> => {
if (!username) {
throw new Error("missing parameter: username");
}
const { data } = await axios.get(
const res = await axios.get<unknown>(
`https://${HOST}/api/wakatime-proxy?username=${username}`,
);
return data;
return res.data;
};
export { fetchWakatimeStats };
+2 -1
View File
@@ -1,7 +1,8 @@
{
"extends": ["../../tsconfig.base.json"],
"include": ["src", "src/**/*.json"],
"include": ["src", "src/**/*.json", "vite.config.ts"],
"compilerOptions": {
"lib": ["DOM"],
"composite": true,
"module": "esnext",
"moduleResolution": "bundler",
+8 -2
View File
@@ -27,10 +27,16 @@ export default defineConfig({
{
name: "empty-pg-package",
resolveId(id) {
if (id === "pg") return id;
if (id === "pg") {
return id;
}
return undefined;
},
load(id) {
if (id === "pg") return "export default {}";
if (id === "pg") {
return "export default {}";
}
return undefined;
},
},
],
+39 -4
View File
@@ -7,6 +7,7 @@ import jsdoc from "eslint-plugin-jsdoc";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import { includeIgnoreFile } from "@eslint/compat";
import tseslint from "typescript-eslint";
const gitignorePath = fileURLToPath(new URL(".gitignore", import.meta.url));
@@ -25,8 +26,6 @@ export default defineConfig(
},
plugins: {
jsdoc,
react,
"react-hooks": reactHooks,
},
rules: {
"no-unexpected-multiline": "error",
@@ -64,7 +63,6 @@ export default defineConfig(
"no-this-before-super": "error",
"object-shorthand": ["warn"],
"no-mixed-spaces-and-tabs": "warn",
"no-multiple-empty-lines": "warn",
"no-negated-condition": "warn",
"no-unneeded-ternary": "warn",
"keyword-spacing": [
@@ -80,6 +78,43 @@ export default defineConfig(
"jsdoc/require-jsdoc": "warn",
},
},
{
files: ["**/*.{d.ts,ts,tsx}"],
ignores: ["apps/backend/**"],
extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylistic],
rules: {
"@typescript-eslint/array-type": ["error", { default: "generic" }],
"@typescript-eslint/restrict-template-expressions": [
"error",
{
allowAny: false,
allowBoolean: true, // for query parameters
allowNever: false,
allowNullish: false,
allowNumber: true,
allowRegExp: false,
},
],
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
},
],
// We don't need this we have typescript
"jsdoc/require-returns": "off",
"jsdoc/require-returns-description": "off",
"jsdoc/require-param-description": "off",
"jsdoc/require-jsdoc": "off",
},
languageOptions: {
parserOptions: {
projectService: true,
},
},
},
{
files: ["apps/backend/**/*.{js}"],
languageOptions: {
@@ -89,7 +124,7 @@ export default defineConfig(
},
},
{
files: ["apps/frontend/**/*.{js,jsx}"],
files: ["apps/frontend/**/*.{js,jsx,ts,tsx}"],
plugins: {
react,
"react-hooks": reactHooks,
+2 -1
View File
@@ -15,7 +15,8 @@
"knip": "5.81.0",
"lint-staged": "16.2.7",
"prettier": "3.7.4",
"typescript": "5.9.3"
"typescript": "5.9.3",
"typescript-eslint": "8.53.1"
},
"scripts": {
"prepare": "husky",
+187
View File
@@ -44,6 +44,9 @@ importers:
typescript:
specifier: 5.9.3
version: 5.9.3
typescript-eslint:
specifier: 8.53.1
version: 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
apps/backend:
dependencies:
@@ -1339,10 +1342,69 @@ packages:
'@types/yargs@17.0.35':
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
'@typescript-eslint/eslint-plugin@8.53.1':
resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.53.1
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.53.1':
resolution: {integrity: sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.53.1':
resolution: {integrity: sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.53.1':
resolution: {integrity: sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.53.1':
resolution: {integrity: sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.53.1':
resolution: {integrity: sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/types@8.52.0':
resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/types@8.53.1':
resolution: {integrity: sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.53.1':
resolution: {integrity: sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.53.1':
resolution: {integrity: sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@8.53.1':
resolution: {integrity: sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
@@ -2530,6 +2592,10 @@ packages:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
ignore@7.0.5:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
immer@11.1.3:
resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==}
@@ -3993,6 +4059,12 @@ packages:
try@1.0.3:
resolution: {integrity: sha512-AHA8khVCII6zKyRkyPo6pRwoR9v5jb7QFw6e5avtaVSkxVfaEucYIo06xnwB+pJaEarfYNbs7W3Vq+LZLZiWyA==}
ts-api-utils@2.4.0:
resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
@@ -4052,6 +4124,13 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
typescript-eslint@8.53.1:
resolution: {integrity: sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -5411,8 +5490,99 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.53.1
'@typescript-eslint/type-utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.53.1
eslint: 9.39.2(jiti@2.6.1)
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.53.1
'@typescript-eslint/types': 8.53.1
'@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.53.1
debug: 4.4.3
eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.53.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3)
'@typescript-eslint/types': 8.53.1
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.53.1':
dependencies:
'@typescript-eslint/types': 8.53.1
'@typescript-eslint/visitor-keys': 8.53.1
'@typescript-eslint/tsconfig-utils@8.53.1(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.53.1
'@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.2(jiti@2.6.1)
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.52.0': {}
'@typescript-eslint/types@8.53.1': {}
'@typescript-eslint/typescript-estree@8.53.1(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.53.1(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.53.1(typescript@5.9.3)
'@typescript-eslint/types': 8.53.1
'@typescript-eslint/visitor-keys': 8.53.1
debug: 4.4.3
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1))
'@typescript-eslint/scope-manager': 8.53.1
'@typescript-eslint/types': 8.53.1
'@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.53.1':
dependencies:
'@typescript-eslint/types': 8.53.1
eslint-visitor-keys: 4.2.1
'@ungap/structured-clone@1.3.0': {}
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
@@ -6821,6 +6991,8 @@ snapshots:
ignore@5.3.2: {}
ignore@7.0.5: {}
immer@11.1.3: {}
import-fresh@3.3.1:
@@ -8625,6 +8797,10 @@ snapshots:
try@1.0.3: {}
ts-api-utils@2.4.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
ts-interface-checker@0.1.13: {}
ts-node@10.9.2(@swc/core@1.15.8)(@types/node@25.0.3)(typescript@5.9.3):
@@ -8702,6 +8878,17 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
typescript-eslint@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.53.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
typescript@5.9.3: {}
unbox-primitive@1.1.0: