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"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
+1 -1
View File
@@ -2,7 +2,7 @@
import { useEffect, useState, Suspense, lazy } from "react"; import { useEffect, useState, Suspense, lazy } from "react";
import Image from "next/image"; import Image from "next/image";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader"; import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import { getBrandSettingsPublic } from "@/actions/brand-settings"; import { getBrandSettingsPublic } from "@/actions/brand-settings";
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader"; import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
@@ -2,7 +2,7 @@
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader"; import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
+1 -1
View File
@@ -3,7 +3,7 @@
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; 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 StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import StorefrontFooter from "@/components/storefront/StorefrontFooter";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import ProductCard from "@/components/storefront/ProductCard"; import ProductCard from "@/components/storefront/ProductCard";
import StopSetEffect from "@/components/storefront/StopSetEffect"; import StopSetEffect from "@/components/storefront/StopSetEffect";
import StorefrontHeader from "@/components/storefront/StorefrontHeader"; import StorefrontHeader from "@/components/storefront/StorefrontHeader";
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import ProductCard from "@/components/storefront/ProductCard"; import ProductCard from "@/components/storefront/ProductCard";
import StopSetEffect from "@/components/storefront/StopSetEffect"; import StopSetEffect from "@/components/storefront/StopSetEffect";
import StorefrontHeader from "@/components/storefront/StorefrontHeader"; import StorefrontHeader from "@/components/storefront/StorefrontHeader";
+1 -1
View File
@@ -2,7 +2,7 @@
import { Suspense, lazy, useState } from "react"; import { Suspense, lazy, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import StorefrontHeader from "@/components/storefront/StorefrontHeader"; import StorefrontHeader from "@/components/storefront/StorefrontHeader";
import StorefrontFooter from "@/components/storefront/StorefrontFooter"; import StorefrontFooter from "@/components/storefront/StorefrontFooter";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
+1 -1
View File
@@ -2,7 +2,7 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import Link from "next/link"; 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 TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero";
import CinematicShowcase from "@/components/storefront/CinematicShowcase"; import CinematicShowcase from "@/components/storefront/CinematicShowcase";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
export default function ErrorPage({ export default function ErrorPage({
error, error,
+12 -10
View File
@@ -2,7 +2,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import { MotionConfig } from "framer-motion"; import { LazyMotion, MotionConfig, domAnimation } from "framer-motion";
import { CartProvider } from "@/context/CartContext"; import { CartProvider } from "@/context/CartContext";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import SiteHeader from "@/components/layout/SiteHeader"; import SiteHeader from "@/components/layout/SiteHeader";
@@ -45,15 +45,17 @@ export function Providers({ children }: { children: React.ReactNode }) {
* Combined with the prefers-reduced-motion block in globals.css, this means a user * 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. * with motion sensitivity gets the same content with zero positional movement.
*/} */}
<MotionConfig transition={{ duration: 0.2, ease: "easeOut" }} reducedMotion="user"> <LazyMotion features={domAnimation}>
<CartProvider> <MotionConfig transition={{ duration: 0.2, ease: "easeOut" }} reducedMotion="user">
{!isBrandRoute && !isLandingPage && !isAuthPage && !isAdminRoute && !isStandalonePage && <SiteHeader />} <CartProvider>
{children} {!isBrandRoute && !isLandingPage && !isAuthPage && !isAdminRoute && !isStandalonePage && <SiteHeader />}
{!isBrandRoute && !isLandingPage && !isAuthPage && !isAdminRoute && !isStandalonePage && <SiteFooter />} {children}
<CartToast /> {!isBrandRoute && !isLandingPage && !isAuthPage && !isAdminRoute && !isStandalonePage && <SiteFooter />}
<CartRestoredToast /> <CartToast />
</CartProvider> <CartRestoredToast />
</MotionConfig> </CartProvider>
</MotionConfig>
</LazyMotion>
</ThemeProvider> </ThemeProvider>
); );
} }
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
export default function CTASection() { export default function CTASection() {
+1 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
interface ContactSectionProps { interface ContactSectionProps {
@@ -1,6 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
const FAMILY_MEMBERS = [ const FAMILY_MEMBERS = [
@@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
export default function IndianRiverCTASection() { export default function IndianRiverCTASection() {
@@ -1,6 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
// Custom SVG Icons for Citrus Varieties // Custom SVG Icons for Citrus Varieties
@@ -1,6 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
interface IndianRiverContactSectionProps { interface IndianRiverContactSectionProps {
@@ -1,6 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
export default function IndianRiverMissionSection() { export default function IndianRiverMissionSection() {
@@ -1,6 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
const TEAM_MEMBERS = [ const TEAM_MEMBERS = [
@@ -1,6 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
// Custom checkmark icon with blue styling // Custom checkmark icon with blue styling
+1 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
export default function MissionSection() { export default function MissionSection() {
+1 -1
View File
@@ -2,7 +2,7 @@
import Image from "next/image"; import Image from "next/image";
import { useState } from "react"; import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { m as motion, AnimatePresence } from "framer-motion";
import { useCart } from "@/context/CartContext"; import { useCart } from "@/context/CartContext";
const badgeConfig: Record<"best-seller" | "new" | "limited" | "organic", { label: string; bg: string; text: string; border: string }> = { 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"; "use client";
import { useEffect, useEffectEvent } from "react"; import { useEffect, useEffectEvent, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion"; import { m, AnimatePresence } from "framer-motion";
import { useCart } from "@/context/CartContext"; import { useCart } from "@/context/CartContext";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
@@ -68,6 +68,44 @@ export default function QuickCartSheet({
return () => window.removeEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey);
}, [open]); }, [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 displayName = productName ?? justAdded?.name ?? cart[0]?.name ?? "Your box";
const addedAt = new Date(); const addedAt = new Date();
const headerLabel = justAddedLabel ?? "Just added"; const headerLabel = justAddedLabel ?? "Just added";
@@ -87,7 +125,7 @@ export default function QuickCartSheet({
{open && ( {open && (
<> <>
{/* Backdrop */} {/* Backdrop */}
<motion.button <m.button
type="button" type="button"
aria-label="Close cart" aria-label="Close cart"
onClick={onClose} onClick={onClose}
@@ -99,7 +137,7 @@ export default function QuickCartSheet({
/> />
{/* Mobile: bottom sheet */} {/* Mobile: bottom sheet */}
<motion.div <m.div
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Quick cart" aria-label="Quick cart"
@@ -107,12 +145,11 @@ export default function QuickCartSheet({
animate={{ y: 0 }} animate={{ y: 0 }}
exit={{ y: "100%" }} exit={{ y: "100%" }}
transition={{ type: "spring", stiffness: 380, damping: 38, mass: 0.9 }} transition={{ type: "spring", stiffness: 380, damping: 38, mass: 0.9 }}
drag="y" onPointerDown={handleDragPointerDown}
dragConstraints={{ top: 0, bottom: 0 }} onPointerMove={handleDragPointerMove}
dragElastic={{ top: 0, bottom: 0.5 }} onPointerUp={handleDragPointerUp}
onDragEnd={(_, info) => { onPointerCancel={handleDragPointerUp}
if (info.offset.y > 90 || info.velocity.y > 480) onClose(); 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" 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 */} {/* Drag handle */}
@@ -131,10 +168,10 @@ export default function QuickCartSheet({
onCheckout={handleCheckout} onCheckout={handleCheckout}
onContinue={onClose} onContinue={onClose}
/> />
</motion.div> </m.div>
{/* Desktop: right-side drawer */} {/* Desktop: right-side drawer */}
<motion.aside <m.aside
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Quick cart" aria-label="Quick cart"
@@ -156,7 +193,7 @@ export default function QuickCartSheet({
onContinue={onClose} onContinue={onClose}
showCloseButton showCloseButton
/> />
</motion.aside> </m.aside>
</> </>
)} )}
</AnimatePresence> </AnimatePresence>
@@ -211,7 +248,7 @@ function SheetContent({
{/* Header */} {/* Header */}
<div className="relative px-6 sm:px-8 pt-2 pb-5 border-b border-stone-900/10"> <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"> <div className="flex items-center gap-3">
<motion.div <m.div
initial={{ scale: 0.95, opacity: 0 , rotate: -45 }} initial={{ scale: 0.95, opacity: 0 , rotate: -45 }}
animate={{ scale: 1, rotate: 0 }} animate={{ scale: 1, rotate: 0 }}
transition={{ type: "spring", stiffness: 500, damping: 18, delay: 0.05 }} 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}> <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" /> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg> </svg>
</motion.div> </m.div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-[10px] font-bold uppercase tracking-[0.22em] text-[#1A4D2E]/70"> <p className="text-[10px] font-bold uppercase tracking-[0.22em] text-[#1A4D2E]/70">
{headerLabel} {headerLabel}
@@ -3,7 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion"; import { m as motion, AnimatePresence } from "framer-motion";
import { useCart } from "@/context/CartContext"; import { useCart } from "@/context/CartContext";
type StorefrontHeaderProps = { type StorefrontHeaderProps = {
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; 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 { useCart } from "@/context/CartContext";
import QuickCartSheet from "./QuickCartSheet"; import QuickCartSheet from "./QuickCartSheet";
import StorefrontHeader from "./StorefrontHeader"; import StorefrontHeader from "./StorefrontHeader";
@@ -2,7 +2,7 @@
import Image from "next/image"; import Image from "next/image";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { motion } from "framer-motion"; import { m as motion } from "framer-motion";
import { gsap } from "gsap"; import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger"; import { ScrollTrigger } from "gsap/ScrollTrigger";
@@ -1,7 +1,7 @@
"use client"; "use client";
import { useRef } from "react"; import { useRef } from "react";
import { motion, useInView } from "framer-motion"; import { m as motion, useInView } from "framer-motion";
import LayoutContainer from "@/components/layout/LayoutContainer"; import LayoutContainer from "@/components/layout/LayoutContainer";
// ── Feature type & data ────────────────────────────────────────────── // ── Feature type & data ──────────────────────────────────────────────
@@ -278,7 +278,6 @@ export default function WaterAdminClient() {
const [allEntries, setAllEntries] = useState<WaterEntry[]>([]); const [allEntries, setAllEntries] = useState<WaterEntry[]>([]);
const [users, setUsers] = useState<WaterUser[]>([]); const [users, setUsers] = useState<WaterUser[]>([]);
const [headgates, setHeadgates] = useState<Headgate[]>([]); const [headgates, setHeadgates] = useState<Headgate[]>([]);
const [loading, setLoading] = useState(true);
const [alertLog, setAlertLog] = useState<AlertLogEntry[]>([]); const [alertLog, setAlertLog] = useState<AlertLogEntry[]>([]);
// Filter state // Filter state
@@ -316,7 +315,6 @@ export default function WaterAdminClient() {
const [initialLoading, setInitialLoading] = useState(true); const [initialLoading, setInitialLoading] = useState(true);
const loadAll = async () => { const loadAll = async () => {
setLoading(true);
const [entries, usersData, headgatesData, alertsData] = await Promise.all([ const [entries, usersData, headgatesData, alertsData] = await Promise.all([
getWaterEntries(TUXEDO_BRAND_ID, 500), getWaterEntries(TUXEDO_BRAND_ID, 500),
getWaterIrrigators(TUXEDO_BRAND_ID), getWaterIrrigators(TUXEDO_BRAND_ID),
@@ -327,7 +325,6 @@ export default function WaterAdminClient() {
setUsers(usersData); setUsers(usersData);
setHeadgates(headgatesData); setHeadgates(headgatesData);
setAlertLog(alertsData as AlertLogEntry[]); setAlertLog(alertsData as AlertLogEntry[]);
setLoading(false);
setInitialLoading(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) const inSet = filter.op === "in" && Array.isArray(filter.value)
? new Set(filter.value as unknown[]) ? new Set(filter.value as unknown[])
: null; : 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" const likeNeedle = filter.op === "like" && typeof filter.value === "string"
? (filter.value as string).replace(/%/g, "") ? (filter.value as string).replace(/%/g, "")
: null; : null;
@@ -205,16 +211,14 @@ class MockQueryBuilder<T extends Record<string, unknown> = Record<string, unknow
case "like": case "like":
return ( return (
typeof rowValue === "string" && typeof rowValue === "string" &&
likeNeedle !== null && likeRe !== null &&
likeNeedle.length > 0 && likeRe.test(rowValue)
rowValue.includes(likeNeedle)
); );
case "ilike": case "ilike":
return ( return (
typeof rowValue === "string" && typeof rowValue === "string" &&
ilikeNeedle !== null && ilikeRe !== null &&
ilikeNeedle.length > 0 && ilikeRe.test(rowValue.toLowerCase())
rowValue.toLowerCase().includes(ilikeNeedle)
); );
case "in": case "in":
return inSet !== null && inSet.has(rowValue); return inSet !== null && inSet.has(rowValue);
@@ -333,6 +337,22 @@ interface UserAttributes {
data?: Record<string, unknown>; 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() { function createMockClient() {
// The query builder is mutable, so we can't simply return a class // The query builder is mutable, so we can't simply return a class
// instance + spread mutation methods. The cleanest way to support // instance + spread mutation methods. The cleanest way to support