From 367775fb6c8a7df8416284b4b3b60605ae611b2e Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:49:40 -0600 Subject: [PATCH 01/36] easy fix --- eslint.config.js | 1 - src/app/admin/layout/List.tsx | 16 +++---- tsconfig.json | 84 ++++++++++++++++++++--------------- 3 files changed, 55 insertions(+), 46 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 1fc6a0a7..fe7594cd 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,7 +2,6 @@ import js from '@eslint/js' import nextCoreWebVitals from 'eslint-config-next/core-web-vitals' import nextTypescript from 'eslint-config-next/typescript' import eslintConfigPrettier from 'eslint-config-prettier/flat' -import tailwind from 'eslint-plugin-tailwindcss' import { defineConfig } from 'eslint/config' import globals from 'globals' import tseslint from 'typescript-eslint' diff --git a/src/app/admin/layout/List.tsx b/src/app/admin/layout/List.tsx index ef7bf643..21c2ac51 100644 --- a/src/app/admin/layout/List.tsx +++ b/src/app/admin/layout/List.tsx @@ -473,25 +473,25 @@ function PageSelect({ onChange, }: PageSelectProps) { const [value, setValue] = useState('') - + const pageCount = Math.ceil(count / pageSize) const canNavigate = pageCount > 1 const maxPage = pageCount - 1 - + const handleChangeValue = (value: string) => { if (!value || (/^\d+$/.test(value) && value.length < 10)) setValue(value) } - + const handleSubmit = () => { const newPage = +value - 1 if (0 <= newPage && newPage <= maxPage) onChange(newPage) - else setValue((page + 1).toString()) + else setValue((page + 1).toString()) + } + const pageStr = (page + 1).toString() + if (value != pageStr) { + setValue(pageStr) } - - useEffect(() => { - setValue((page + 1).toString()) - }, [page]) useEffect(() => { if (page < 0) onChange(0) diff --git a/tsconfig.json b/tsconfig.json index 75bbabbb..b803adad 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,40 +1,50 @@ { - "compilerOptions": { - "target": "ESNext", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "ESNext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noImplicitAny": false, - "jsx": "react-jsx", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"], - "auth": ["./src/util/auth"] - } - }, - "include": [ - "process.d.ts", - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - "**/*.mjs", - "**/*.js", - "vitest.config.mts", - ".next/dev/types/**/*.ts" + "compilerOptions": { + "target": "ESNext", + "lib": [ + "dom", + "dom.iterable", + "esnext" ], - "exclude": ["node_modules"] + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noImplicitAny": false, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ], + "auth": [ + "./src/util/auth" + ] + } + }, + "include": [ + "process.d.ts", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "**/*.mjs", + "**/*.js", + "vitest.config.mts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } From 284fc08788700a6f3c177a8031901d99a985ad6b Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:56:00 -0600 Subject: [PATCH 02/36] Set granularity in first render --- .../fundraising/useFundraisingDashboardController.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts b/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts index 6cffd614..5370221b 100644 --- a/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts +++ b/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts @@ -281,11 +281,12 @@ export function useFundraisingDashboardController(onGet: OnGet) { }) }, [chartBuckets, chartBucketQueries]) - useEffect(() => { - if (!validGranularityModes.includes(granularityMode)) { - setGranularityMode('auto') - } - }, [validGranularityModes, granularityMode]) + if ( + granularityMode != 'auto' && + !validGranularityModes.includes(granularityMode) + ) { + setGranularityMode('auto') + } const selectedRangeLabel = useMemo( () => getSelectedRangeLabel(committedPreset, startDate, endDate), From b3c82afae4f12a89a85f72d26b8e635906686d08 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:07:48 -0600 Subject: [PATCH 03/36] Just commenting these two out. Feels wrong, but the chart seems fully fine to me, and I'm not seeing it get set normally either. But commented out because surely I am missing something and it isn't dead code --- src/app/admin/panels/fundraising/page.tsx | 2 +- .../useFundraisingDashboardController.ts | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/admin/panels/fundraising/page.tsx b/src/app/admin/panels/fundraising/page.tsx index cfef15d0..0496c7b3 100644 --- a/src/app/admin/panels/fundraising/page.tsx +++ b/src/app/admin/panels/fundraising/page.tsx @@ -545,7 +545,7 @@ export default function Page() { if (inclusiveRangeMs < oneDayMs) { return } - + console.log("Applying chart view override range:", { startIso, endIso }) applyChartViewOverrideRange({ startIso, endIso, diff --git a/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts b/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts index 5370221b..ddedc93c 100644 --- a/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts +++ b/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts @@ -20,7 +20,7 @@ import { } from '@/components/common/charts/timeBuckets' import type { QueryParams, ZodSchema } from '@/util/hooks/useFetch' import { keepPreviousData, useQueries, useQuery } from '@tanstack/react-query' -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' interface GetOptions { query?: QueryParams @@ -230,15 +230,15 @@ export function useFundraisingDashboardController(onGet: OnGet) { ] ) - useEffect(() => { - setChartViewOverrideRange(null) - }, [startDate, endDate, committedPreset]) + // useEffect(() => { + // setChartViewOverrideRange(null) + // }, [startDate, endDate, committedPreset]) - useEffect(() => { - if (!zoomEnabled) { - setChartViewOverrideRange(null) - } - }, [zoomEnabled]) + // useEffect(() => { + // if (!zoomEnabled) { + // setChartViewOverrideRange(null) + // } + // }, [zoomEnabled]) const chartBucketQueries = useQueries({ queries: chartBuckets.map((bucket) => ({ @@ -338,7 +338,7 @@ export function useFundraisingDashboardController(onGet: OnGet) { setChartViewOverrideRange(null) return } - + console.debug("Applying chart view override range:", range) setChartViewOverrideRange(range) } From 768093512246ccb6250106599d74da654b1e20be Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:26:41 -0600 Subject: [PATCH 04/36] Again, I don't see any effect from removing this. Last one I'll just comment out before just tapping out on the assumption I'm missing something --- .../panels/members/panel_views/DonorView.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/app/admin/panels/members/panel_views/DonorView.tsx b/src/app/admin/panels/members/panel_views/DonorView.tsx index fdab94c2..f0955443 100644 --- a/src/app/admin/panels/members/panel_views/DonorView.tsx +++ b/src/app/admin/panels/members/panel_views/DonorView.tsx @@ -140,14 +140,15 @@ export function DonorView({ const [overlayMounted, setOverlayMounted] = useState(false) const [overlayOpen, setOverlayOpen] = useState(false) - useEffect(() => { - if (pickingDonor) { - setOverlayMounted(true) - requestAnimationFrame(() => setOverlayOpen(true)) - } else if (overlayMounted) { - setOverlayOpen(false) - } - }, [pickingDonor, overlayMounted]) + console.debug("Picking Donor? : ", pickingDonor); + // useEffect(() => { + // if (pickingDonor) { + // setOverlayMounted(true) + // requestAnimationFrame(() => setOverlayOpen(true)) + // } else if (overlayMounted) { + // setOverlayOpen(false) + // } + // }, [pickingDonor, overlayMounted]) const backdropVariants = { open: { From b0bd918f747ffd7b154630f8cd4b1aff19169ff8 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:38:39 -0600 Subject: [PATCH 05/36] Nothing outside of the `LayoutEffect` sets any of those three values, and we were setting all three to their default values. Thus, we can remove the setState calls without any worry --- src/app/endorsements/components/FilterButtonRow.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/app/endorsements/components/FilterButtonRow.tsx b/src/app/endorsements/components/FilterButtonRow.tsx index b6a46b76..856bd13f 100644 --- a/src/app/endorsements/components/FilterButtonRow.tsx +++ b/src/app/endorsements/components/FilterButtonRow.tsx @@ -195,9 +195,6 @@ export function FilterButtonRow({ useLayoutEffect(() => { if (!isMobile) { - setMobileSearchWidth(null) - setSingleColumnLayout(false) - setShowSearchLabel(false) return } @@ -217,7 +214,7 @@ export function FilterButtonRow({ } measure() - const rafId = window.requestAnimationFrame(measure) + const rafId = globalThis.requestAnimationFrame(measure) const resizeObserver = new ResizeObserver(measure) resizeObserver.observe(row) @@ -231,7 +228,7 @@ export function FilterButtonRow({ window.addEventListener('resize', measure) return () => { - window.cancelAnimationFrame(rafId) + globalThis.cancelAnimationFrame(rafId) resizeObserver.disconnect() window.removeEventListener('resize', measure) } From 3a2557d254fb344bd5e5bf62293c6977953faf12 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:57:04 -0600 Subject: [PATCH 06/36] low hanging fruit --- src/app/admin/panels/members/panel_views/DonorView.tsx | 6 +++--- src/app/endorsements/endorsements.utils.ts | 2 +- src/components/common/forms/PhoneField.tsx | 2 +- src/components/common/forms/TextField.tsx | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/admin/panels/members/panel_views/DonorView.tsx b/src/app/admin/panels/members/panel_views/DonorView.tsx index f0955443..dd0d9603 100644 --- a/src/app/admin/panels/members/panel_views/DonorView.tsx +++ b/src/app/admin/panels/members/panel_views/DonorView.tsx @@ -22,7 +22,7 @@ import type { UseQueryResult } from '@tanstack/react-query' import cx from 'classnames' import { motion } from 'motion/react' import Link from 'next/link' -import React, { ChangeEvent, useEffect, useMemo, useState } from 'react' +import React, { ChangeEvent, useMemo, useState } from 'react' export interface DonorViewProps { selectedId: number @@ -138,8 +138,8 @@ export function DonorView({ } const [overlayMounted, setOverlayMounted] = useState(false) - const [overlayOpen, setOverlayOpen] = useState(false) - + //const [overlayOpen, setOverlayOpen] = useState(false) + const overlayOpen = false; // For consistency since setOverlay open was only used in the seemingly unused effect console.debug("Picking Donor? : ", pickingDonor); // useEffect(() => { // if (pickingDonor) { diff --git a/src/app/endorsements/endorsements.utils.ts b/src/app/endorsements/endorsements.utils.ts index f1037941..eb3bf4f2 100644 --- a/src/app/endorsements/endorsements.utils.ts +++ b/src/app/endorsements/endorsements.utils.ts @@ -146,7 +146,7 @@ export function compareSectionEntries( return labelA === PAST_ELECTION_LABEL ? 1 : -1 } - let comparison + let comparison: number if (sectionMode === 'electionDate') { const timeA = candidatesA[0]?.electionDate?.getTime() ?? Infinity diff --git a/src/components/common/forms/PhoneField.tsx b/src/components/common/forms/PhoneField.tsx index 73896128..0bc5e5d0 100644 --- a/src/components/common/forms/PhoneField.tsx +++ b/src/components/common/forms/PhoneField.tsx @@ -2,7 +2,7 @@ import { FormField, FormFieldProps, useConfigure } from './FormField' import styles from './FormField.module.css' import cx from 'classnames' import phone from 'phone' -import { ChangeEvent, InputEvent, useCallback } from 'react' +import { InputEvent, useCallback } from 'react' /** * Formats a phone number string for display in readonly mode. diff --git a/src/components/common/forms/TextField.tsx b/src/components/common/forms/TextField.tsx index 04faa87e..4ca1432d 100644 --- a/src/components/common/forms/TextField.tsx +++ b/src/components/common/forms/TextField.tsx @@ -2,7 +2,6 @@ import { FormField, FormFieldProps, useConfigure } from './FormField' import styles from './FormField.module.css' import cx from 'classnames' import { - ChangeEvent, HTMLInputAutoCompleteAttribute, InputEvent, useCallback, From ce5a27136c522f8037728abbcf23585fccbb9226 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:22:33 -0600 Subject: [PATCH 07/36] Medium Hanging Fruit --- .gitignore | 1 + src/app/login/LoginCard.tsx | 10 ++++++---- src/app/volunteer/VolunteerPage.tsx | 2 +- src/components/common/ToolTip.tsx | 6 ++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index f3e2fa02..27843699 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ node_modules/ #misc .direnv shell.nix +.eslintcache diff --git a/src/app/login/LoginCard.tsx b/src/app/login/LoginCard.tsx index e3e6a51b..c4ac4d03 100644 --- a/src/app/login/LoginCard.tsx +++ b/src/app/login/LoginCard.tsx @@ -3,6 +3,7 @@ import { useAuth } from '@/util/hooks' import Image from 'next/image' import { useSearchParams } from 'next/navigation' +import { useEffect } from 'react' export function LoginCard() { const { session, onLogin } = useAuth() @@ -19,10 +20,11 @@ export function LoginCard() { ? 'An unknown error occurred. Please try again later.' : null - if (session) { - window.location.href = redirect - return null - } + useEffect(() => { + if (session) { + globalThis.location.href = redirect + } + }, [session, redirect]) return (
diff --git a/src/app/volunteer/VolunteerPage.tsx b/src/app/volunteer/VolunteerPage.tsx index fa4cae46..f24d4858 100644 --- a/src/app/volunteer/VolunteerPage.tsx +++ b/src/app/volunteer/VolunteerPage.tsx @@ -181,7 +181,7 @@ export default function VolunteerPage() { if (isSessionLoading) return null if (!session) { - window.location.href = '/login?redirect=/volunteer' + globalThis.location.replace('/login?redirect=/volunteer'); return null } diff --git a/src/components/common/ToolTip.tsx b/src/components/common/ToolTip.tsx index c61665d9..a713fbd8 100644 --- a/src/components/common/ToolTip.tsx +++ b/src/components/common/ToolTip.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef, RefObject } from 'react' +import { RefObject, useEffect, useRef, useState } from 'react' /* preliminary notes: component that generates a button which upon being clicked displays an @@ -52,7 +52,9 @@ export function ToolTip({ setCloserList(closers) }, [open]) - useOutsideDetector(wrapperRef) + // For code quality, an effect is wrapped in a function. + // ESlint can't verify this function call as legal though, so we need to help it (pulling it out of the function passes cleanly) + useOutsideDetector(wrapperRef) //eslint-disable-line react-hooks/refs closerList?.map((element: Element) => { if (element instanceof HTMLButtonElement) { From 0d311d7fdc739789e6c686c1f7b44da2859edf97 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:24:04 -0600 Subject: [PATCH 08/36] Medium Hanging Fruit but I might be misunderstanding why we were trying to Memoize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✖ 10 problems (0 errors, 10 warnings) - All `react-hooks/set-state-in-effect` --- src/components/common/forms/FormField.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/common/forms/FormField.tsx b/src/components/common/forms/FormField.tsx index babf800a..4cc4005b 100644 --- a/src/components/common/forms/FormField.tsx +++ b/src/components/common/forms/FormField.tsx @@ -1,5 +1,5 @@ import styles from './FormField.module.css' -import { ReactElement, useEffect, useMemo } from 'react' +import { ReactElement, useCallback, useEffect, useMemo } from 'react' /** * Properties for the FormField component. All form fields extend their @@ -167,7 +167,7 @@ export function useConfigure( // Use the provided getter if available, or default to `form[props.field]`. // If no field is provided either, be sad and return undefined. - const getter = useMemo(() => { + const getter = useCallback(() => { const getter = props.getter if (getter) return getter @@ -180,7 +180,7 @@ export function useConfigure( // Use the provided setter if available, or default to `form[props.field]`. // If no field is provided either, be sad and return undefined. - const setter = useMemo(() => { + const setter = useCallback(() => { const setter = props.setter if (setter) return setter From 030fd8729f5549fc1245934ef132ef82e3e8efe0 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:11:59 -0600 Subject: [PATCH 09/36] Revert "Medium Hanging Fruit but I might be misunderstanding why we were trying to Memoize" This reverts commit 0d311d7fdc739789e6c686c1f7b44da2859edf97. This is the one seeming to cause the problems it seems. (hopefully I'm right and it will go back to 12 for the CI as well) --- src/components/common/forms/FormField.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/common/forms/FormField.tsx b/src/components/common/forms/FormField.tsx index 4cc4005b..babf800a 100644 --- a/src/components/common/forms/FormField.tsx +++ b/src/components/common/forms/FormField.tsx @@ -1,5 +1,5 @@ import styles from './FormField.module.css' -import { ReactElement, useCallback, useEffect, useMemo } from 'react' +import { ReactElement, useEffect, useMemo } from 'react' /** * Properties for the FormField component. All form fields extend their @@ -167,7 +167,7 @@ export function useConfigure( // Use the provided getter if available, or default to `form[props.field]`. // If no field is provided either, be sad and return undefined. - const getter = useCallback(() => { + const getter = useMemo(() => { const getter = props.getter if (getter) return getter @@ -180,7 +180,7 @@ export function useConfigure( // Use the provided setter if available, or default to `form[props.field]`. // If no field is provided either, be sad and return undefined. - const setter = useCallback(() => { + const setter = useMemo(() => { const setter = props.setter if (setter) return setter From 01961bf7576e0f9e4e03f816fd4af78d394e7c5e Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:26:41 -0600 Subject: [PATCH 10/36] run formatter :( --- package.json | 7 +- public/sitemap-0.xml | 40 ++++----- src/app/admin/layout/List.tsx | 8 +- src/app/admin/panels/fundraising/page.tsx | 5 +- .../useFundraisingDashboardController.ts | 2 +- .../panels/members/panel_views/DonorView.tsx | 4 +- src/app/volunteer/VolunteerPage.tsx | 2 +- src/components/common/ToolTip.tsx | 2 +- src/components/common/forms/TextField.tsx | 6 +- tsconfig.json | 84 ++++++++----------- 10 files changed, 77 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index a36ad2d7..df1c3584 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,11 @@ "overrides": { "@babel/runtime@<7.26.10": ">=7.26.10", "undici@>=6.0.0 <6.21.2": ">=6.21.2" - } + }, + "onlyBuiltDependencies": [ + "esbuild", + "sharp", + "unrs-resolver" + ] } } diff --git a/public/sitemap-0.xml b/public/sitemap-0.xml index 86a9ab5e..102c68e3 100644 --- a/public/sitemap-0.xml +++ b/public/sitemap-0.xml @@ -1,23 +1,23 @@ -https://www.progressivevictory.win2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/about2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/account2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/admin2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/admin/panels/contributions2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/admin/panels/donors2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/admin/panels/fundraising2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/admin/panels/members2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/admin/panels/permissions2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/admin/panels/roles2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/blog2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/endorsements2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/events2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/initiative2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/initiatives/franklin2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/initiatives/section-2302026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/login2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/membership_support2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/privacy2026-06-19T14:03:29.861Zdaily0.7 -https://www.progressivevictory.win/volunteer2026-06-19T14:03:29.861Zdaily0.7 +https://www.progressivevictory.win2026-06-21T21:44:13.201Zdaily0.7 +https://www.progressivevictory.win/about2026-06-21T21:44:13.201Zdaily0.7 +https://www.progressivevictory.win/account2026-06-21T21:44:13.201Zdaily0.7 +https://www.progressivevictory.win/admin2026-06-21T21:44:13.201Zdaily0.7 +https://www.progressivevictory.win/admin/panels/contributions2026-06-21T21:44:13.201Zdaily0.7 +https://www.progressivevictory.win/admin/panels/donors2026-06-21T21:44:13.201Zdaily0.7 +https://www.progressivevictory.win/admin/panels/fundraising2026-06-21T21:44:13.201Zdaily0.7 +https://www.progressivevictory.win/admin/panels/members2026-06-21T21:44:13.201Zdaily0.7 +https://www.progressivevictory.win/admin/panels/permissions2026-06-21T21:44:13.201Zdaily0.7 +https://www.progressivevictory.win/admin/panels/roles2026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/blog2026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/endorsements2026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/events2026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/initiative2026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/initiatives/franklin2026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/initiatives/section-2302026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/login2026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/membership_support2026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/privacy2026-06-21T21:44:13.202Zdaily0.7 +https://www.progressivevictory.win/volunteer2026-06-21T21:44:13.202Zdaily0.7 \ No newline at end of file diff --git a/src/app/admin/layout/List.tsx b/src/app/admin/layout/List.tsx index 21c2ac51..2e4239cd 100644 --- a/src/app/admin/layout/List.tsx +++ b/src/app/admin/layout/List.tsx @@ -473,20 +473,20 @@ function PageSelect({ onChange, }: PageSelectProps) { const [value, setValue] = useState('') - + const pageCount = Math.ceil(count / pageSize) const canNavigate = pageCount > 1 const maxPage = pageCount - 1 - + const handleChangeValue = (value: string) => { if (!value || (/^\d+$/.test(value) && value.length < 10)) setValue(value) } - + const handleSubmit = () => { const newPage = +value - 1 if (0 <= newPage && newPage <= maxPage) onChange(newPage) - else setValue((page + 1).toString()) + else setValue((page + 1).toString()) } const pageStr = (page + 1).toString() if (value != pageStr) { diff --git a/src/app/admin/panels/fundraising/page.tsx b/src/app/admin/panels/fundraising/page.tsx index 0496c7b3..091ad9a2 100644 --- a/src/app/admin/panels/fundraising/page.tsx +++ b/src/app/admin/panels/fundraising/page.tsx @@ -545,7 +545,10 @@ export default function Page() { if (inclusiveRangeMs < oneDayMs) { return } - console.log("Applying chart view override range:", { startIso, endIso }) + console.log( + 'Applying chart view override range:', + { startIso, endIso } + ) applyChartViewOverrideRange({ startIso, endIso, diff --git a/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts b/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts index ddedc93c..7c8d04fd 100644 --- a/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts +++ b/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts @@ -338,7 +338,7 @@ export function useFundraisingDashboardController(onGet: OnGet) { setChartViewOverrideRange(null) return } - console.debug("Applying chart view override range:", range) + console.debug('Applying chart view override range:', range) setChartViewOverrideRange(range) } diff --git a/src/app/admin/panels/members/panel_views/DonorView.tsx b/src/app/admin/panels/members/panel_views/DonorView.tsx index dd0d9603..e3c1de09 100644 --- a/src/app/admin/panels/members/panel_views/DonorView.tsx +++ b/src/app/admin/panels/members/panel_views/DonorView.tsx @@ -139,8 +139,8 @@ export function DonorView({ const [overlayMounted, setOverlayMounted] = useState(false) //const [overlayOpen, setOverlayOpen] = useState(false) - const overlayOpen = false; // For consistency since setOverlay open was only used in the seemingly unused effect - console.debug("Picking Donor? : ", pickingDonor); + const overlayOpen = false // For consistency since setOverlay open was only used in the seemingly unused effect + console.debug('Picking Donor? : ', pickingDonor) // useEffect(() => { // if (pickingDonor) { // setOverlayMounted(true) diff --git a/src/app/volunteer/VolunteerPage.tsx b/src/app/volunteer/VolunteerPage.tsx index f24d4858..becafc69 100644 --- a/src/app/volunteer/VolunteerPage.tsx +++ b/src/app/volunteer/VolunteerPage.tsx @@ -181,7 +181,7 @@ export default function VolunteerPage() { if (isSessionLoading) return null if (!session) { - globalThis.location.replace('/login?redirect=/volunteer'); + globalThis.location.replace('/login?redirect=/volunteer') return null } diff --git a/src/components/common/ToolTip.tsx b/src/components/common/ToolTip.tsx index a713fbd8..de1f1e50 100644 --- a/src/components/common/ToolTip.tsx +++ b/src/components/common/ToolTip.tsx @@ -52,7 +52,7 @@ export function ToolTip({ setCloserList(closers) }, [open]) - // For code quality, an effect is wrapped in a function. + // For code quality, an effect is wrapped in a function. // ESlint can't verify this function call as legal though, so we need to help it (pulling it out of the function passes cleanly) useOutsideDetector(wrapperRef) //eslint-disable-line react-hooks/refs diff --git a/src/components/common/forms/TextField.tsx b/src/components/common/forms/TextField.tsx index 4ca1432d..e442174b 100644 --- a/src/components/common/forms/TextField.tsx +++ b/src/components/common/forms/TextField.tsx @@ -1,11 +1,7 @@ import { FormField, FormFieldProps, useConfigure } from './FormField' import styles from './FormField.module.css' import cx from 'classnames' -import { - HTMLInputAutoCompleteAttribute, - InputEvent, - useCallback, -} from 'react' +import { HTMLInputAutoCompleteAttribute, InputEvent, useCallback } from 'react' export interface TextFieldProps extends FormFieldProps< T, diff --git a/tsconfig.json b/tsconfig.json index b803adad..75bbabbb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,50 +1,40 @@ { - "compilerOptions": { - "target": "ESNext", - "lib": [ - "dom", - "dom.iterable", - "esnext" + "compilerOptions": { + "target": "ESNext", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noImplicitAny": false, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"], + "auth": ["./src/util/auth"] + } + }, + "include": [ + "process.d.ts", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "**/*.mjs", + "**/*.js", + "vitest.config.mts", + ".next/dev/types/**/*.ts" ], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noEmit": true, - "esModuleInterop": true, - "module": "ESNext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noImplicitAny": false, - "jsx": "react-jsx", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": [ - "./src/*" - ], - "auth": [ - "./src/util/auth" - ] - } - }, - "include": [ - "process.d.ts", - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - "**/*.mjs", - "**/*.js", - "vitest.config.mts", - ".next/dev/types/**/*.ts" - ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From 0d447f4cfe9884646ce45e51b41aacaedcce8d90 Mon Sep 17 00:00:00 2001 From: roguediv <47837581+roguediv@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:50:13 -0500 Subject: [PATCH 11/36] Feature/added mobile dontate button to main header (#435) (#442) * Feature/added mobile dontate button to main header (#435) * installed index lint plugin * Task/update eslint (#446) * fixed most linting issues * fixed header lint issue * lint fix on date range picker needs testing * fixed most of the lint issues * added contracts repo (#447) * Rebasing with dev --------- Co-authored-by: sH3llH0und --- src/components/layout/Header.tsx | 59 ++++++++++++++++--------- src/components/layout/header.module.css | 35 +++++++++++++++ 2 files changed, 73 insertions(+), 21 deletions(-) diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 9bcdcc0e..abbaa95e 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -838,29 +838,46 @@ export function Header() { )}
- { - closeSubnav() - setIsOpen((prev) => !prev) +
- - + + + { + closeSubnav() + setIsOpen((prev) => !prev) + }} + initial={false} + animate={isOpen ? 'open' : 'closed'} + whileHover="hover" + whileTap="tap" + variants={menuButtonVariants} + transition={{ + type: 'spring', + stiffness: 520, + damping: 32, + }} + aria-label={ + isOpen + ? 'Close navigation menu' + : 'Open navigation menu' + } + aria-expanded={isOpen} + aria-controls="site-nav-drawer" + > + + +
diff --git a/src/components/layout/header.module.css b/src/components/layout/header.module.css index b846d89d..8ac0ef80 100644 --- a/src/components/layout/header.module.css +++ b/src/components/layout/header.module.css @@ -336,3 +336,38 @@ text-overflow: ellipsis; white-space: nowrap; } + +.mobileDonateButton { + @media (min-width: 1280px) { + display: none; + } + animation: fadeInUp 0.1s ease forwards; +} + +.fadeOutTop { + animation: fadeOutUp 0.25s ease forwards; +} + +@keyframes fadeOutUp { + from { + opacity: 1; + transform: translateY(0); + } + + to { + opacity: 0; + transform: translateY(-15px); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(-15px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} From 3f411c0cd1c0b8852d0cbd762040b7728c7e521e Mon Sep 17 00:00:00 2001 From: roguediv <47837581+roguediv@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:01:49 -0500 Subject: [PATCH 12/36] updated the linter and fixed a couple linter errors (#452) --- eslint.config.js | 25 ++++++++++++--------- src/components/common/ImageWithFallback.tsx | 17 +++++--------- src/components/layout/Header.tsx | 15 +++++++------ src/util/hooks/useHydration.ts | 17 +++++++------- vitest.config.mts | 4 +++- 5 files changed, 40 insertions(+), 38 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index fe7594cd..83217d3c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,18 +31,23 @@ export default defineConfig([ }, { rules: { + // Show stoppers + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-argument': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + + // Keep track of 'object-shorthand': 'warn', - '@typescript-eslint/no-unsafe-assignment': 'warn', - '@typescript-eslint/no-unsafe-member-access': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn', - '@typescript-eslint/no-unsafe-enum-comparison': 'off', - '@typescript-eslint/no-unsafe-call': 'warn', '@typescript-eslint/no-unsafe-return': 'warn', + '@typescript-eslint/no-unsafe-enum-comparison': 'off', 'tailwindcss/no-custom-classname': 'off', - 'react-hooks/set-state-in-effect': 'warn', //temp - 'react-hooks/preserve-manual-memoization': 'warn', //temp - 'react-hooks/refs': 'warn', //temp - 'react-hooks/immutability': 'warn', //temp + + // React Compiler rules (for testing) + 'react-hooks/set-state-in-effect': 'off', // 7 look functional or good 2026-06-22 + 'react-hooks/preserve-manual-memoization': 'off', // 2 look like valid patterns 2026-06-22 + 'react-hooks/refs': 'warn', + 'react-hooks/immutability': 'warn', }, }, { @@ -50,4 +55,4 @@ export default defineConfig([ extends: [tseslint.configs.disableTypeChecked], }, eslintConfigPrettier, -]) +]) \ No newline at end of file diff --git a/src/components/common/ImageWithFallback.tsx b/src/components/common/ImageWithFallback.tsx index e4c2fed8..9b4db72f 100644 --- a/src/components/common/ImageWithFallback.tsx +++ b/src/components/common/ImageWithFallback.tsx @@ -2,7 +2,7 @@ import cx from 'classnames' import Image from 'next/image' -import { useEffect, useState } from 'react' +import { useState } from 'react' export interface ImageWithFallbackProps { src: string @@ -26,18 +26,13 @@ export function ImageWithFallback({ priority = false, }: ImageWithFallbackProps) { const [hasErrored, setHasErrored] = useState(false) - - useEffect(() => { - setHasErrored(false) - }, [src, useFallback]) + const fallbackSrc = 'https://dummyjson.com/image/100x100/e8e0e0/d0c8c8?text=!&fontFamily=Poppins' + const finalSrc = hasErrored || useFallback ? fallbackSrc : src return ( {alt} ) -} +} \ No newline at end of file diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index d2f8cee3..5b9653e3 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -616,6 +616,11 @@ export function Header() { null ) + const closeNav = () => { + setIsOpen(false); + setMobileSubnavItem(null) + } + const { data: discordUsers } = useQuery({ queryKey: [`/discordUsers/${session?.userId}`], queryFn: @@ -633,7 +638,7 @@ export function Header() { if (!isOpen) return const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') setIsOpen(false) + if (e.key === 'Escape') closeNav(); } document.addEventListener('keydown', onKeyDown) @@ -654,10 +659,6 @@ export function Header() { return () => document.removeEventListener('keydown', onKeyDown) }, []) - useEffect(() => { - if (!isOpen) setMobileSubnavItem(null) - }, [isOpen]) - useEffect(() => { if (typeof window === 'undefined') return @@ -665,7 +666,7 @@ export function Header() { const syncToBreakpoint = () => { if (desktopMQ.matches) { - setIsOpen(false) + closeNav() } else { setActiveSubnav(null) } @@ -920,7 +921,7 @@ export function Header() { WebkitBackdropFilter: 'blur(10px)', backgroundColor: 'rgba(0, 0, 0, 0.18)', }} - onClick={() => setIsOpen(false)} + onClick={() => closeNav()} /> )} diff --git a/src/util/hooks/useHydration.ts b/src/util/hooks/useHydration.ts index 62e58610..f83d9a1b 100644 --- a/src/util/hooks/useHydration.ts +++ b/src/util/hooks/useHydration.ts @@ -1,11 +1,10 @@ -import { useEffect, useState } from 'react' +import { useSyncExternalStore } from 'react' + export function useHydration() { - const [hydrated, setHydrated] = useState(false) - - useEffect(() => { - setHydrated(true) - }, []) - - return hydrated -} + return useSyncExternalStore( + () => () => null, + () => true, + () => false + ) +} \ No newline at end of file diff --git a/vitest.config.mts b/vitest.config.mts index 82f8630d..8c18e494 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -18,7 +18,9 @@ const tstConfig = testConfig({ }, }) -export default { +const mergedConfig = { ...config, ...tstConfig, } + +export default mergedConfig \ No newline at end of file From 363f3e4cc2205cb78d9e24c7504832c2bbbec76c Mon Sep 17 00:00:00 2001 From: roguediv <47837581+roguediv@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:07:30 -0500 Subject: [PATCH 13/36] ran prettier (#452) --- eslint.config.js | 2 +- src/components/common/ImageWithFallback.tsx | 5 +++-- src/components/layout/Header.tsx | 4 ++-- src/util/hooks/useHydration.ts | 3 +-- vitest.config.mts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 83217d3c..bd796fe1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -55,4 +55,4 @@ export default defineConfig([ extends: [tseslint.configs.disableTypeChecked], }, eslintConfigPrettier, -]) \ No newline at end of file +]) diff --git a/src/components/common/ImageWithFallback.tsx b/src/components/common/ImageWithFallback.tsx index 9b4db72f..65cfb8ad 100644 --- a/src/components/common/ImageWithFallback.tsx +++ b/src/components/common/ImageWithFallback.tsx @@ -26,7 +26,8 @@ export function ImageWithFallback({ priority = false, }: ImageWithFallbackProps) { const [hasErrored, setHasErrored] = useState(false) - const fallbackSrc = 'https://dummyjson.com/image/100x100/e8e0e0/d0c8c8?text=!&fontFamily=Poppins' + const fallbackSrc = + 'https://dummyjson.com/image/100x100/e8e0e0/d0c8c8?text=!&fontFamily=Poppins' const finalSrc = hasErrored || useFallback ? fallbackSrc : src return ( @@ -42,4 +43,4 @@ export function ImageWithFallback({ priority={priority} /> ) -} \ No newline at end of file +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 5b9653e3..62511375 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -617,7 +617,7 @@ export function Header() { ) const closeNav = () => { - setIsOpen(false); + setIsOpen(false) setMobileSubnavItem(null) } @@ -638,7 +638,7 @@ export function Header() { if (!isOpen) return const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') closeNav(); + if (e.key === 'Escape') closeNav() } document.addEventListener('keydown', onKeyDown) diff --git a/src/util/hooks/useHydration.ts b/src/util/hooks/useHydration.ts index f83d9a1b..8ab97d51 100644 --- a/src/util/hooks/useHydration.ts +++ b/src/util/hooks/useHydration.ts @@ -1,10 +1,9 @@ import { useSyncExternalStore } from 'react' - export function useHydration() { return useSyncExternalStore( () => () => null, () => true, () => false ) -} \ No newline at end of file +} diff --git a/vitest.config.mts b/vitest.config.mts index 8c18e494..b703e726 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -23,4 +23,4 @@ const mergedConfig = { ...tstConfig, } -export default mergedConfig \ No newline at end of file +export default mergedConfig From 9a0bbcb84bbe3565868628a60e050b38cf0f8a69 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:58:29 -0600 Subject: [PATCH 14/36] Why did the merge do that. "0 warnings" --- .../admin/panels/members/panel_views/DonorView.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/app/admin/panels/members/panel_views/DonorView.tsx b/src/app/admin/panels/members/panel_views/DonorView.tsx index fe89791d..8738782c 100644 --- a/src/app/admin/panels/members/panel_views/DonorView.tsx +++ b/src/app/admin/panels/members/panel_views/DonorView.tsx @@ -8,20 +8,15 @@ import { FormGroup, TextField, } from '@/components/common/forms' -import { FetchError } from '@/models' -import type { UseQueryResult } from '@tanstack/react-query' -import cx from 'classnames' -import { motion } from 'motion/react' -import Link from 'next/link' import { ActBlueContribution, ActBlueContributionCustomField, ActBlueDonor, ActBlueLineitem, User, -} from '@/contracts/data' -import type { SearchRequest } from '@/contracts/requests' -import type { PaginatedResponse } from '@/contracts/responses' +} from 'pv-contracts/data' +import type { SearchRequest } from 'pv-contracts/requests' +import type { PaginatedResponse } from 'pv-contracts/responses' import { FetchError } from '@/models' import type { UseQueryResult } from '@tanstack/react-query' import cx from 'classnames' From b083f9914534e9084366135785006f574a7dc29c Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:49:07 -0600 Subject: [PATCH 15/36] Fixed 2 `preserve-manual-memoization` errors. 0 Problems, 7 `set-state` ignored The compiler failing to use our manual memoization is a genuine issue, and is something we wouldn't want to miss --- eslint.config.js | 1 - .../panels/members/panel_views/DonorView.tsx | 10 ++-- src/components/common/forms/FormField.tsx | 47 +++++++++++-------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index bd796fe1..ddb6b5f5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -45,7 +45,6 @@ export default defineConfig([ // React Compiler rules (for testing) 'react-hooks/set-state-in-effect': 'off', // 7 look functional or good 2026-06-22 - 'react-hooks/preserve-manual-memoization': 'off', // 2 look like valid patterns 2026-06-22 'react-hooks/refs': 'warn', 'react-hooks/immutability': 'warn', }, diff --git a/src/app/admin/panels/members/panel_views/DonorView.tsx b/src/app/admin/panels/members/panel_views/DonorView.tsx index 8738782c..a3fb10e4 100644 --- a/src/app/admin/panels/members/panel_views/DonorView.tsx +++ b/src/app/admin/panels/members/panel_views/DonorView.tsx @@ -8,6 +8,11 @@ import { FormGroup, TextField, } from '@/components/common/forms' +import { FetchError } from '@/models' +import type { UseQueryResult } from '@tanstack/react-query' +import cx from 'classnames' +import { motion } from 'motion/react' +import Link from 'next/link' import { ActBlueContribution, ActBlueContributionCustomField, @@ -17,11 +22,6 @@ import { } from 'pv-contracts/data' import type { SearchRequest } from 'pv-contracts/requests' import type { PaginatedResponse } from 'pv-contracts/responses' -import { FetchError } from '@/models' -import type { UseQueryResult } from '@tanstack/react-query' -import cx from 'classnames' -import { motion } from 'motion/react' -import Link from 'next/link' import React, { ChangeEvent, useMemo, useState } from 'react' export interface DonorViewProps { diff --git a/src/components/common/forms/FormField.tsx b/src/components/common/forms/FormField.tsx index babf800a..b4e60579 100644 --- a/src/components/common/forms/FormField.tsx +++ b/src/components/common/forms/FormField.tsx @@ -1,5 +1,5 @@ import styles from './FormField.module.css' -import { ReactElement, useEffect, useMemo } from 'react' +import { ReactElement, useCallback, useEffect, useMemo } from 'react' /** * Properties for the FormField component. All form fields extend their @@ -165,34 +165,41 @@ export function useConfigure( const id = props.id const onConfigure = props.dynamic?.onConfigure + // Props object changing will re-trigger Callback, so we need to grab the values beforehand + // So that we only regenerate on change + const propGetter = props.getter + const propSetter = props.setter + const propField = props.field // Use the provided getter if available, or default to `form[props.field]`. // If no field is provided either, be sad and return undefined. - const getter = useMemo(() => { - const getter = props.getter - if (getter) return getter + const getter = useCallback( + (form: FormType): FieldType | undefined => { + if (propGetter) return propGetter(form) - const key = props.field - if (key) - return (form: FormType) => (form as Record)[key] - - return () => undefined - }, [props.getter, props.field]) + const key = propField + if (!key) return undefined + return (form as Record)[key] + }, + [propGetter, propField] + ) // Use the provided setter if available, or default to `form[props.field]`. // If no field is provided either, be sad and return undefined. - const setter = useMemo(() => { - const setter = props.setter - if (setter) return setter - const key = props.field - if (key) - return (form: FormType, field: FieldType) => ({ + const setter = useCallback( + (form: FormType, field: FieldType): FormType => { + if (propSetter) return propSetter(form, field) + + const key = propField + if (!key) return form + + return { ...form, [key]: field, - }) - - return (form: FormType) => form - }, [props.setter, props.field]) + } + }, + [propSetter, propField] + ) // Use the provided validator if available, or default to the, well, // default one. We can't be sad here because it can't be undefined. From 96296c687abc6c0ea438ea622dcf9221618da100 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:01:54 -0600 Subject: [PATCH 16/36] Manually ignore each instance of `set-state-in-effect` rather than globally ignoring. This is not a good thing. We might not deem it necessary to fix right now, but it's still a problem. So use line ignores so more instances don't happen, and so that they all eventually can be fixed --- eslint.config.js | 3 +-- src/app/home/MemberBanner.tsx | 3 ++- src/app/home/Volunteer.tsx | 5 +++-- src/app/volunteer/CollectInfoStage.tsx | 4 +++- src/app/volunteer/PhoneVerifyStage.tsx | 3 ++- src/app/volunteer/VolunteerPage.tsx | 3 ++- src/components/common/charts/DualAxisBarLineChart.tsx | 3 ++- src/components/common/mini_calendar/DateRangePicker.tsx | 3 ++- 8 files changed, 17 insertions(+), 10 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index ddb6b5f5..75c24132 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -40,11 +40,10 @@ export default defineConfig([ // Keep track of 'object-shorthand': 'warn', '@typescript-eslint/no-unsafe-return': 'warn', - '@typescript-eslint/no-unsafe-enum-comparison': 'off', + '@typescript-eslint/no-unsafe-enum-comparison': 'warn', 'tailwindcss/no-custom-classname': 'off', // React Compiler rules (for testing) - 'react-hooks/set-state-in-effect': 'off', // 7 look functional or good 2026-06-22 'react-hooks/refs': 'warn', 'react-hooks/immutability': 'warn', }, diff --git a/src/app/home/MemberBanner.tsx b/src/app/home/MemberBanner.tsx index 71e9c769..1fb59054 100644 --- a/src/app/home/MemberBanner.tsx +++ b/src/app/home/MemberBanner.tsx @@ -266,7 +266,8 @@ export function MemberBanner() { useEffect(() => { try { - if (inView) setVisible(true) + // eslint-disable-next-line react-hooks/set-state-in-effect + if (inView) setVisible(true) // 6/23/26 - Not worth addressing } catch (err) { safeLogError(err, 'inView effect error:') } diff --git a/src/app/home/Volunteer.tsx b/src/app/home/Volunteer.tsx index fe1cf3be..3fbda99c 100644 --- a/src/app/home/Volunteer.tsx +++ b/src/app/home/Volunteer.tsx @@ -3,7 +3,7 @@ import styles from './volunteer.module.css' import { motion } from 'motion/react' import Image from 'next/image' -import { useState, useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' const actions = [ { @@ -36,7 +36,8 @@ export function Volunteer() { }, [observe]) useEffect(() => { - if (inView) setVisible(true) + // eslint-disable-next-line react-hooks/set-state-in-effect + if (inView) setVisible(true) // 6/23/26 - Not worth addressing }, [inView]) return ( diff --git a/src/app/volunteer/CollectInfoStage.tsx b/src/app/volunteer/CollectInfoStage.tsx index 860334c1..61b73f38 100644 --- a/src/app/volunteer/CollectInfoStage.tsx +++ b/src/app/volunteer/CollectInfoStage.tsx @@ -4,7 +4,7 @@ import { useFetch, useInit } from '@/util/hooks' import Link from 'next/link' import phone from 'phone' import { Country, isValidCountryPostalCode } from 'postal-code-validator' -import { zLocation, Location } from 'pv-contracts/data' +import { Location, zLocation } from 'pv-contracts/data' import { useCallback, useEffect, useMemo, useState } from 'react' export interface IOnboardingForm { @@ -99,7 +99,9 @@ export function CollectInfoStage({ useEffect(() => { if (parsedPhone.isValid) + // eslint-disable-next-line react-hooks/set-state-in-effect setForm((f) => ({ + // 6/23/26 - Not worth addressing ...f, phoneNumber: parsedPhone.phoneNumber.substring(2), })) diff --git a/src/app/volunteer/PhoneVerifyStage.tsx b/src/app/volunteer/PhoneVerifyStage.tsx index d24727a5..24d2f454 100644 --- a/src/app/volunteer/PhoneVerifyStage.tsx +++ b/src/app/volunteer/PhoneVerifyStage.tsx @@ -48,7 +48,8 @@ export function PhoneVerifyStage({ } useEffect(() => { - setCodeTimer(getCodeTime()) + // eslint-disable-next-line react-hooks/set-state-in-effect + setCodeTimer(getCodeTime()) // 6/23/26 - Not worth addressing const interval = setInterval(() => { setCodeTimer(getCodeTime()) }, 200) diff --git a/src/app/volunteer/VolunteerPage.tsx b/src/app/volunteer/VolunteerPage.tsx index faef43d4..7d6c051a 100644 --- a/src/app/volunteer/VolunteerPage.tsx +++ b/src/app/volunteer/VolunteerPage.tsx @@ -174,7 +174,8 @@ export default function VolunteerPage() { !user.data?.address?.zip || !user.data?.phone) ) { - setOverrideStage(OnboardingStage.NOT_STARTED) + // eslint-disable-next-line react-hooks/set-state-in-effect + setOverrideStage(OnboardingStage.NOT_STARTED) // 6/23/26 - Not worth addressing } }, [user.data, currentStage]) diff --git a/src/components/common/charts/DualAxisBarLineChart.tsx b/src/components/common/charts/DualAxisBarLineChart.tsx index 02d45abe..03cc1106 100644 --- a/src/components/common/charts/DualAxisBarLineChart.tsx +++ b/src/components/common/charts/DualAxisBarLineChart.tsx @@ -239,7 +239,8 @@ export function Chart({ useLayoutEffect(() => { if (hoverIdx == null || !hoverPos) { - setTooltipPos(null) + // eslint-disable-next-line react-hooks/set-state-in-effect + setTooltipPos(null) // 6/23/26 - Not worth addressing return } diff --git a/src/components/common/mini_calendar/DateRangePicker.tsx b/src/components/common/mini_calendar/DateRangePicker.tsx index 707c4b4b..393d85e5 100644 --- a/src/components/common/mini_calendar/DateRangePicker.tsx +++ b/src/components/common/mini_calendar/DateRangePicker.tsx @@ -36,7 +36,8 @@ export function DateRangePicker({ if (!startDate) return const anchor = new Date(startDate) if (Number.isNaN(anchor.getTime())) return - setCalendarMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1)) + // eslint-disable-next-line react-hooks/set-state-in-effect + setCalendarMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1)) // 6/23/26 - Not worth addressing }, [startDate]) const todayInputValue = useMemo( From 94ac242ef53de7a0c4d9859ea92a5616623a586c Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:14:19 -0600 Subject: [PATCH 17/36] Set max warnings to 0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb7e7c36..c37882c4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 26 .", + "lint-ci": "eslint --max-warnings 0 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." From 57dfab16b204695816119be5b9367aaf2ce20a57 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:32:29 -0600 Subject: [PATCH 18/36] 6 --- src/app/home/Volunteer.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/home/Volunteer.tsx b/src/app/home/Volunteer.tsx index 3fbda99c..ab601ccb 100644 --- a/src/app/home/Volunteer.tsx +++ b/src/app/home/Volunteer.tsx @@ -35,10 +35,7 @@ export function Volunteer() { if (divRef.current) observe(divRef.current) }, [observe]) - useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect - if (inView) setVisible(true) // 6/23/26 - Not worth addressing - }, [inView]) + if (!visible && inView) setVisible(true) return (
From 2d8147dda6ee2ed3b4c6f7f7dbe80c6fcd0e5532 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:35:01 -0600 Subject: [PATCH 19/36] 5 --- src/app/volunteer/VolunteerPage.tsx | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/app/volunteer/VolunteerPage.tsx b/src/app/volunteer/VolunteerPage.tsx index 7d6c051a..76494461 100644 --- a/src/app/volunteer/VolunteerPage.tsx +++ b/src/app/volunteer/VolunteerPage.tsx @@ -29,7 +29,7 @@ import { DiscordUserIsInServerResponse, zDiscordUserIsInServerResponse, } from 'pv-contracts/responses' -import { useEffect, useState } from 'react' +import { useState } from 'react' import z from 'zod' export default function VolunteerPage() { @@ -165,19 +165,18 @@ export default function VolunteerPage() { const currentStage = overrideStage ?? user.data?.onboardingStage - useEffect(() => { - if ( - currentStage === OnboardingStage.JOINED && - (!user.data?.firstName || - !user.data?.lastName || - !user.data?.birthdate || - !user.data?.address?.zip || - !user.data?.phone) - ) { - // eslint-disable-next-line react-hooks/set-state-in-effect - setOverrideStage(OnboardingStage.NOT_STARTED) // 6/23/26 - Not worth addressing - } - }, [user.data, currentStage]) + // useEffect(() => { + if ( + currentStage === OnboardingStage.JOINED && + (!user.data?.firstName || + !user.data?.lastName || + !user.data?.birthdate || + !user.data?.address?.zip || + !user.data?.phone) + ) { + setOverrideStage(OnboardingStage.NOT_STARTED) + } + // }, [user.data, currentStage]) if (isSessionLoading) return null From 247b7f526247545c791da353ba6fdfc962ef3276 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:47:08 -0600 Subject: [PATCH 20/36] 4 - [ ] `components\common\mini_calendar\DateRangePicker.tsx: 39` - [ ] `app\volunteer\PhoneVerifyStage.tsx: 51` - [ ] `app\home\MemberBanner.tsx: 270` - [ ] `app\volunteer\CollectInfoStage.tsx: 102` --- src/components/common/charts/DualAxisBarLineChart.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/common/charts/DualAxisBarLineChart.tsx b/src/components/common/charts/DualAxisBarLineChart.tsx index 03cc1106..1765b61c 100644 --- a/src/components/common/charts/DualAxisBarLineChart.tsx +++ b/src/components/common/charts/DualAxisBarLineChart.tsx @@ -237,13 +237,13 @@ export function Chart({ } }, []) + if (tooltipPos && (hoverIdx == null || !hoverPos)) { + setTooltipPos(null) + } useLayoutEffect(() => { if (hoverIdx == null || !hoverPos) { - // eslint-disable-next-line react-hooks/set-state-in-effect - setTooltipPos(null) // 6/23/26 - Not worth addressing return } - const wrapNode = wrapRef.current const tooltipNode = tooltipRef.current if (!wrapNode || !tooltipNode) return From 17b026f4099feb495a4a0536b35acc96622f5149 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:17:30 -0600 Subject: [PATCH 21/36] Ok merge issues fixed 1 effect state was removed, but a new one was added :( --- .../sections/AccountMembershipSection.tsx | 3 ++- src/app/volunteer/CollectInfoStage.tsx | 1 - src/app/volunteer/VolunteerPage.tsx | 10 ---------- vitest.config.mts | 18 ++---------------- 4 files changed, 4 insertions(+), 28 deletions(-) diff --git a/src/app/account/sections/AccountMembershipSection.tsx b/src/app/account/sections/AccountMembershipSection.tsx index 3371d295..271130e6 100644 --- a/src/app/account/sections/AccountMembershipSection.tsx +++ b/src/app/account/sections/AccountMembershipSection.tsx @@ -28,7 +28,8 @@ export function AccountMembershipSection() { useEffect(() => { try { - if (inView) setVisible(true) + // eslint-disable-next-line react-hooks/set-state-in-effect + if (inView) setVisible(true) // 07/02/26 - not currently worth addressing } catch (err) { safeLogError(err, 'inView effect error:') } diff --git a/src/app/volunteer/CollectInfoStage.tsx b/src/app/volunteer/CollectInfoStage.tsx index ae38d3d8..db779cf8 100644 --- a/src/app/volunteer/CollectInfoStage.tsx +++ b/src/app/volunteer/CollectInfoStage.tsx @@ -5,7 +5,6 @@ import { useFetch, useInit } from '@/util/hooks' import Link from 'next/link' import phone from 'phone' import { Country, isValidCountryPostalCode } from 'postal-code-validator' -import { Location, zLocation } from 'pv-contracts/data' import { useCallback, useEffect, useMemo, useState } from 'react' export interface IOnboardingForm { diff --git a/src/app/volunteer/VolunteerPage.tsx b/src/app/volunteer/VolunteerPage.tsx index f0e5580a..33e21cf4 100644 --- a/src/app/volunteer/VolunteerPage.tsx +++ b/src/app/volunteer/VolunteerPage.tsx @@ -29,16 +29,6 @@ import { useQuery, useQueryClient, } from '@tanstack/react-query' -import { OnboardingStage } from 'pv-contracts/data' -import { - UserOnboardingCollectInfoRequest, - UserOnboardingVerifyRequest, - zUserOnboardingCollectInfoRequest, -} from 'pv-contracts/requests' -import { - DiscordUserIsInServerResponse, - zDiscordUserIsInServerResponse, -} from 'pv-contracts/responses' import { useState } from 'react' import z from 'zod' diff --git a/vitest.config.mts b/vitest.config.mts index 8c18e494..6a4c2f56 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,26 +1,12 @@ -/// -/// import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' import { nodePolyfills } from 'vite-plugin-node-polyfills' import tsconfigPaths from 'vite-tsconfig-paths' -import { defineConfig as testConfig } from 'vitest/config' +import { defineConfig } from 'vitest/config' -// vite config -const config = defineConfig({ +export default defineConfig({ plugins: [tsconfigPaths(), react(), nodePolyfills()], -}) - -const tstConfig = testConfig({ test: { environment: 'jsdom', setupFiles: ['vitest.setup.ts'], }, }) - -const mergedConfig = { - ...config, - ...tstConfig, -} - -export default mergedConfig \ No newline at end of file From c6aab25f0596a7cf87e48033a42a0e15a595a744 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:44:14 -0600 Subject: [PATCH 22/36] 8 problems. 7 set-state, one immutability with `window.location.href` (Which I thought I dealt with already). --- eslint.config.js | 1 + package.json | 2 +- src/app/account/sections/AccountMembershipSection.tsx | 3 +-- src/app/admin/panels/fundraising/page.tsx | 4 ---- src/app/home/MemberBanner.tsx | 3 +-- src/app/volunteer/CollectInfoStage.tsx | 2 -- src/app/volunteer/PhoneVerifyStage.tsx | 3 +-- src/app/volunteer/VolunteerPage.tsx | 4 +--- src/components/common/mini_calendar/DateRangePicker.tsx | 3 +-- 9 files changed, 7 insertions(+), 18 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index d096b3b7..a60e2eaf 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -42,6 +42,7 @@ export default defineConfig([ 'object-shorthand': 'warn', '@typescript-eslint/no-unsafe-return': 'warn', '@typescript-eslint/no-unsafe-enum-comparison': 'warn', + 'react-hooks/set-state-in-effect': 'warn', // Should go back to (default) error when current set are dealt with 'tailwindcss/no-custom-classname': 'off', // React Compiler rules (for testing) diff --git a/package.json b/package.json index 107ba9f7..892697c7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 0 .", + "lint-ci": "eslint --max-warnings 8 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/app/account/sections/AccountMembershipSection.tsx b/src/app/account/sections/AccountMembershipSection.tsx index 271130e6..3371d295 100644 --- a/src/app/account/sections/AccountMembershipSection.tsx +++ b/src/app/account/sections/AccountMembershipSection.tsx @@ -28,8 +28,7 @@ export function AccountMembershipSection() { useEffect(() => { try { - // eslint-disable-next-line react-hooks/set-state-in-effect - if (inView) setVisible(true) // 07/02/26 - not currently worth addressing + if (inView) setVisible(true) } catch (err) { safeLogError(err, 'inView effect error:') } diff --git a/src/app/admin/panels/fundraising/page.tsx b/src/app/admin/panels/fundraising/page.tsx index a54b24ce..9085c835 100644 --- a/src/app/admin/panels/fundraising/page.tsx +++ b/src/app/admin/panels/fundraising/page.tsx @@ -569,10 +569,6 @@ export default function Page() { if (inclusiveRangeMs < oneDayMs) { return } - console.log( - 'Applying chart view override range:', - { startIso, endIso } - ) applyChartViewOverrideRange({ start, end, diff --git a/src/app/home/MemberBanner.tsx b/src/app/home/MemberBanner.tsx index 7e0026de..a5afc768 100644 --- a/src/app/home/MemberBanner.tsx +++ b/src/app/home/MemberBanner.tsx @@ -180,8 +180,7 @@ export function MemberBanner() { useEffect(() => { try { - // eslint-disable-next-line react-hooks/set-state-in-effect - if (inView) setVisible(true) // 6/23/26 - Not worth addressing + if (inView) setVisible(true) } catch (err) { safeLogError(err, 'inView effect error:') } diff --git a/src/app/volunteer/CollectInfoStage.tsx b/src/app/volunteer/CollectInfoStage.tsx index c60ff7f5..75e14249 100644 --- a/src/app/volunteer/CollectInfoStage.tsx +++ b/src/app/volunteer/CollectInfoStage.tsx @@ -101,9 +101,7 @@ export function CollectInfoStage({ useEffect(() => { if (parsedPhone.isValid) - // eslint-disable-next-line react-hooks/set-state-in-effect setForm((f) => ({ - // 6/23/26 - Not worth addressing ...f, phoneNumber: parsedPhone.phoneNumber.substring(2), })) diff --git a/src/app/volunteer/PhoneVerifyStage.tsx b/src/app/volunteer/PhoneVerifyStage.tsx index 24d2f454..d24727a5 100644 --- a/src/app/volunteer/PhoneVerifyStage.tsx +++ b/src/app/volunteer/PhoneVerifyStage.tsx @@ -48,8 +48,7 @@ export function PhoneVerifyStage({ } useEffect(() => { - // eslint-disable-next-line react-hooks/set-state-in-effect - setCodeTimer(getCodeTime()) // 6/23/26 - Not worth addressing + setCodeTimer(getCodeTime()) const interval = setInterval(() => { setCodeTimer(getCodeTime()) }, 200) diff --git a/src/app/volunteer/VolunteerPage.tsx b/src/app/volunteer/VolunteerPage.tsx index 682fe59a..282f8754 100644 --- a/src/app/volunteer/VolunteerPage.tsx +++ b/src/app/volunteer/VolunteerPage.tsx @@ -159,7 +159,6 @@ export default function VolunteerPage() { const currentStage = overrideStage ?? user.data?.onboardingStage - // useEffect(() => { if ( currentStage === OnboardingStage.JOINED && (!user.data?.firstName || @@ -170,12 +169,11 @@ export default function VolunteerPage() { ) { setOverrideStage(OnboardingStage.NOT_STARTED) } - // }, [user.data, currentStage]) if (isSessionLoading) return null if (!session) { - globalThis.location.replace('/login?redirect=/volunteer') + window.location.replace('/login?redirect=/volunteer') return null } diff --git a/src/components/common/mini_calendar/DateRangePicker.tsx b/src/components/common/mini_calendar/DateRangePicker.tsx index 16dbde81..bde0dfd6 100644 --- a/src/components/common/mini_calendar/DateRangePicker.tsx +++ b/src/components/common/mini_calendar/DateRangePicker.tsx @@ -44,8 +44,7 @@ export function DateRangePicker({ if (!startDate) return const anchor = new Date(startDate) if (Number.isNaN(anchor.getTime())) return - // eslint-disable-next-line react-hooks/set-state-in-effect - setCalendarMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1)) // 6/23/26 - Not worth addressing + setCalendarMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1)) }, [startDate]) const todayInputValue = useMemo( From eb11da453e5ff1486d9cab95b29a6a54102df51e Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:10:33 -0600 Subject: [PATCH 23/36] Last little bits of cleanup (i think) --- .prettierignore | 1 + .../useFundraisingDashboardController.ts | 11 ----------- .../panels/members/panel_views/DonorView.tsx | 18 +++++++----------- .../components/FilterButtonRow.tsx | 10 ++++++++-- src/app/login/LoginCard.tsx | 2 +- src/components/common/ImageWithFallback.tsx | 10 +++++----- 6 files changed, 22 insertions(+), 30 deletions(-) diff --git a/.prettierignore b/.prettierignore index 4a38b494..e18f0954 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ .next/ pnpm-lock.yaml node_modules/ +public/images/ \ No newline at end of file diff --git a/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts b/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts index d24cca2f..19eedad9 100644 --- a/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts +++ b/src/app/admin/panels/fundraising/useFundraisingDashboardController.ts @@ -291,16 +291,6 @@ export function useFundraisingDashboardController() { ] ) - // useEffect(() => { - // setChartViewOverrideRange(null) - // }, [startDate, endDate, committedPreset]) - - // useEffect(() => { - // if (!zoomEnabled) { - // setChartViewOverrideRange(null) - // } - // }, [zoomEnabled]) - const chartBucketQueries = useQueries({ queries: chartBuckets.map((bucket) => ({ queryKey: [ @@ -399,7 +389,6 @@ export function useFundraisingDashboardController() { return } } - console.debug('Applying chart view override range:', range) setChartViewOverrideRange(range) } diff --git a/src/app/admin/panels/members/panel_views/DonorView.tsx b/src/app/admin/panels/members/panel_views/DonorView.tsx index baa4cfb0..c828db67 100644 --- a/src/app/admin/panels/members/panel_views/DonorView.tsx +++ b/src/app/admin/panels/members/panel_views/DonorView.tsx @@ -143,17 +143,13 @@ export function DonorView({ } const [overlayMounted, setOverlayMounted] = useState(false) - //const [overlayOpen, setOverlayOpen] = useState(false) - const overlayOpen = false // For consistency since setOverlay open was only used in the seemingly unused effect - console.debug('Picking Donor? : ', pickingDonor) - // useEffect(() => { - // if (pickingDonor) { - // setOverlayMounted(true) - // requestAnimationFrame(() => setOverlayOpen(true)) - // } else if (overlayMounted) { - // setOverlayOpen(false) - // } - // }, [pickingDonor, overlayMounted]) + const [overlayOpen, setOverlayOpen] = useState(false) + if (pickingDonor) { + setOverlayMounted(true) + requestAnimationFrame(() => setOverlayOpen(true)) + } else if (overlayMounted) { + setOverlayOpen(false) + } const backdropVariants = { open: { diff --git a/src/app/endorsements/components/FilterButtonRow.tsx b/src/app/endorsements/components/FilterButtonRow.tsx index 408bfe9f..b5b3cd99 100644 --- a/src/app/endorsements/components/FilterButtonRow.tsx +++ b/src/app/endorsements/components/FilterButtonRow.tsx @@ -187,6 +187,12 @@ export function FilterButtonRow({ }) }) } + if (!mobile && prev) { + // If we're transitioning from mobile to non-mobile, reset the layout + setMobileSearchWidth(null) + setSingleColumnLayout(false) + setShowSearchLabel(false) + } return mobile }) if (mobile) { @@ -226,7 +232,7 @@ export function FilterButtonRow({ } measure() - const rafId = globalThis.requestAnimationFrame(measure) + const rafId = window.requestAnimationFrame(measure) const resizeObserver = new ResizeObserver(measure) resizeObserver.observe(row) @@ -240,7 +246,7 @@ export function FilterButtonRow({ window.addEventListener('resize', measure) return () => { - globalThis.cancelAnimationFrame(rafId) + window.cancelAnimationFrame(rafId) resizeObserver.disconnect() window.removeEventListener('resize', measure) } diff --git a/src/app/login/LoginCard.tsx b/src/app/login/LoginCard.tsx index c4ac4d03..3a52a07e 100644 --- a/src/app/login/LoginCard.tsx +++ b/src/app/login/LoginCard.tsx @@ -22,7 +22,7 @@ export function LoginCard() { useEffect(() => { if (session) { - globalThis.location.href = redirect + window.location.href = redirect } }, [session, redirect]) diff --git a/src/components/common/ImageWithFallback.tsx b/src/components/common/ImageWithFallback.tsx index 65cfb8ad..e707bf0a 100644 --- a/src/components/common/ImageWithFallback.tsx +++ b/src/components/common/ImageWithFallback.tsx @@ -24,21 +24,21 @@ export function ImageWithFallback({ className, loading = 'lazy', priority = false, -}: ImageWithFallbackProps) { - const [hasErrored, setHasErrored] = useState(false) +}: Readonly) { + const [erroredSrc, setErroredSrc] = useState(null) + const fallbackSrc = 'https://dummyjson.com/image/100x100/e8e0e0/d0c8c8?text=!&fontFamily=Poppins' - const finalSrc = hasErrored || useFallback ? fallbackSrc : src + const finalSrc = useFallback || erroredSrc === src ? fallbackSrc : src return ( {alt} setHasErrored(true)} + onError={() => setErroredSrc(src)} loading={priority ? undefined : loading} priority={priority} /> From b899073b5b815d95953e3e413165d6c055de7cb5 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:16:38 -0600 Subject: [PATCH 24/36] Now actually got through all of the comments --- src/components/common/ToolTip.tsx | 25 +++---------------------- src/components/layout/Header.tsx | 8 ++++---- src/util/hooks/index.ts | 1 + src/util/hooks/useOutsideDetector.ts | 22 ++++++++++++++++++++++ 4 files changed, 30 insertions(+), 26 deletions(-) create mode 100644 src/util/hooks/useOutsideDetector.ts diff --git a/src/components/common/ToolTip.tsx b/src/components/common/ToolTip.tsx index de1f1e50..53c159b0 100644 --- a/src/components/common/ToolTip.tsx +++ b/src/components/common/ToolTip.tsx @@ -1,4 +1,5 @@ -import { RefObject, useEffect, useRef, useState } from 'react' +import { useOutsideDetector } from '@/util/hooks' +import { useEffect, useRef, useState } from 'react' /* preliminary notes: component that generates a button which upon being clicked displays an @@ -25,24 +26,6 @@ export function ToolTip({ const [closerList, setCloserList] = useState([]) const wrapperRef = useRef(null) - function useOutsideDetector(ref: RefObject) { - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if ( - ref.current && - !ref.current.contains(event.target as HTMLElement) - ) { - setOpen(false) - } - } - - document.addEventListener('mousedown', handleClickOutside) - return () => { - document.removeEventListener('mousedown', handleClickOutside) - } - }, [ref]) - } - useEffect(() => { if (!wrapperRef.current) return const closers = Array.from( @@ -52,9 +35,7 @@ export function ToolTip({ setCloserList(closers) }, [open]) - // For code quality, an effect is wrapped in a function. - // ESlint can't verify this function call as legal though, so we need to help it (pulling it out of the function passes cleanly) - useOutsideDetector(wrapperRef) //eslint-disable-line react-hooks/refs + useOutsideDetector(wrapperRef, setOpen) closerList?.map((element: Element) => { if (element instanceof HTMLButtonElement) { diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index ca53799b..b569bf38 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -616,7 +616,7 @@ export function Header() { null ) - const closeNav = () => { + const handleClose = () => { setIsOpen(false) setMobileSubnavItem(null) } @@ -638,7 +638,7 @@ export function Header() { if (!isOpen) return const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') closeNav() + if (e.key === 'Escape') handleClose() } document.addEventListener('keydown', onKeyDown) @@ -666,7 +666,7 @@ export function Header() { const syncToBreakpoint = () => { if (desktopMQ.matches) { - closeNav() + handleClose() } else { setActiveSubnav(null) } @@ -938,7 +938,7 @@ export function Header() { WebkitBackdropFilter: 'blur(10px)', backgroundColor: 'rgba(0, 0, 0, 0.18)', }} - onClick={() => closeNav()} + onClick={handleClose} /> )} diff --git a/src/util/hooks/index.ts b/src/util/hooks/index.ts index 5b4823e0..81296fcf 100644 --- a/src/util/hooks/index.ts +++ b/src/util/hooks/index.ts @@ -7,5 +7,6 @@ export * from './useHydration' export * from './useInit' export * from './useInView' export * from './useOptimisticMutation' +export * from './useOutsideDetector' export * from './usePaginatedSearch' export * from './useUnpaginatedSearch' diff --git a/src/util/hooks/useOutsideDetector.ts b/src/util/hooks/useOutsideDetector.ts new file mode 100644 index 00000000..e295077f --- /dev/null +++ b/src/util/hooks/useOutsideDetector.ts @@ -0,0 +1,22 @@ +import { RefObject, useEffect } from 'react' + +export function useOutsideDetector( + ref: RefObject, + setOpen: (open: boolean) => void +) { + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + ref.current && + !ref.current.contains(event.target as HTMLElement) + ) { + setOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [ref, setOpen]) +} From 7f3e1ec7373c0d1c8e9a866f178c2ebcb667d103 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:38:17 -0600 Subject: [PATCH 25/36] Remove illegal assignment to href I do not understand how setting the url to '/volunteer' (without appending) is supposed to work. But this is the way to do it without a mutability error --- package.json | 2 +- src/app/account/AccountPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 892697c7..ca1c9989 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 8 .", + "lint-ci": "eslint --max-warnings 7 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/app/account/AccountPage.tsx b/src/app/account/AccountPage.tsx index 07083c9d..3cdadc86 100644 --- a/src/app/account/AccountPage.tsx +++ b/src/app/account/AccountPage.tsx @@ -72,7 +72,7 @@ export function AccountPage() { loggedInUser.data && loggedInUser.data.onboardingStage != OnboardingStage.JOINED ) { - window.location.href = '/volunteer' + window.location.assign('/volunteer'); return null } From b0abee1c5ac031a5b876af995485af41df2d350d Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:04:11 -0600 Subject: [PATCH 26/36] Directly update user in render rather than as part of effect. Other one in here will be harder. - UpdatedUser is set in the tsx, which is why we need a new state variable. - We definitely need to do large scale refactors to reduce the complexity of our stuff (600 line function; CC34; I didn't fully read it to verify my changes) --- package.json | 2 +- src/app/account/sections/AccountDetailsSection.tsx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ca1c9989..a7102e6c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 7 .", + "lint-ci": "eslint --max-warnings 6 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/app/account/sections/AccountDetailsSection.tsx b/src/app/account/sections/AccountDetailsSection.tsx index d7dd326f..08c7f054 100644 --- a/src/app/account/sections/AccountDetailsSection.tsx +++ b/src/app/account/sections/AccountDetailsSection.tsx @@ -35,8 +35,9 @@ export function AccountDetailsSection({ onSave, donorLinkError, onDonorLinkSubmit, -}: AccountDetailsSectionProps) { +}: Readonly) { const { ready, onGet } = useFetch() + const [lastUserData, setLastUserData] = useState(userData); const [updatedUser, setUpdatedUser] = useState(userData) const discordUserId = userData.discordUsers?.[0]?.id ?? null @@ -53,9 +54,10 @@ export function AccountDetailsSection({ : skipToken, }) - useEffect(() => { - setUpdatedUser(userData) - }, [userData]) + if (lastUserData !== userData) { + setUpdatedUser(userData); + setLastUserData(userData); + } const membershipDeliverableLabels: Record< MembershipDeliverableStatus, From f8656ce7b872ec88cffcd06df95bb64b6cc82bf9 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:06:52 -0600 Subject: [PATCH 27/36] prettier hates semicolons now? --- src/app/account/AccountPage.tsx | 2 +- src/app/account/sections/AccountDetailsSection.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/account/AccountPage.tsx b/src/app/account/AccountPage.tsx index 3cdadc86..1ff57429 100644 --- a/src/app/account/AccountPage.tsx +++ b/src/app/account/AccountPage.tsx @@ -72,7 +72,7 @@ export function AccountPage() { loggedInUser.data && loggedInUser.data.onboardingStage != OnboardingStage.JOINED ) { - window.location.assign('/volunteer'); + window.location.assign('/volunteer') return null } diff --git a/src/app/account/sections/AccountDetailsSection.tsx b/src/app/account/sections/AccountDetailsSection.tsx index 08c7f054..402dd481 100644 --- a/src/app/account/sections/AccountDetailsSection.tsx +++ b/src/app/account/sections/AccountDetailsSection.tsx @@ -37,7 +37,7 @@ export function AccountDetailsSection({ onDonorLinkSubmit, }: Readonly) { const { ready, onGet } = useFetch() - const [lastUserData, setLastUserData] = useState(userData); + const [lastUserData, setLastUserData] = useState(userData) const [updatedUser, setUpdatedUser] = useState(userData) const discordUserId = userData.discordUsers?.[0]?.id ?? null @@ -55,8 +55,8 @@ export function AccountDetailsSection({ }) if (lastUserData !== userData) { - setUpdatedUser(userData); - setLastUserData(userData); + setUpdatedUser(userData) + setLastUserData(userData) } const membershipDeliverableLabels: Record< From de691fdafcdec1fee0e5eb93ab25430aa00211e7 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:39:28 -0600 Subject: [PATCH 28/36] 5 left. got the other account details effect fixed. I don't love this solution. I think there's a cleaner way that I'm missing --- package.json | 2 +- .../sections/AccountDetailsSection.tsx | 85 ++++++++++--------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index a7102e6c..4e9622ec 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 6 .", + "lint-ci": "eslint --max-warnings 5 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/app/account/sections/AccountDetailsSection.tsx b/src/app/account/sections/AccountDetailsSection.tsx index 402dd481..f1a90524 100644 --- a/src/app/account/sections/AccountDetailsSection.tsx +++ b/src/app/account/sections/AccountDetailsSection.tsx @@ -13,7 +13,7 @@ import { zDiscordUserIsInServerResponse } from '@/contracts/responses' import { useFetch } from '@/util/hooks' import { skipToken, useQuery } from '@tanstack/react-query' import cx from 'classnames' -import { ChangeEvent, FormEvent, useEffect, useState } from 'react' +import { ChangeEvent, FormEvent, useState } from 'react' import { IoClose } from 'react-icons/io5' interface AccountDetailsSectionProps { @@ -39,6 +39,21 @@ export function AccountDetailsSection({ const { ready, onGet } = useFetch() const [lastUserData, setLastUserData] = useState(userData) const [updatedUser, setUpdatedUser] = useState(userData) + const [donorLinkForm, setDonorLinkForm] = useState({ + donorEmail: '', + orderId: '', + }) + const [showDonorLinkForm, setShowDonorLinkForm] = useState(false) + const [pendingLinkEmail, setPendingLinkEmail] = useState( + null + ) + const [showAddressConfirmModal, setShowAddressConfirmModal] = + useState(false) + const [matchedDonorEmail, setMatchedDonorEmail] = useState( + null + ) + const [addressDraft, setAddressDraft] = useState({ ...userData.address }) + const userHasDonor = !!userData.donors?.length const discordUserId = userData.discordUsers?.[0]?.id ?? null const isInServerResult = useQuery({ @@ -54,9 +69,37 @@ export function AccountDetailsSection({ : skipToken, }) + function updateDonor() { + if (!pendingLinkEmail) return + + const matchedDonor = userData.donors?.find( + (donor) => normalizeEmail(donor.email) === pendingLinkEmail + ) + if (!matchedDonor) return + + setAddressDraft({ + ...userData.address, + addressLine1: matchedDonor.addr1 ?? userData.address.addressLine1, + city: matchedDonor.city ?? userData.address.city, + state: matchedDonor.state ?? userData.address.state, + zip: matchedDonor.zip ?? userData.address.zip, + }) + setMatchedDonorEmail(matchedDonor.email) + setShowDonorLinkForm(false) + setShowAddressConfirmModal(true) + setPendingLinkEmail(null) + } + if (lastUserData !== userData) { setUpdatedUser(userData) setLastUserData(userData) + if ( + pendingLinkEmail && + (userData.address !== lastUserData.address || + userData.donors !== lastUserData.donors) + ) { + updateDonor() + } } const membershipDeliverableLabels: Record< @@ -149,43 +192,6 @@ export function AccountDetailsSection({ }) ) ?? false - const [donorLinkForm, setDonorLinkForm] = useState({ - donorEmail: '', - orderId: '', - }) - const [showDonorLinkForm, setShowDonorLinkForm] = useState(false) - const [pendingLinkEmail, setPendingLinkEmail] = useState( - null - ) - const [showAddressConfirmModal, setShowAddressConfirmModal] = - useState(false) - const [matchedDonorEmail, setMatchedDonorEmail] = useState( - null - ) - const [addressDraft, setAddressDraft] = useState({ ...userData.address }) - const userHasDonor = !!userData.donors?.length - - useEffect(() => { - if (!pendingLinkEmail) return - - const matchedDonor = userData.donors?.find( - (donor) => normalizeEmail(donor.email) === pendingLinkEmail - ) - if (!matchedDonor) return - - setAddressDraft({ - ...userData.address, - addressLine1: matchedDonor.addr1 ?? userData.address.addressLine1, - city: matchedDonor.city ?? userData.address.city, - state: matchedDonor.state ?? userData.address.state, - zip: matchedDonor.zip ?? userData.address.zip, - }) - setMatchedDonorEmail(matchedDonor.email) - setShowDonorLinkForm(false) - setShowAddressConfirmModal(true) - setPendingLinkEmail(null) - }, [pendingLinkEmail, userData.address, userData.donors]) - const handleChangeDonorEmail = (e: ChangeEvent) => { setDonorLinkForm({ ...donorLinkForm, @@ -204,6 +210,9 @@ export function AccountDetailsSection({ e.preventDefault() setPendingLinkEmail(normalizeEmail(donorLinkForm.donorEmail)) onDonorLinkSubmit(donorLinkForm) + if (pendingLinkEmail) { + updateDonor() + } } const renderDonorLinkForm = () => { From de78f6fb55d5fef38efdf0d0a41fefb8a121bd3a Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Mon, 27 Jul 2026 07:56:17 -0600 Subject: [PATCH 29/36] Revert "5 left." Trying to merge dev showed that the offending section was modified, so better to just start over This reverts commit de691fdafcdec1fee0e5eb93ab25430aa00211e7. --- package.json | 2 +- .../sections/AccountDetailsSection.tsx | 85 +++++++++---------- 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 4e9622ec..a7102e6c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 5 .", + "lint-ci": "eslint --max-warnings 6 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/app/account/sections/AccountDetailsSection.tsx b/src/app/account/sections/AccountDetailsSection.tsx index f1a90524..402dd481 100644 --- a/src/app/account/sections/AccountDetailsSection.tsx +++ b/src/app/account/sections/AccountDetailsSection.tsx @@ -13,7 +13,7 @@ import { zDiscordUserIsInServerResponse } from '@/contracts/responses' import { useFetch } from '@/util/hooks' import { skipToken, useQuery } from '@tanstack/react-query' import cx from 'classnames' -import { ChangeEvent, FormEvent, useState } from 'react' +import { ChangeEvent, FormEvent, useEffect, useState } from 'react' import { IoClose } from 'react-icons/io5' interface AccountDetailsSectionProps { @@ -39,21 +39,6 @@ export function AccountDetailsSection({ const { ready, onGet } = useFetch() const [lastUserData, setLastUserData] = useState(userData) const [updatedUser, setUpdatedUser] = useState(userData) - const [donorLinkForm, setDonorLinkForm] = useState({ - donorEmail: '', - orderId: '', - }) - const [showDonorLinkForm, setShowDonorLinkForm] = useState(false) - const [pendingLinkEmail, setPendingLinkEmail] = useState( - null - ) - const [showAddressConfirmModal, setShowAddressConfirmModal] = - useState(false) - const [matchedDonorEmail, setMatchedDonorEmail] = useState( - null - ) - const [addressDraft, setAddressDraft] = useState({ ...userData.address }) - const userHasDonor = !!userData.donors?.length const discordUserId = userData.discordUsers?.[0]?.id ?? null const isInServerResult = useQuery({ @@ -69,37 +54,9 @@ export function AccountDetailsSection({ : skipToken, }) - function updateDonor() { - if (!pendingLinkEmail) return - - const matchedDonor = userData.donors?.find( - (donor) => normalizeEmail(donor.email) === pendingLinkEmail - ) - if (!matchedDonor) return - - setAddressDraft({ - ...userData.address, - addressLine1: matchedDonor.addr1 ?? userData.address.addressLine1, - city: matchedDonor.city ?? userData.address.city, - state: matchedDonor.state ?? userData.address.state, - zip: matchedDonor.zip ?? userData.address.zip, - }) - setMatchedDonorEmail(matchedDonor.email) - setShowDonorLinkForm(false) - setShowAddressConfirmModal(true) - setPendingLinkEmail(null) - } - if (lastUserData !== userData) { setUpdatedUser(userData) setLastUserData(userData) - if ( - pendingLinkEmail && - (userData.address !== lastUserData.address || - userData.donors !== lastUserData.donors) - ) { - updateDonor() - } } const membershipDeliverableLabels: Record< @@ -192,6 +149,43 @@ export function AccountDetailsSection({ }) ) ?? false + const [donorLinkForm, setDonorLinkForm] = useState({ + donorEmail: '', + orderId: '', + }) + const [showDonorLinkForm, setShowDonorLinkForm] = useState(false) + const [pendingLinkEmail, setPendingLinkEmail] = useState( + null + ) + const [showAddressConfirmModal, setShowAddressConfirmModal] = + useState(false) + const [matchedDonorEmail, setMatchedDonorEmail] = useState( + null + ) + const [addressDraft, setAddressDraft] = useState({ ...userData.address }) + const userHasDonor = !!userData.donors?.length + + useEffect(() => { + if (!pendingLinkEmail) return + + const matchedDonor = userData.donors?.find( + (donor) => normalizeEmail(donor.email) === pendingLinkEmail + ) + if (!matchedDonor) return + + setAddressDraft({ + ...userData.address, + addressLine1: matchedDonor.addr1 ?? userData.address.addressLine1, + city: matchedDonor.city ?? userData.address.city, + state: matchedDonor.state ?? userData.address.state, + zip: matchedDonor.zip ?? userData.address.zip, + }) + setMatchedDonorEmail(matchedDonor.email) + setShowDonorLinkForm(false) + setShowAddressConfirmModal(true) + setPendingLinkEmail(null) + }, [pendingLinkEmail, userData.address, userData.donors]) + const handleChangeDonorEmail = (e: ChangeEvent) => { setDonorLinkForm({ ...donorLinkForm, @@ -210,9 +204,6 @@ export function AccountDetailsSection({ e.preventDefault() setPendingLinkEmail(normalizeEmail(donorLinkForm.donorEmail)) onDonorLinkSubmit(donorLinkForm) - if (pendingLinkEmail) { - updateDonor() - } } const renderDonorLinkForm = () => { From 87314459c77c7ad3c35821ac117c314d7355b5d7 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:52:53 -0600 Subject: [PATCH 30/36] 4 (one was removed in the merge) Remove effect and replace it with a conditional. Rename variable to better explain it's purpose. The animation should play once, and latch to fully visible --- package.json | 2 +- src/app/home/MemberBanner.tsx | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index a7102e6c..e910411a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 6 .", + "lint-ci": "eslint --max-warnings 4 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/app/home/MemberBanner.tsx b/src/app/home/MemberBanner.tsx index cdfee3c7..36ec15fe 100644 --- a/src/app/home/MemberBanner.tsx +++ b/src/app/home/MemberBanner.tsx @@ -168,7 +168,7 @@ export function safeLogError(err: unknown, prefix = '') { export function MemberBanner() { const { inView, observe } = useInView() const containerRef = useRef(null) - const [visible, setVisible] = useState(false) + const [latchVisible, setLatchVisible] = useState(false) const loggedInUser = useCurrentUser() const discordId = loggedInUser?.data?.discordUsers?.[0]?.id ?? null @@ -184,13 +184,9 @@ export function MemberBanner() { } }, [observe]) - useEffect(() => { - try { - if (inView) setVisible(true) - } catch (err) { - safeLogError(err, 'inView effect error:') - } - }, [inView]) + if(!latchVisible && inView){ + setLatchVisible(true) + } return (
@@ -206,7 +202,7 @@ export function MemberBanner() {
@@ -225,7 +221,7 @@ export function MemberBanner() {
- +
From 5fbaebb6862f79e64029571b20e9372851f991c9 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:30:25 -0600 Subject: [PATCH 31/36] 3 Change effect to top level conditional using state variables to determine Better solution than last time. If something new starts to set pendingLinkEmail, this solution will still be valid, since a truish value of pendingLinkEmail will always be modified (wrt it being reset to null at the end) --- package.json | 2 +- .../sections/AccountDetailsSection.tsx | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index e910411a..c3a6f897 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 4 .", + "lint-ci": "eslint --max-warnings 3 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/app/account/sections/AccountDetailsSection.tsx b/src/app/account/sections/AccountDetailsSection.tsx index 05bb384d..02722c93 100644 --- a/src/app/account/sections/AccountDetailsSection.tsx +++ b/src/app/account/sections/AccountDetailsSection.tsx @@ -13,7 +13,7 @@ import { zDiscordUserIsInServerResponse } from '@/contracts/responses' import { useFetch } from '@/util/hooks' import { skipToken, useQuery } from '@tanstack/react-query' import cx from 'classnames' -import { ChangeEvent, FormEvent, useEffect, useState } from 'react' +import { ChangeEvent, FormEvent, useState } from 'react' import { IoClose } from 'react-icons/io5' interface AccountDetailsSectionProps { @@ -170,10 +170,17 @@ export function AccountDetailsSection({ shirtSize: userData.shirtSize ?? null, }) const userHasDonor = !!userData.donors?.length - - useEffect(() => { - if (!pendingLinkEmail) return - + const [prevUserAddress, setPrevUserAddress] = useState(userData.address); + const [prevUserDonors, setPrevUserDonors] = useState(userData.donors) + + // Possibly deprecated - pendingLinkEmail is only set within this statement, which will never run with it's default value + if ( + pendingLinkEmail && + (userData.address !== prevUserAddress || + userData.donors !== prevUserDonors) + ) { + setPrevUserAddress(userData.address); + setPrevUserDonors(userData.donors); const matchedDonor = userData.donors?.find( (donor) => normalizeEmail(donor.email) === pendingLinkEmail ) @@ -190,7 +197,7 @@ export function AccountDetailsSection({ setShowDonorLinkForm(false) setShowAddressConfirmModal(true) setPendingLinkEmail(null) - }, [pendingLinkEmail, userData.address, userData.donors]) + } const handleChangeDonorEmail = (e: ChangeEvent) => { setDonorLinkForm({ From 2e2143691b08ab13e3d983f1b41ae585b1a058dd Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:41:39 -0600 Subject: [PATCH 32/36] 2 Effect triggered on change of Memoized value. Moved effect body into producing function. --- package.json | 2 +- src/app/volunteer/CollectInfoStage.tsx | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index c3a6f897..cb1e9731 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 3 .", + "lint-ci": "eslint --max-warnings 2 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/app/volunteer/CollectInfoStage.tsx b/src/app/volunteer/CollectInfoStage.tsx index 75e14249..aee66be7 100644 --- a/src/app/volunteer/CollectInfoStage.tsx +++ b/src/app/volunteer/CollectInfoStage.tsx @@ -5,7 +5,7 @@ import { useFetch, useInit } from '@/util/hooks' import Link from 'next/link' import phone from 'phone' import { Country, isValidCountryPostalCode } from 'postal-code-validator' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' export interface IOnboardingForm { firstName: string @@ -50,12 +50,19 @@ export function CollectInfoStage({ ) const [zipCodeError, setZipCodeError] = useState(false) - const parsePhone = (number: string) => - phone(number, { + const parsePhone = (number: string) => { + const p = phone(number, { country: 'US', strictDetection: true, validateMobilePrefix: true, }) + if (p.isValid) + setForm((f) => ({ + ...f, + phoneNumber: number.substring(2), + })) + return p + } const parsedPhone = useMemo(() => parsePhone(phoneNumber), [phoneNumber]) const setFormattedPhoneNumber = (number: string) => { @@ -99,14 +106,6 @@ export function CollectInfoStage({ useInit(() => setFormattedPhoneNumber(initialForm.phoneNumber)) - useEffect(() => { - if (parsedPhone.isValid) - setForm((f) => ({ - ...f, - phoneNumber: parsedPhone.phoneNumber.substring(2), - })) - }, [parsedPhone]) - return (
From 0c16941f7c874c3452f3a007df5da62b9ff1b6a9 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:32:15 -0600 Subject: [PATCH 33/36] 1 Set it immediately as the default, and remove it from the effect --- package.json | 2 +- src/app/volunteer/PhoneVerifyStage.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index cb1e9731..a84fd5dd 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 2 .", + "lint-ci": "eslint --max-warnings 1 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/app/volunteer/PhoneVerifyStage.tsx b/src/app/volunteer/PhoneVerifyStage.tsx index d24727a5..c60b2cb5 100644 --- a/src/app/volunteer/PhoneVerifyStage.tsx +++ b/src/app/volunteer/PhoneVerifyStage.tsx @@ -33,7 +33,7 @@ export function PhoneVerifyStage({ }, [lastSmsCodeSendTimeUtc]) const [securityCode, setSecurityCode] = useState('') - const [codeTimer, setCodeTimer] = useState(0) + const [codeTimer, setCodeTimer] = useState(getCodeTime()) const handleChange = (e: ChangeEvent) => { const value = e.target.value @@ -48,7 +48,6 @@ export function PhoneVerifyStage({ } useEffect(() => { - setCodeTimer(getCodeTime()) const interval = setInterval(() => { setCodeTimer(getCodeTime()) }, 200) From 4772b604acbcd2257c9b6a94bc2128cedc36e020 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:32:59 -0600 Subject: [PATCH 34/36] Remembered Prettier --- src/app/account/sections/AccountDetailsSection.tsx | 6 +++--- src/app/home/MemberBanner.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/account/sections/AccountDetailsSection.tsx b/src/app/account/sections/AccountDetailsSection.tsx index 02722c93..f6090e0c 100644 --- a/src/app/account/sections/AccountDetailsSection.tsx +++ b/src/app/account/sections/AccountDetailsSection.tsx @@ -170,7 +170,7 @@ export function AccountDetailsSection({ shirtSize: userData.shirtSize ?? null, }) const userHasDonor = !!userData.donors?.length - const [prevUserAddress, setPrevUserAddress] = useState(userData.address); + const [prevUserAddress, setPrevUserAddress] = useState(userData.address) const [prevUserDonors, setPrevUserDonors] = useState(userData.donors) // Possibly deprecated - pendingLinkEmail is only set within this statement, which will never run with it's default value @@ -179,8 +179,8 @@ export function AccountDetailsSection({ (userData.address !== prevUserAddress || userData.donors !== prevUserDonors) ) { - setPrevUserAddress(userData.address); - setPrevUserDonors(userData.donors); + setPrevUserAddress(userData.address) + setPrevUserDonors(userData.donors) const matchedDonor = userData.donors?.find( (donor) => normalizeEmail(donor.email) === pendingLinkEmail ) diff --git a/src/app/home/MemberBanner.tsx b/src/app/home/MemberBanner.tsx index 36ec15fe..9b3097e3 100644 --- a/src/app/home/MemberBanner.tsx +++ b/src/app/home/MemberBanner.tsx @@ -184,7 +184,7 @@ export function MemberBanner() { } }, [observe]) - if(!latchVisible && inView){ + if (!latchVisible && inView) { setLatchVisible(true) } From 0a7795a68a78eb023af18ad4755ebca8b1b273e7 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:58:59 -0600 Subject: [PATCH 35/36] 0! Save date between renders. In total this iPR is a hack job, and we probably could make many of these calculate on the fly rather than cause a rerender. But it's down to zero warnings which was the request, and nothing is disastrously worse --- package.json | 2 +- .../common/mini_calendar/DateRangePicker.tsx | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a84fd5dd..107ba9f7 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "postbuild": "next-sitemap", "start": "next start", "lint": "eslint --fix .", - "lint-ci": "eslint --max-warnings 1 .", + "lint-ci": "eslint --max-warnings 0 .", "test": "vitest", "prettier": "prettier --write --ignore-path .prettierignore .", "prettier-ci": "prettier --check --ignore-path .prettierignore ." diff --git a/src/components/common/mini_calendar/DateRangePicker.tsx b/src/components/common/mini_calendar/DateRangePicker.tsx index bde0dfd6..c7754a72 100644 --- a/src/components/common/mini_calendar/DateRangePicker.tsx +++ b/src/components/common/mini_calendar/DateRangePicker.tsx @@ -10,7 +10,7 @@ import { startOfDayISO, } from './dateRange.helpers' import { useDateRangeSelectionController } from './useDateRangeSelectionController' -import { useEffect, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' interface DateRangePickerProps { startDate: Date | null @@ -39,13 +39,15 @@ export function DateRangePicker({ const anchor = startDate ? new Date(startDate) : new Date() return new Date(anchor.getFullYear(), anchor.getMonth(), 1) }) + const [prevDate, setPrevDate] = useState(startDate); - useEffect(() => { - if (!startDate) return + if (startDate && startDate != prevDate) { + setPrevDate(startDate) const anchor = new Date(startDate) - if (Number.isNaN(anchor.getTime())) return - setCalendarMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1)) - }, [startDate]) + if (!Number.isNaN(anchor.getTime())) { + setCalendarMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1)) + } + } const todayInputValue = useMemo( () => isoToDateInput(new Date().toISOString()), From ad27c1a43a4082f309161e0e1d91dc91af298ff2 Mon Sep 17 00:00:00 2001 From: Luke Norman <60233616+LNorman8@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:59:33 -0600 Subject: [PATCH 36/36] Remembered Prettier :( --- src/components/common/mini_calendar/DateRangePicker.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/common/mini_calendar/DateRangePicker.tsx b/src/components/common/mini_calendar/DateRangePicker.tsx index c7754a72..239a9eb4 100644 --- a/src/components/common/mini_calendar/DateRangePicker.tsx +++ b/src/components/common/mini_calendar/DateRangePicker.tsx @@ -39,13 +39,15 @@ export function DateRangePicker({ const anchor = startDate ? new Date(startDate) : new Date() return new Date(anchor.getFullYear(), anchor.getMonth(), 1) }) - const [prevDate, setPrevDate] = useState(startDate); + const [prevDate, setPrevDate] = useState(startDate) if (startDate && startDate != prevDate) { setPrevDate(startDate) const anchor = new Date(startDate) if (!Number.isNaN(anchor.getTime())) { - setCalendarMonth(new Date(anchor.getFullYear(), anchor.getMonth(), 1)) + setCalendarMonth( + new Date(anchor.getFullYear(), anchor.getMonth(), 1) + ) } }