chore(frontend): add eslint quality gate

This commit is contained in:
cesnimda
2026-08-30 00:19:51 +02:00
parent 7318b02e11
commit a74daa7aa4
21 changed files with 3729 additions and 82 deletions
+35
View File
@@ -0,0 +1,35 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTypeScript from "eslint-config-next/typescript";
export default defineConfig([
...nextVitals,
...nextTypeScript,
{
rules: {
"@typescript-eslint/no-explicit-any": "off",
"react-hooks/set-state-in-effect": "off",
"react-hooks/immutability": "off",
"react-hooks/refs": "off",
"react-hooks/use-memo": "off",
"react-hooks/purity": "off",
"@next/next/no-page-custom-font": "off",
"@next/next/no-img-element": "off",
},
},
{
files: ["**/*.test.{ts,tsx}", "e2e/**/*.{ts,tsx}", "src/setupTests.ts"],
rules: {
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-require-imports": "off",
"react/display-name": "off",
},
},
globalIgnores([
".next/**",
"out/**",
"build/**",
"coverage/**",
"next-env.d.ts",
]),
]);
+3653 -20
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -33,6 +33,8 @@
"start": "next dev --webpack",
"serve:export": "node ./scripts/serve-export.mjs",
"build": "node --max-old-space-size=4096 ./node_modules/next/dist/bin/next build",
"lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix",
"test": "jest",
"test:e2e": "node ./scripts/run-e2e.mjs"
},
@@ -55,12 +57,14 @@
"@playwright/test": "^1.62.1",
"@types/jest": "^30.0.0",
"babel-jest": "^30.4.1",
"eslint": "^9.39.5",
"eslint-config-next": "^16.2.12",
"identity-obj-proxy": "^3.0.0",
"jest": "^30.4.2",
"jest-environment-jsdom": "^30.4.1"
},
"overrides": {
"brace-expansion": "5.0.9",
"brace-expansion": "2.1.4",
"postcss": "8.5.25",
"sharp": "0.35.3"
}
@@ -11,14 +11,10 @@ import {
DialogContent,
DialogTitle,
Divider,
FormControl,
InputLabel,
List,
ListItemButton,
ListItemText,
MenuItem,
Paper,
Select,
Tab,
Tabs,
TextField,
@@ -190,7 +186,7 @@ export default function Correspondence({ jobId, jobContext }: { jobId: number; j
} finally {
setGmailMatchesLoading(false);
}
}, [jobId, toast]);
}, [jobId, t, toast]);
const loadAvailableJobs = useCallback(async (query?: string) => {
try {
@@ -19,7 +19,7 @@ type State = {
class ErrorBoundaryInner extends React.Component<ErrorBoundaryInnerProps, State> {
state: State = { hasError: false };
static getDerivedStateFromError(_: any) {
static getDerivedStateFromError() {
return { hasError: true };
}
@@ -30,7 +30,7 @@ import { emptyTailoredCvDraft, joinLines, normalizeTailoredCvDraft, splitLines }
import Correspondence from "./Correspondence";
import Attachments from "./Attachments";
import JobInsightTabs from "./JobInsightTabs";
import { DraftCard, ListCard, MatchScoreCard, PaperRow, SectionChips, TwoColumnSection, WorkspaceDraftCard } from "./JobDetailsPanels";
import { ListCard, PaperRow, WorkspaceDraftCard } from "./JobDetailsPanels";
import JobFlowBar from "./JobFlowBar";
import GradientButton from "./GradientButton";
import { useI18n } from "../i18n/I18nProvider";
@@ -81,10 +81,6 @@ function getFitLevel(candidateFit: CandidateFit | null): { label: string; color:
return { label: candidateFit.fitLevel, color: "default" };
}
function copyLines(items: string[]) {
return navigator.clipboard.writeText(items.map((item) => `${item}`).join("\n"));
}
function getWorkspaceStatus(currentValue: string, savedValue: string) {
const current = currentValue.trim();
const saved = savedValue.trim();
@@ -1,8 +1,7 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import {
Alert,
Box,
Button,
Dialog,
DialogActions,
@@ -44,7 +43,7 @@ export default function SessionsSettingsCard() {
const [error, setError] = useState<string | null>(null);
const [confirmRevokeOthers, setConfirmRevokeOthers] = useState(false);
const loadSessions = () => {
const loadSessions = useCallback(() => {
setLoading(true);
setError(null);
api
@@ -52,9 +51,9 @@ export default function SessionsSettingsCard() {
.then((r) => setSessions(r.data))
.catch((e) => setError(apiErrorMessage(e, t)))
.finally(() => setLoading(false));
};
}, [t]);
useEffect(() => { loadSessions(); }, []);
useEffect(() => { loadSessions(); }, [loadSessions]);
async function revokeSession(id: string, isCurrentSession: boolean) {
try {
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import {
Alert,
@@ -69,11 +69,11 @@ export default function TwoFactorSettingsCard() {
const [devicesLoading, setDevicesLoading] = useState(false);
const [devicesError, setDevicesError] = useState<string | null>(null);
const loadStatus = () => {
const loadStatus = useCallback(() => {
api.get<Status>("/auth/2fa/status").then((r) => setStatus(r.data)).catch(() => setStatus(null));
};
}, []);
const loadDevices = () => {
const loadDevices = useCallback(() => {
setDevicesLoading(true);
setDevicesError(null);
api
@@ -81,9 +81,9 @@ export default function TwoFactorSettingsCard() {
.then((r) => setDevices(r.data))
.catch((e) => setDevicesError(apiErrorMessage(e, t)))
.finally(() => setDevicesLoading(false));
};
}, [t]);
useEffect(() => { loadStatus(); loadDevices(); }, []);
useEffect(() => { loadStatus(); loadDevices(); }, [loadDevices, loadStatus]);
async function revokeDevice(id: number) {
try {
@@ -14,7 +14,6 @@ jest.mock('./api', () => ({
getApiErrorMessage: jest.fn((_error, fallback) => fallback),
}));
// eslint-disable-next-line import/first
import KanbanBoard from './components/KanbanBoard';
const mockedApi = api as jest.Mocked<typeof api>;
@@ -10,7 +10,6 @@ jest.mock('@mui/x-date-pickers/DatePicker', () => ({
DatePicker: ({ label }: any) => <div>{label}</div>,
}));
// eslint-disable-next-line import/first
import AddJobModal from './components/AddJobModal';
jest.setTimeout(15000);
+1
View File
@@ -220,6 +220,7 @@ function buildTypography() {
}
export const getTheme = (_mode: "light" | "dark") => {
void _mode;
const lightPalette = buildLightPalette();
const darkPalette = buildDarkPalette();
+5 -5
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
Alert,
@@ -222,7 +222,7 @@ export default function AdminSystemPage() {
const [testEmailMessage, setTestEmailMessage] = useState(() => t("adminSystemDefaultTestMessage"));
const [sendingTestEmail, setSendingTestEmail] = useState(false);
const load = async () => {
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
@@ -244,11 +244,11 @@ export default function AdminSystemPage() {
} finally {
setLoading(false);
}
};
}, [t]);
useEffect(() => {
void load();
}, []);
}, [load]);
const dbTone = useMemo(() => {
if (!status) return "default" as const;
@@ -290,7 +290,7 @@ export default function AdminSystemPage() {
}
return findings;
}).slice(0, 10);
}, [benchmarkIndex, language]);
}, [benchmarkIndex, t]);
const sendTestEmail = async () => {
setSendingTestEmail(true);
@@ -7,11 +7,6 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import PhotoCameraOutlinedIcon from "@mui/icons-material/PhotoCameraOutlined";
import { api, getApiErrorMessage } from "../api";
import GoogleAuthCard from "../components/GoogleAuthCard";
import MicrosoftAuthCard from "../components/MicrosoftAuthCard";
import AuthStatusCard from "../components/AuthStatusCard";
import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard";
import SessionsSettingsCard from "../components/SessionsSettingsCard";
import CropImageDialog from "../components/CropImageDialog";
import ProfileCompleteness from "./career/ProfileCompleteness";
import CareerWorkspaceOverview from "./career/CareerWorkspaceOverview";
@@ -34,11 +29,7 @@ import type { UserOperation } from "../types";
import {
emptyStructuredCv,
getStructuredCvFieldMetadata,
joinLines,
normalizeStructuredCv,
parseStructuredCvJson,
splitLines,
StructuredCvFieldMetadata,
StructuredCvProfile,
} from "../profileCv";
@@ -175,7 +175,7 @@ export default function CorrespondenceInboxPage() {
} finally {
setLoading(false);
}
}, [direction, inboxPage, language, linkState, query, toast]);
}, [direction, inboxPage, linkState, query, t, toast]);
useEffect(() => {
if (view === "inbox") void load();
@@ -215,7 +215,7 @@ export default function CorrespondenceInboxPage() {
} catch {
setDraftError(t("emailInboxDraftsLoadFailed"));
}
}, [language]);
}, [t]);
useEffect(() => {
void loadStoredDrafts();
+1 -1
View File
@@ -1060,7 +1060,7 @@ function EntryEditor({ section, row, settings, onPatch, onUpdateSettings }: {
const m: Record<string, (typeof section.entries)[number]> = {};
section.entries.forEach((e) => { if (e.key) m[e.key] = e; });
return m;
}, [section.entries]);
}, [section]);
const drag = useDragReorder((from, to) => onPatch({ itemOrder: moveItem(orderedKeys, from, to) }));
+5 -5
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
@@ -31,7 +31,7 @@ export default function CvBuilderPage() {
const [newTheme, setNewTheme] = useState("modern");
const [creating, setCreating] = useState(false);
const load = async () => {
const load = useCallback(async () => {
try {
setVariants(await cvBuilderApi.list());
try { setThemes(await cvBuilderApi.themes()); } catch { setThemes([]); }
@@ -40,10 +40,10 @@ export default function CvBuilderPage() {
} finally {
setLoading(false);
}
};
}, [t]);
useEffect(() => {
load();
}, []);
void load();
}, [load]);
const createNew = async () => {
setCreateOpen(true);
+1 -1
View File
@@ -55,7 +55,7 @@ export default function OperationsPage() {
} finally {
setLoading(false);
}
}, [language]);
}, [t]);
useEffect(() => {
void load();
@@ -98,7 +98,7 @@ export default function CareerWorkspaceOverview({ completeness, runs, loading, l
if (active.length > 0) return { severity: "info" as const, label: t("careerOverviewViewProcessing"), message: t(active.length === 1 ? "careerOverviewProcessingOne" : "careerOverviewProcessingMany", { count: active.length }) };
if (failed.length > 0) return { severity: "error" as const, label: t("careerOverviewResolveImport"), message: t(failed.length === 1 ? "careerOverviewFailedOne" : "careerOverviewFailedMany", { count: failed.length }) };
return { severity: "success" as const, label: t("careerOverviewImportCv"), message: t(runs.length > 0 ? "careerOverviewImportComplete" : "careerOverviewNoImport") };
}, [language, runs, t]);
}, [runs, t]);
const isFirstRun = !loading && !recentLoading && !loadError && (completeness?.percent ?? 0) === 0 && runs.length === 0 && recentCvs.length === 0;