fix: react-doctor → 64/100 (Bugs 122, Perf 286, A11y 613, Maint 436)

- CartContext: lazy initializers replace mount-only useEffect
  hydration; remove 8 no-initialize-state warnings
- Toast/AdminSearchInput: React 19 useContext/use + drop
  forwardRef (3 no-react19-deprecated-apis)
- ProductFormModal: lazy initializers + useSyncExternalStore
  for mount; parent adds key=editingProduct.id
- InstallPrompt: useReducer for prompt state (no-cascading-set-state)
- QRScanModal: ref-based latest-callback pattern replaces
  useEffectEvent deps mistake
- OnboardingFlow: functional setState (rerender-functional-setstate)
- UsersPage/StopsCalendar/FeaturesAndStats: lazy initializers
  (rerender-lazy-state-init)
- FAQClientPage: server-side brand settings fetch via getBrandSettingsPublic
  in layout; remove supabase import
- LandingPageWrapper: href='#' → href='#top' (anchor-is-valid)
- TuxedoVideoHero: replace animate-bounce with ease-out-expo
  (no-inline-bounce-easing)
- ProductTableClient: useCallback for handleDeleted
  (jsx-no-new-function-as-prop)
- excel-parser: pre-compile delimiter regexes (js-hoist-regexp)
- water-log/settings: Promise.all for parallel DB calls
  (async-parallel)
- ToastNotification: extract toast store to separate file
  (only-export-components)
- WholesaleClient: inline <WholesaleIcon/> instead of hoisting to
  const (rendering-hoist-jsx)
This commit is contained in:
Nora
2026-06-26 02:41:56 -06:00
parent 8e011da521
commit 29d9d23a26
88 changed files with 1399 additions and 1015 deletions
+38 -47
View File
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useEffectEvent, useRef, useState, useSyncExternalStore } from "react";
import Image from "next/image";
import { createPortal } from "react-dom";
@@ -91,22 +91,31 @@ export default function ProductFormModal({
initialImageUrl = null,
onUploadImage,
}: ProductFormModalProps) {
// Form state
const [name, setName] = useState(initial?.name ?? "");
const [description, setDescription] = useState(initial?.description ?? "");
const [price, setPrice] = useState(initial?.price ?? "");
const [type, setType] = useState<ProductFormValues["type"]>(initial?.type ?? "pickup");
const [brandId, setBrandId] = useState(
initial?.brand_id ?? lockedBrandId ?? brands[0]?.id ?? ""
// Form state — lazy initializers read the latest `initial` / `initialImageUrl`
// props. The parent (ProductsClient) supplies a `key` derived from
// `editingProduct?.id` so changing the product under edit triggers a
// remount instead of a state-reset effect.
const [name, setName] = useState(() => initial?.name ?? "");
const [description, setDescription] = useState(() => initial?.description ?? "");
const [price, setPrice] = useState(() => initial?.price ?? "");
const [type, setType] = useState<ProductFormValues["type"]>(
() => (initial?.type as ProductFormValues["type"]) ?? "pickup"
);
const [brandId, setBrandId] = useState(
() => initial?.brand_id ?? lockedBrandId ?? brands[0]?.id ?? ""
);
const [active, setActive] = useState(() => initial?.active ?? true);
const [isTaxable, setIsTaxable] = useState(() => initial?.is_taxable ?? true);
const [availableFrom, setAvailableFrom] = useState(
() => (initial?.available_from ? initial.available_from.split("T")[0] : "")
);
const [availableUntil, setAvailableUntil] = useState(
() => (initial?.available_until ? initial.available_until.split("T")[0] : "")
);
const [active, setActive] = useState(initial?.active ?? true);
const [isTaxable, setIsTaxable] = useState(initial?.is_taxable ?? true);
const [availableFrom, setAvailableFrom] = useState(initial?.available_from ? initial.available_from.split('T')[0] : "");
const [availableUntil, setAvailableUntil] = useState(initial?.available_until ? initial.available_until.split('T')[0] : "");
// Image state
const [imagePreview, setImagePreview] = useState<string | null>(initialImageUrl);
const [imageUrl, setImageUrl] = useState<string | null>(initialImageUrl);
const [imagePreview, setImagePreview] = useState<string | null>(() => initialImageUrl ?? null);
const [imageUrl, setImageUrl] = useState<string | null>(() => initialImageUrl ?? null);
const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
const [isDrag, setIsDrag] = useState(false);
@@ -115,53 +124,35 @@ export default function ProductFormModal({
// Submit state
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
const init = async () => {
setMounted(true);
};
init();
}, []);
// Reset state when opening with new initial values
useEffect(() => {
const init = async () => {
if (!open) return;
setName(initial?.name ?? "");
setDescription(initial?.description ?? "");
setPrice(initial?.price ?? "");
setType((initial?.type as ProductFormValues["type"]) ?? "pickup");
setBrandId(initial?.brand_id ?? lockedBrandId ?? brands[0]?.id ?? "");
setActive(initial?.active ?? true);
setIsTaxable(initial?.is_taxable ?? true);
setAvailableFrom(initial?.available_from ? initial.available_from.split('T')[0] : "");
setAvailableUntil(initial?.available_until ? initial.available_until.split('T')[0] : "");
setImagePreview(initialImageUrl);
setImageUrl(initialImageUrl);
setUploading(false);
setUploadError(null);
setError(null);
setSaving(false);
setIsDrag(false);
};
init();
}, [open, initial, initialImageUrl, lockedBrandId, brands]);
// SSR-safe mounted flag via useSyncExternalStore so the first render
// already reflects "we are on the client" — no flash of an empty
// portal, no `useEffect(() => setMounted(true), [])` to flag.
const mounted = useSyncExternalStore(
() => () => {},
() => true,
() => false,
);
// Body scroll lock + escape key
// useEffectEvent so the latest onClose is always called even though
// it's no longer in the effect's dependency array.
const onCloseEffect = useEffectEvent(() => {
onClose();
});
useEffect(() => {
if (!open) return;
const original = document.body.style.overflow;
document.body.style.overflow = "hidden";
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
if (e.key === "Escape") onCloseEffect();
};
window.addEventListener("keydown", onKey);
return () => {
document.body.style.overflow = original;
window.removeEventListener("keydown", onKey);
};
}, [open, onClose]);
}, [open]);
const handleFile = useCallback(
async (file: File) => {