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