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
@@ -1,7 +1,5 @@
"use client";
import { useEffect } from "react";
type BrandColors = {
primaryColor?: string | null;
secondaryColor?: string | null;
@@ -9,21 +7,26 @@ type BrandColors = {
textColor?: string | null;
};
export default function BrandStylesProvider({
function buildCssBlock({
primaryColor,
secondaryColor,
bgColor,
textColor,
}: BrandColors) {
useEffect(() => {
if (primaryColor || bgColor || textColor) {
const root = document.documentElement;
if (primaryColor) root.style.setProperty("--brand-primary", primaryColor);
if (secondaryColor) root.style.setProperty("--brand-secondary", secondaryColor);
if (bgColor) root.style.setProperty("--brand-bg", bgColor);
if (textColor) root.style.setProperty("--brand-text", textColor);
}
}, [primaryColor, secondaryColor, bgColor, textColor]);
}: BrandColors): string {
const declarations: string[] = [];
if (primaryColor) declarations.push(`--brand-primary: ${primaryColor};`);
if (secondaryColor) declarations.push(`--brand-secondary: ${secondaryColor};`);
if (bgColor) declarations.push(`--brand-bg: ${bgColor};`);
if (textColor) declarations.push(`--brand-text: ${textColor};`);
if (declarations.length === 0) return "";
return `:root{${declarations.join("")}}`;
}
return null;
export default function BrandStylesProvider(props: BrandColors) {
const css = buildCssBlock(props);
if (!css) return null;
// Render an inline <style> tag so the brand tokens are applied to :root
// on every render. No useEffect needed — the DOM mutation is replaced by
// a declarative style element that React reconciles on each prop change.
return <style data-brand-styles dangerouslySetInnerHTML={{ __html: css }} />;
}
+8 -3
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect } from "react";
import { useEffect, useEffectEvent } from "react";
import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion";
import { useCart } from "@/context/CartContext";
@@ -54,14 +54,19 @@ export default function QuickCartSheet({
}, [open]);
// Close on Escape
// 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;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
if (e.key === "Escape") onCloseEffect();
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);
}, [open]);
const displayName = productName ?? justAdded?.name ?? cart[0]?.name ?? "Your box";
const addedAt = new Date();
@@ -62,7 +62,6 @@ export default function StorefrontFooter({
</div>
<form
className="flex w-full shrink-0 gap-0 sm:w-auto"
onSubmit={(e) => e.preventDefault()}
>
<input
type="email"
@@ -167,7 +167,7 @@ export default function StripeExpressCheckout(props: Props) {
function persistPendingCheckout(intentId: string) {
if (typeof sessionStorage === "undefined") return;
sessionStorage.setItem(
"pending_checkout",
"pending_checkout:v1",
JSON.stringify({
customerName: props.customerName,
customerEmail: props.customerEmail,
+12 -12
View File
@@ -198,7 +198,7 @@ export default function TuxedoVideoHero({
style={{
background: "radial-gradient(circle, rgba(255,215,0,0.4) 0%, transparent 70%)",
filter: "blur(40px)",
animation: "float-slow 10s ease-in-out infinite",
animation: "tvh-float-slow 10s ease-in-out infinite",
}}
/>
{/* Emerald orb bottom-right */}
@@ -207,7 +207,7 @@ export default function TuxedoVideoHero({
style={{
background: "radial-gradient(circle, rgba(16,185,129,0.4) 0%, transparent 70%)",
filter: "blur(30px)",
animation: "float-delayed 12s ease-in-out infinite",
animation: "tvh-float-delayed 12s ease-in-out infinite",
}}
/>
{/* Subtle grain overlay */}
@@ -384,7 +384,7 @@ export default function TuxedoVideoHero({
className="w-6 h-10 rounded-full border border-white/30 flex items-start justify-center p-1.5"
>
<div
className="w-1.5 h-3 rounded-full bg-white animate-bounce"
className="w-1.5 h-3 rounded-full bg-white tvh-scroll-indicator"
style={{ animationDuration: "1.5s" }}
/>
</div>
@@ -424,30 +424,30 @@ export default function TuxedoVideoHero({
</section>
{/* ─── GLOBAL ANIMATIONS ──────────────────────────────────────────────── */}
<style jsx>{`
<style dangerouslySetInnerHTML={{ __html: `
/* Ambient orb drift — gentle, slow, predictable.
* Travel reduced from ~20px to ~8px so the eye isn't pulled
* around by the decoration. */
@keyframes float-slow {
@keyframes tvh-float-slow {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(6px, -8px); }
}
@keyframes float-delayed {
@keyframes tvh-float-delayed {
0%, 100% { transform: translate(0, 0); }
50% { transform: translate(-8px, 6px); }
}
/* Scroll-indicator dot — was a 1.5s ease-in-out bounce. Now a
* slower 2.4s linear slide so it reads as a steady hint instead
* of a constant bounce. */
@keyframes bounce {
* slower 2.4s ease-out-expo slide so it reads as a steady hint
* instead of a constant bounce. */
@keyframes tvh-scroll-indicator {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(3px); }
}
.animate-bounce {
animation: bounce 2.4s ease-in-out infinite;
.tvh-scroll-indicator {
animation: tvh-scroll-indicator 2.4s cubic-bezier(0.16, 1, 0.3, 1) infinite;
}
.parallax-float {
@@ -472,7 +472,7 @@ export default function TuxedoVideoHero({
transform: none !important;
}
}
`}</style>
`}} />
</>
);
}