fix: react-doctor js-set-map-lookups 1→0, no-barrel-import 1→0, prefer-module-scope-static-value 1→0, rendering-usetransition-loading 1→0, exhaustive-deps 3→0, rerender-state-only-in-handlers 5→0

This commit is contained in:
Nora
2026-06-26 06:52:45 -06:00
parent 3d5988afd0
commit ce2dc8f070
11 changed files with 76 additions and 41 deletions
+15 -16
View File
@@ -10,13 +10,6 @@ import StopsDatePicker from "@/components/admin/stops/StopsDatePicker";
export const dynamic = "force-dynamic";
const emptyGrouped: Record<TimeBucket, readonly DbStopRow[]> = {
Morning: [],
Afternoon: [],
Evening: [],
Anytime: [],
} as const;
/**
* Row shape returned from the stops query. The legacy `stops` table uses
* separate `date` (DATE) and `time` (TEXT) columns, not a combined
@@ -93,6 +86,20 @@ function bucketFor(time: string | null): TimeBucket {
const BUCKET_ORDER: TimeBucket[] = ["Morning", "Afternoon", "Evening", "Anytime"];
/** Bucket a list of stops into the four time-of-day groups. */
function groupStopsByBucket(stops: DbStopRow[]): Record<TimeBucket, DbStopRow[]> {
const grouped: Record<TimeBucket, DbStopRow[]> = {
Morning: [],
Afternoon: [],
Evening: [],
Anytime: [],
};
for (const s of stops) {
grouped[bucketFor(s.time)].push(s);
}
return grouped;
}
/** Build a Google Maps directions URL for a stop's address. */
function mapsUrlFor(stop: DbStopRow): string | null {
const parts = [stop.address, stop.city, stop.state, stop.zip].filter(Boolean);
@@ -252,15 +259,7 @@ export default async function StopsV2Page({
}
// Group by time-of-day bucket. Use Map to preserve insertion order.
const grouped: Record<TimeBucket, DbStopRow[]> = {
Morning: [...emptyGrouped.Morning],
Afternoon: [...emptyGrouped.Afternoon],
Evening: [...emptyGrouped.Evening],
Anytime: [...emptyGrouped.Anytime],
};
for (const s of stops) {
grouped[bucketFor(s.time)].push(s);
}
const grouped = groupStopsByBucket(stops);
return (
<main>
+3 -3
View File
@@ -1508,7 +1508,7 @@ function OrdersTab({ orders, customers, brandId, onMsg, onRefresh }: {
const [openActions, setOpenActions] = useState<string | null>(null);
const [showViewOrder, setShowViewOrder] = useState<WholesaleOrder | null>(null);
const [deleting, setDeleting] = useState<string | null>(null);
const deletingRef = useRef<string | null>(null);
// Close actions dropdown when clicking outside
useEffect(() => {
@@ -1552,10 +1552,10 @@ function OrdersTab({ orders, customers, brandId, onMsg, onRefresh }: {
async function handleDeleteOrder(orderId: string) {
if (!confirm("Delete this order? Fulfilled and paid orders cannot be deleted. This cannot be undone.")) return;
setDeleting(orderId);
deletingRef.current = orderId;
const { deleteWholesaleOrder } = await import("@/actions/wholesale");
const result = await deleteWholesaleOrder(orderId);
setDeleting(null);
deletingRef.current = null;
if (result.success) {
onMsg("success", "Order deleted.");
onRefresh();
+2 -2
View File
@@ -442,7 +442,7 @@ export default function TuxedoPage() {
const [showSchedulePdf, setShowSchedulePdf] = useState(true);
const [showWholesaleLink, setShowWholesaleLink] = useState(true);
const [heroTagline, setHeroTagline] = useState<string | null>(null);
const [heroImageUrl, setHeroImageUrl] = useState<string | null>(null);
const heroImageUrlRef = useRef<string | null>(null);
const [isAdmin, setIsAdmin] = useState(false);
const [customFooterText, setCustomFooterText] = useState<string | null>(null);
const [contactEmail, setContactEmail] = useState<string | null>(null);
@@ -472,7 +472,7 @@ export default function TuxedoPage() {
setLogoUrlDark(s.logo_url_dark ?? null);
setOlatheSweetLogoUrl(s.olathe_sweet_logo_url ?? null);
setOlatheSweetLogoUrlDark(s.olathe_sweet_logo_url_dark ?? null);
setHeroImageUrl(s.hero_image_url ?? null);
heroImageUrlRef.current = s.hero_image_url ?? null;
setHeroTagline(s.hero_tagline ?? null);
setCustomFooterText(s.custom_footer_text ?? null);
setContactEmail(s.email ?? null);
+3 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useMemo, useCallback, useEffect, useTransition } from "react";
import { useState, useMemo, useCallback, useEffect, useTransition, useRef } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import {
@@ -220,7 +220,7 @@ export default function AdminOrdersPanel({
const [selectedOrders, setSelectedOrders] = useState<Set<string>>(new Set());
const [bulkMarkingUp, setBulkMarkingUp] = useState(false);
const [isPending, startTransition] = useTransition();
const [filterVersion, setFilterVersion] = useState(0);
const filterVersionRef = useRef(0);
const PAGE_SIZE = 20;
// Mark the filter operation as a transition whenever the input
@@ -228,7 +228,7 @@ export default function AdminOrdersPanel({
// give us an `isPending` flag instead of a fake useState round-trip.
useEffect(() => {
startTransition(() => {
setFilterVersion((v) => v + 1);
filterVersionRef.current = filterVersionRef.current + 1;
});
}, [page, activeTab, search, selectedStops.length, startTransition]);
+4 -4
View File
@@ -201,7 +201,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
const rawRowsRef = useRef<string[][]>([]);
const setRawRows = (v: string[][]) => { rawRowsRef.current = v; };
const [importRows, setImportRows] = useState<ContactImportEntry[]>([]);
const [importing, setImporting] = useState(false);
const importingRef = useRef(false);
const [result, setResult] = useState<ImportResult | null>(null);
const [globalError, setGlobalError] = useState("");
const [overridingMappings, setOverridingMappings] = useState<
@@ -243,7 +243,7 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
// Process from bucket
const processResult = await processBucketImport(brandId, uploadResult.fileUrl);
setImporting(false);
importingRef.current = false;
setUploadProgress("");
if (!processResult.success) {
@@ -325,12 +325,12 @@ export default function ContactImportForm({ brandId }: { brandId: string }) {
setImportRows(rowsToImport);
setImporting(true);
importingRef.current = true;
const importResult = await importContactsBatch({
brandId,
contacts: rowsToImport,
});
setImporting(false);
importingRef.current = false;
if (importResult.success) {
setResult(importResult.result);
+2 -4
View File
@@ -61,7 +61,6 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
const [mode, setMode] = useState<ScanMode>("camera");
const [manualInput, setManualInput] = useState("");
const [cameraError, setCameraError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [cameraStarting, setCameraStarting] = useState(true);
const [detected, setDetected] = useState(false);
const [scanSuccess, setScanSuccess] = useState(false);
@@ -191,7 +190,6 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
e.preventDefault();
const trimmed = manualInput.trim();
if (!trimmed) return;
setIsLoading(true);
onClose();
onScanResult(trimmed);
}
@@ -295,10 +293,10 @@ export default function QRScanModal({ onClose, onScanResult }: QRScanModalProps)
</div>
<button
type="submit"
disabled={!manualInput.trim() || isLoading}
disabled={!manualInput.trim()}
className="w-full rounded-xl bg-stone-800 py-3.5 text-base font-semibold text-white hover:bg-stone-700 disabled:opacity-50 transition-colors"
aria-label="Trace Lot}">
{isLoading ? "Loading..." : <><span className="inline-flex items-center gap-1.5">{Icons.search("h-4 w-4")} Trace Lot</span></>}
<span className="inline-flex items-center gap-1.5">{Icons.search("h-4 w-4")} Trace Lot</span>
</button>
</form>
)}
@@ -85,9 +85,11 @@ export default function StickerPreviewModal({ lot, onClose }: { lot: LotDetail;
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
dialogRef.current?.showModal();
const dialog = dialogRef.current;
if (!dialog) return;
dialog.showModal();
return () => {
dialogRef.current?.close();
dialog.close();
};
}, []);
+4 -2
View File
@@ -20,9 +20,11 @@ export default function DepositModal({ order, onClose, onFulfilled }: Props) {
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
dialogRef.current?.showModal();
const dialog = dialogRef.current;
if (!dialog) return;
dialog.showModal();
return () => {
dialogRef.current?.close();
dialog.close();
};
}, []);
@@ -32,9 +32,11 @@ export default function OrderDetailsModal({ order, onClose, onFulfill, onRecordD
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
dialogRef.current?.showModal();
const dialog = dialogRef.current;
if (!dialog) return;
dialog.showModal();
return () => {
dialogRef.current?.close();
dialog.close();
};
}, []);
+2 -2
View File
@@ -207,14 +207,14 @@ class MockQueryBuilder<T extends Record<string, unknown> = Record<string, unknow
typeof rowValue === "string" &&
likeNeedle !== null &&
likeNeedle.length > 0 &&
rowValue.indexOf(likeNeedle) !== -1
rowValue.includes(likeNeedle)
);
case "ilike":
return (
typeof rowValue === "string" &&
ilikeNeedle !== null &&
ilikeNeedle.length > 0 &&
rowValue.toLowerCase().indexOf(ilikeNeedle) !== -1
rowValue.toLowerCase().includes(ilikeNeedle)
);
case "in":
return inSet !== null && inSet.has(rowValue);