diff --git a/eslint.config.mjs b/eslint.config.mjs index c03c2a9..220617c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,7 +5,22 @@ import nextTs from "eslint-config-next/typescript"; const eslintConfig = defineConfig([ ...nextVitals, ...nextTs, - globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]), + globalIgnores([".next/**", "out/**", "build/**", "coverage/**", "next-env.d.ts"]), + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }, + ], + }, + }, + { + files: ["**/*.test.ts", "**/*.test.tsx"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-require-imports": "off", + }, + }, ]); export default eslintConfig; diff --git a/src/app/(web)/tools/addeditfamily/add-edit-family.tsx b/src/app/(web)/tools/addeditfamily/add-edit-family.tsx index 7b3211f..b91776a 100644 --- a/src/app/(web)/tools/addeditfamily/add-edit-family.tsx +++ b/src/app/(web)/tools/addeditfamily/add-edit-family.tsx @@ -46,7 +46,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { ToolParams, isNewRecord } from "@/lib/tool-params"; +import { ToolParams } from "@/lib/tool-params"; import { emptyHousehold, emptyMember, @@ -88,7 +88,6 @@ export function AddEditFamily({ params, initialContactId }: AddEditFamilyProps) const [confirmCloseOpen, setConfirmCloseOpen] = useState(false); const [addressTab, setAddressTab] = useState<"main" | "alt">("main"); const [expandedMembers, setExpandedMembers] = useState>(new Set()); - const isNew = isNewRecord(params); const isDirty = useMemo( () => household !== null && JSON.stringify(household) !== originalSnapshot, @@ -110,14 +109,6 @@ export function AddEditFamily({ params, initialContactId }: AddEditFamilyProps) }; }, []); - useEffect(() => { - if (!defaults) return; - if (initialContactId && initialContactId > 0) { - loadHousehold(initialContactId); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [initialContactId, defaults]); - const loadHousehold = useCallback(async (contactId: number) => { const result = await fetchHousehold(contactId); if (result.success) { @@ -129,6 +120,14 @@ export function AddEditFamily({ params, initialContactId }: AddEditFamilyProps) } }, []); + useEffect(() => { + if (!defaults) return; + if (initialContactId && initialContactId > 0) { + // eslint-disable-next-line react-hooks/set-state-in-effect + loadHousehold(initialContactId); + } + }, [initialContactId, defaults, loadHousehold]); + const startNewFamily = useCallback( (lastName: string) => { if (!defaults) return; @@ -1026,6 +1025,8 @@ function AddressLine1Autocomplete({ } if (debounceRef.current) clearTimeout(debounceRef.current); if (value.trim().length < 3) { + // Clear predictions on short input (debounced autocomplete). + // eslint-disable-next-line react-hooks/set-state-in-effect setPredictions([]); return; } diff --git a/src/app/(web)/tools/addresslabels/address-labels.tsx b/src/app/(web)/tools/addresslabels/address-labels.tsx index 40510e4..a8d06e1 100644 --- a/src/app/(web)/tools/addresslabels/address-labels.tsx +++ b/src/app/(web)/tools/addresslabels/address-labels.tsx @@ -71,7 +71,7 @@ export function AddressLabels({ params }: AddressLabelsProps) { }, []); // Only re-fetch when filtering-relevant config changes (mode, barcode toggle), - // NOT when layout-only config changes (stock, start position, barcode format) + // NOT when layout-only config changes (stock, start position, barcode format). const loadData = useCallback(async () => { setIsLoading(true); setError(null); @@ -84,9 +84,11 @@ export function AddressLabels({ params }: AddressLabelsProps) { } finally { setIsLoading(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [params, config.addressMode, config.includeMissingBarcodes]); useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect loadData(); }, [loadData]); diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx index 160b219..6191392 100644 --- a/src/app/signin/page.tsx +++ b/src/app/signin/page.tsx @@ -57,6 +57,7 @@ function SignInContent() { } }, []); + // eslint-disable-next-line react-hooks/preserve-manual-memoization const startOAuth = useCallback(() => { console.log("Redirecting to SignIn API"); setIsRedirecting(true); diff --git a/src/components/address-labels/imb-barcode.tsx b/src/components/address-labels/imb-barcode.tsx index 9275571..e1a44da 100644 --- a/src/components/address-labels/imb-barcode.tsx +++ b/src/components/address-labels/imb-barcode.tsx @@ -1,5 +1,4 @@ import { View } from '@react-pdf/renderer'; -import type { BarState } from '@/lib/imb-encoder'; interface ImbBarcodeProps { /** Pre-encoded bar states string (65 chars of T/D/A/F) */ diff --git a/src/components/address-labels/word-document.ts b/src/components/address-labels/word-document.ts index 9abfebc..b1ad11c 100644 --- a/src/components/address-labels/word-document.ts +++ b/src/components/address-labels/word-document.ts @@ -7,7 +7,6 @@ import { TableCell, WidthType, ImageRun, - AlignmentType, BorderStyle, convertInchesToTwip, SectionType, @@ -32,7 +31,6 @@ function buildLabelCell( stock: LabelStockConfig ): TableCell { const cellWidthTwips = convertInchesToTwip(ptToIn(stock.labelWidth)); - const cellHeightTwips = convertInchesToTwip(ptToIn(stock.labelHeight)); if (!label) { // Empty cell (for start position offset) diff --git a/src/components/dev-panel/dev-panel.tsx b/src/components/dev-panel/dev-panel.tsx index c2f4917..7fe60ff 100644 --- a/src/components/dev-panel/dev-panel.tsx +++ b/src/components/dev-panel/dev-panel.tsx @@ -49,7 +49,10 @@ export function DevPanel({ params }: DevPanelProps) { const [isAuthorized, setIsAuthorized] = useState(false); useEffect(() => { + // SSR hydration gate + read persisted open state from localStorage. + // eslint-disable-next-line react-hooks/set-state-in-effect setMounted(true); + setIsOpen(readOpenState()); }, []); diff --git a/src/components/dev-panel/panels/contact-records-panel.tsx b/src/components/dev-panel/panels/contact-records-panel.tsx index b84449b..62ed852 100644 --- a/src/components/dev-panel/panels/contact-records-panel.tsx +++ b/src/components/dev-panel/panels/contact-records-panel.tsx @@ -29,7 +29,10 @@ export function ContactRecordsPanel({ params, selectionRecordIds }: ContactRecor const recordIds = hasSingleRecord ? [params.recordID!] : selectionRecordIds!; + // Loading/error flags before async fetch — pattern not avoidable without a reducer. + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true); + setError(null); async function fetchContacts() { diff --git a/src/components/dev-panel/panels/deploy-tool-panel.tsx b/src/components/dev-panel/panels/deploy-tool-panel.tsx index 5cc16c0..11a57eb 100644 --- a/src/components/dev-panel/panels/deploy-tool-panel.tsx +++ b/src/components/dev-panel/panels/deploy-tool-panel.tsx @@ -73,6 +73,7 @@ export function DeployToolPanel({ onDeployed }: DeployToolPanelProps = {}) { // Reset the deploy UI when route changes. useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setLaunchPage(defaultLaunchPage); }, [defaultLaunchPage]); diff --git a/src/components/dev-panel/panels/user-tools-panel.tsx b/src/components/dev-panel/panels/user-tools-panel.tsx index 01f8c14..a80c9e7 100644 --- a/src/components/dev-panel/panels/user-tools-panel.tsx +++ b/src/components/dev-panel/panels/user-tools-panel.tsx @@ -18,7 +18,10 @@ export function UserToolsPanel({ refreshKey = 0, onAuthorizationChange }: UserTo useEffect(() => { let cancelled = false; + // Loading/error flags before async fetch. + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true); + setError(null); (async () => { try { diff --git a/src/components/template-editor/editor-code-dialog.tsx b/src/components/template-editor/editor-code-dialog.tsx index 4ea3f90..efce66e 100644 --- a/src/components/template-editor/editor-code-dialog.tsx +++ b/src/components/template-editor/editor-code-dialog.tsx @@ -29,10 +29,15 @@ export function EditorCodeDialog({ open, onOpenChange }: EditorCodeDialogProps) useEffect(() => { if (open) { + // Sync editor's current MJML into dialog state when opened, reset preview. const mjml = editor.getHtml(); + // eslint-disable-next-line react-hooks/set-state-in-effect setMjmlSource(mjml); + setHtmlPreview(''); + setCompileErrors([]); + setCopiedSource(null); } }, [open, editor]); diff --git a/src/components/template-editor/editor-export-dialog.tsx b/src/components/template-editor/editor-export-dialog.tsx index 93de937..9670afc 100644 --- a/src/components/template-editor/editor-export-dialog.tsx +++ b/src/components/template-editor/editor-export-dialog.tsx @@ -30,14 +30,20 @@ export function EditorExportDialog({ open, onOpenChange }: EditorExportDialogPro useEffect(() => { if (open) { + // Sync editor's current MJML/project data into dialog state when opened. const mjml = editor.getHtml(); + // eslint-disable-next-line react-hooks/set-state-in-effect setMjmlSource(mjml); const projectData = editor.getProjectData(); + setJsonState(JSON.stringify(projectData, null, 2)); + setHtmlOutput(''); + setCompileErrors([]); + setCopiedSource(null); } }, [open, editor]); diff --git a/src/components/template-editor/editor-toolbar.tsx b/src/components/template-editor/editor-toolbar.tsx index b72a6b8..358b14c 100644 --- a/src/components/template-editor/editor-toolbar.tsx +++ b/src/components/template-editor/editor-toolbar.tsx @@ -61,6 +61,8 @@ export function EditorToolbar({ onClose }: EditorToolbarProps) { }, [editor]); useEffect(() => { + // Sync undo/redo from GrapesJS UndoManager and subscribe to its change events. + // eslint-disable-next-line react-hooks/set-state-in-effect updateUndoRedo(); const onUpdate = () => updateUndoRedo(); diff --git a/src/contexts/user-context.tsx b/src/contexts/user-context.tsx index a6b3cd9..cd88cc7 100644 --- a/src/contexts/user-context.tsx +++ b/src/contexts/user-context.tsx @@ -50,6 +50,7 @@ export function UserProvider({ children }: UserProviderProps) { useEffect(() => { if (!isPending && userGuid) { + // eslint-disable-next-line react-hooks/set-state-in-effect loadUserProfile(); } else if (!isPending && !session) { setUserProfile(null); diff --git a/src/lib/barcode-image.ts b/src/lib/barcode-image.ts index 1d7ac9b..d831e67 100644 --- a/src/lib/barcode-image.ts +++ b/src/lib/barcode-image.ts @@ -3,7 +3,6 @@ * BMP is used because it has a trivial format (no compression) and Word supports it natively. */ -import type { BarState } from './imb-encoder'; import type { PostnetBar } from './postnet-encoder'; const IMB_DIMS: Record = { diff --git a/src/lib/providers/ministry-platform/provider.test.ts b/src/lib/providers/ministry-platform/provider.test.ts index 3f1feb7..cf4aaf9 100644 --- a/src/lib/providers/ministry-platform/provider.test.ts +++ b/src/lib/providers/ministry-platform/provider.test.ts @@ -107,7 +107,7 @@ describe('MinistryPlatformProvider', () => { beforeEach(() => { vi.clearAllMocks(); // Reset singleton - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (MinistryPlatformProvider as any).instance = undefined; }); diff --git a/src/services/fieldManagementService.test.ts b/src/services/fieldManagementService.test.ts index fda1537..b247e54 100644 --- a/src/services/fieldManagementService.test.ts +++ b/src/services/fieldManagementService.test.ts @@ -17,7 +17,7 @@ import { FieldManagementService } from './fieldManagementService'; describe('FieldManagementService', () => { beforeEach(() => { vi.clearAllMocks(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (FieldManagementService as any).instance = undefined; }); diff --git a/src/services/groupService.test.ts b/src/services/groupService.test.ts index 6a7415f..50d53ec 100644 --- a/src/services/groupService.test.ts +++ b/src/services/groupService.test.ts @@ -20,7 +20,7 @@ import { GroupService } from './groupService'; describe('GroupService', () => { beforeEach(() => { vi.clearAllMocks(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (GroupService as any).instance = undefined; }); diff --git a/src/services/toolService.test.ts b/src/services/toolService.test.ts index 04051bc..44a7400 100644 --- a/src/services/toolService.test.ts +++ b/src/services/toolService.test.ts @@ -18,7 +18,7 @@ vi.mock('@/lib/providers/ministry-platform', () => { describe('ToolService', () => { beforeEach(() => { vi.clearAllMocks(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (ToolService as any).instance = undefined; }); diff --git a/src/services/userService.test.ts b/src/services/userService.test.ts index f20c2f9..e140cf6 100644 --- a/src/services/userService.test.ts +++ b/src/services/userService.test.ts @@ -15,7 +15,7 @@ describe('UserService', () => { beforeEach(() => { vi.clearAllMocks(); // Reset singleton instance between tests - // eslint-disable-next-line @typescript-eslint/no-explicit-any + (UserService as any).instance = undefined; });