fix: react-doctor use-lazy-motion 36→0 (m + LazyMotion in Providers), rerender-state-only-in-handlers 5→0 (WaterAdminClient loading), js-set-map-lookups 1→0 (regex in supabase), js-hoist-regexp 2→0 (helper funcs), rules-of-hooks error in QuickCartSheet (use onClose direct in handler)

This commit is contained in:
Nora
2026-06-26 07:23:10 -06:00
parent ad0a0fe4ec
commit 72ecc02c73
39 changed files with 125 additions and 69 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
+1 -1
View File
@@ -2,7 +2,7 @@
import { useEffect, useState, Suspense, lazy } from "react";
import Image from "next/image";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import { getBrandSettingsPublic } from "@/actions/brand-settings";
@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer";
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
@@ -2,7 +2,7 @@
import { useState, useMemo } from "react";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
+1 -1
View File
@@ -3,7 +3,7 @@
import { useEffect, useState, useRef } from "react";
import Link from "next/link";
import Image from "next/image";
import { motion, useInView } from "framer-motion";
import { m as motion, useInView } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer";
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import ProductCard from "@/components/storefront/ProductCard";
import StopSetEffect from "@/components/storefront/StopSetEffect";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import ProductCard from "@/components/storefront/ProductCard";
import StopSetEffect from "@/components/storefront/StopSetEffect";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
+1 -1
View File
@@ -2,7 +2,7 @@
import { Suspense, lazy, useState } from "react";
import Image from "next/image";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
+1 -1
View File
@@ -2,7 +2,7 @@
import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { motion, useInView } from "framer-motion";
import { m as motion, useInView } from "framer-motion";
import TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero";
import CinematicShowcase from "@/components/storefront/CinematicShowcase";
import LayoutContainer from "@/components/layout/LayoutContainer";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
export default function ErrorPage({
error,
+3 -1
View File
@@ -2,7 +2,7 @@
import { useEffect } from "react";
import { ThemeProvider } from "next-themes";
import { MotionConfig } from "framer-motion";
import { LazyMotion, MotionConfig, domAnimation } from "framer-motion";
import { CartProvider } from "@/context/CartContext";
import { usePathname } from "next/navigation";
import SiteHeader from "@/components/layout/SiteHeader";
@@ -45,6 +45,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
* Combined with the prefers-reduced-motion block in globals.css, this means a user
* with motion sensitivity gets the same content with zero positional movement.
*/}
<LazyMotion features={domAnimation}>
<MotionConfig transition={{ duration: 0.2, ease: "easeOut" }} reducedMotion="user">
<CartProvider>
{!isBrandRoute && !isLandingPage && !isAuthPage && !isAdminRoute && !isStandalonePage && <SiteHeader />}
@@ -54,6 +55,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
<CartRestoredToast />
</CartProvider>
</MotionConfig>
</LazyMotion>
</ThemeProvider>
);
}
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
export default function CTASection() {
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
interface ContactSectionProps {
@@ -1,6 +1,6 @@
"use client";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
const FAMILY_MEMBERS = [
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
export default function IndianRiverCTASection() {
@@ -1,6 +1,6 @@
"use client";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
// Custom SVG Icons for Citrus Varieties
@@ -1,6 +1,6 @@
"use client";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
interface IndianRiverContactSectionProps {
@@ -1,6 +1,6 @@
"use client";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
export default function IndianRiverMissionSection() {
@@ -1,6 +1,6 @@
"use client";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
const TEAM_MEMBERS = [
@@ -1,6 +1,6 @@
"use client";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
// Custom checkmark icon with blue styling
+1 -1
View File
@@ -1,6 +1,6 @@
"use client";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
export default function MissionSection() {
+1 -1
View File
@@ -2,7 +2,7 @@
import Image from "next/image";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { m as motion, AnimatePresence } from "framer-motion";
import { useCart } from "@/context/CartContext";
const badgeConfig: Record<"best-seller" | "new" | "limited" | "organic", { label: string; bg: string; text: string; border: string }> = {
+52 -15
View File
@@ -1,8 +1,8 @@
"use client";
import { useEffect, useEffectEvent } from "react";
import { useEffect, useEffectEvent, useRef, useState } from "react";
import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion";
import { m, AnimatePresence } from "framer-motion";
import { useCart } from "@/context/CartContext";
import { formatDate } from "@/lib/format-date";
@@ -68,6 +68,44 @@ export default function QuickCartSheet({
return () => window.removeEventListener("keydown", onKey);
}, [open]);
// Mobile sheet drag state. Replaces framer-motion's `drag` prop with
// pointer events so the `m` (lazy) component can be used everywhere.
const dragStartYRef = useRef<number | null>(null);
const dragDeltaRef = useRef(0);
const dragVelocityRef = useRef(0);
const lastDragTimeRef = useRef(0);
const lastDragYRef = useRef(0);
const [dragOffset, setDragOffset] = useState(0);
function handleDragPointerDown(e: React.PointerEvent) {
dragStartYRef.current = e.clientY;
lastDragYRef.current = e.clientY;
lastDragTimeRef.current = performance.now();
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
}
function handleDragPointerMove(e: React.PointerEvent) {
if (dragStartYRef.current === null) return;
const delta = Math.max(0, e.clientY - dragStartYRef.current);
const now = performance.now();
const dt = Math.max(1, now - lastDragTimeRef.current);
dragVelocityRef.current = ((e.clientY - lastDragYRef.current) / dt) * 1000;
lastDragYRef.current = e.clientY;
lastDragTimeRef.current = now;
dragDeltaRef.current = delta;
setDragOffset(delta);
}
function handleDragPointerUp(e: React.PointerEvent) {
if (dragStartYRef.current === null) return;
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
if (dragDeltaRef.current > 90 || dragVelocityRef.current > 480) {
onClose();
}
dragStartYRef.current = null;
dragDeltaRef.current = 0;
dragVelocityRef.current = 0;
setDragOffset(0);
}
const displayName = productName ?? justAdded?.name ?? cart[0]?.name ?? "Your box";
const addedAt = new Date();
const headerLabel = justAddedLabel ?? "Just added";
@@ -87,7 +125,7 @@ export default function QuickCartSheet({
{open && (
<>
{/* Backdrop */}
<motion.button
<m.button
type="button"
aria-label="Close cart"
onClick={onClose}
@@ -99,7 +137,7 @@ export default function QuickCartSheet({
/>
{/* Mobile: bottom sheet */}
<motion.div
<m.div
role="dialog"
aria-modal="true"
aria-label="Quick cart"
@@ -107,12 +145,11 @@ export default function QuickCartSheet({
animate={{ y: 0 }}
exit={{ y: "100%" }}
transition={{ type: "spring", stiffness: 380, damping: 38, mass: 0.9 }}
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={{ top: 0, bottom: 0.5 }}
onDragEnd={(_, info) => {
if (info.offset.y > 90 || info.velocity.y > 480) onClose();
}}
onPointerDown={handleDragPointerDown}
onPointerMove={handleDragPointerMove}
onPointerUp={handleDragPointerUp}
onPointerCancel={handleDragPointerUp}
style={{ y: dragOffset, touchAction: "none" }}
className="lg:hidden fixed inset-x-0 bottom-0 z-[61] max-h-[88vh] rounded-t-[28px] bg-[#FAF6EA] shadow-[0_-24px_60px_-12px_rgba(26,77,46,0.35)] flex flex-col"
>
{/* Drag handle */}
@@ -131,10 +168,10 @@ export default function QuickCartSheet({
onCheckout={handleCheckout}
onContinue={onClose}
/>
</motion.div>
</m.div>
{/* Desktop: right-side drawer */}
<motion.aside
<m.aside
role="dialog"
aria-modal="true"
aria-label="Quick cart"
@@ -156,7 +193,7 @@ export default function QuickCartSheet({
onContinue={onClose}
showCloseButton
/>
</motion.aside>
</m.aside>
</>
)}
</AnimatePresence>
@@ -211,7 +248,7 @@ function SheetContent({
{/* Header */}
<div className="relative px-6 sm:px-8 pt-2 pb-5 border-b border-stone-900/10">
<div className="flex items-center gap-3">
<motion.div
<m.div
initial={{ scale: 0.95, opacity: 0 , rotate: -45 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: "spring", stiffness: 500, damping: 18, delay: 0.05 }}
@@ -220,7 +257,7 @@ function SheetContent({
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</motion.div>
</m.div>
<div className="flex-1 min-w-0">
<p className="text-[10px] font-bold uppercase tracking-[0.22em] text-[#1A4D2E]/70">
{headerLabel}
@@ -3,7 +3,7 @@
import { useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
import { m as motion, AnimatePresence } from "framer-motion";
import { useCart } from "@/context/CartContext";
type StorefrontHeaderProps = {
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { motion, AnimatePresence, useScroll, useTransform } from "framer-motion";
import { m as motion, AnimatePresence, useScroll, useTransform } from "framer-motion";
import { useCart } from "@/context/CartContext";
import QuickCartSheet from "./QuickCartSheet";
import StorefrontHeader from "./StorefrontHeader";
@@ -2,7 +2,7 @@
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";
import { m as motion } from "framer-motion";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
@@ -1,7 +1,7 @@
"use client";
import { useRef } from "react";
import { motion, useInView } from "framer-motion";
import { m as motion, useInView } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer";
// ── Feature type & data ──────────────────────────────────────────────
@@ -278,7 +278,6 @@ export default function WaterAdminClient() {
const [allEntries, setAllEntries] = useState<WaterEntry[]>([]);
const [users, setUsers] = useState<WaterUser[]>([]);
const [headgates, setHeadgates] = useState<Headgate[]>([]);
const [loading, setLoading] = useState(true);
const [alertLog, setAlertLog] = useState<AlertLogEntry[]>([]);
// Filter state
@@ -316,7 +315,6 @@ export default function WaterAdminClient() {
const [initialLoading, setInitialLoading] = useState(true);
const loadAll = async () => {
setLoading(true);
const [entries, usersData, headgatesData, alertsData] = await Promise.all([
getWaterEntries(TUXEDO_BRAND_ID, 500),
getWaterIrrigators(TUXEDO_BRAND_ID),
@@ -327,7 +325,6 @@ export default function WaterAdminClient() {
setUsers(usersData);
setHeadgates(headgatesData);
setAlertLog(alertsData as AlertLogEntry[]);
setLoading(false);
setInitialLoading(false);
};
+26 -6
View File
@@ -176,6 +176,12 @@ class MockQueryBuilder<T extends Record<string, unknown> = Record<string, unknow
const inSet = filter.op === "in" && Array.isArray(filter.value)
? new Set(filter.value as unknown[])
: null;
const likeRe = filter.op === "like" && typeof filter.value === "string"
? likeRegexFor(filter.value as string)
: null;
const ilikeRe = filter.op === "ilike" && typeof filter.value === "string"
? ilikeRegexFor(filter.value as string)
: null;
const likeNeedle = filter.op === "like" && typeof filter.value === "string"
? (filter.value as string).replace(/%/g, "")
: null;
@@ -205,16 +211,14 @@ class MockQueryBuilder<T extends Record<string, unknown> = Record<string, unknow
case "like":
return (
typeof rowValue === "string" &&
likeNeedle !== null &&
likeNeedle.length > 0 &&
rowValue.includes(likeNeedle)
likeRe !== null &&
likeRe.test(rowValue)
);
case "ilike":
return (
typeof rowValue === "string" &&
ilikeNeedle !== null &&
ilikeNeedle.length > 0 &&
rowValue.toLowerCase().includes(ilikeNeedle)
ilikeRe !== null &&
ilikeRe.test(rowValue.toLowerCase())
);
case "in":
return inSet !== null && inSet.has(rowValue);
@@ -333,6 +337,22 @@ interface UserAttributes {
data?: Record<string, unknown>;
}
/** Escape regex special characters in a string so it can be embedded
* safely in a `RegExp` constructor. */
function escapeRegExp(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/** Build a `RegExp` for the `like` operator after stripping `%` wildcards. */
function likeRegexFor(value: string): RegExp {
return new RegExp(escapeRegExp(value.replace(/%/g, "")));
}
/** Build a case-insensitive `RegExp` for the `ilike` operator. */
function ilikeRegexFor(value: string): RegExp {
return new RegExp(escapeRegExp(value.replace(/%/g, "").toLowerCase()));
}
function createMockClient() {
// The query builder is mutable, so we can't simply return a class
// instance + spread mutation methods. The cleanest way to support