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:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user