diff --git a/src/app/portfolio/[slug]/components/PropertyTable.tsx b/src/app/portfolio/[slug]/components/PropertyTable.tsx index 62e0b19..8e9177f 100644 --- a/src/app/portfolio/[slug]/components/PropertyTable.tsx +++ b/src/app/portfolio/[slug]/components/PropertyTable.tsx @@ -1,11 +1,11 @@ "use client"; -import { useState, useMemo, useRef } from "react"; +import { useState, useMemo, useRef, useEffect, useCallback } from "react"; import { useProperties } from "./useProperties"; import DataTable from "./dataTable"; import PropertyFilters from "./PropertyFilters"; import { FilterGroups } from "@/app/utils/propertyFilters"; -import { HomeIcon, FunnelIcon } from "@heroicons/react/24/outline"; +import { HomeIcon, FunnelIcon, ChevronDownIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { columns } from "@/app/portfolio/[slug]/components/propertyTableColumns"; import { @@ -49,6 +49,117 @@ function LoadingOverlay() { ); } +/* ---------------------------------------- + Quick filter dropdown button +----------------------------------------- */ +type QuickFilterKey = "address" | "postcode" | "propertyRef"; + +interface QuickFilterDropdownProps { + label: string; + placeholder: string; + committedValue: string; + isOpen: boolean; + onOpen: () => void; + onCommit: (value: string) => void; + onClear: () => void; + inputWidth?: string; +} + +function QuickFilterDropdown({ + label, + placeholder, + committedValue, + isOpen, + onOpen, + onCommit, + onClear, + inputWidth = "w-52", +}: QuickFilterDropdownProps) { + const containerRef = useRef(null); + const inputRef = useRef(null); + const [draft, setDraft] = useState(committedValue); + + // Sync draft when dropdown opens + useEffect(() => { + if (isOpen) { + setDraft(committedValue); + setTimeout(() => inputRef.current?.focus(), 0); + } + }, [isOpen, committedValue]); + + const commit = useCallback(() => { + onCommit(draft.trim()); + }, [draft, onCommit]); + + // Close + commit on outside click + useEffect(() => { + if (!isOpen) return; + function handleMouseDown(e: MouseEvent) { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + commit(); + } + } + document.addEventListener("mousedown", handleMouseDown); + return () => document.removeEventListener("mousedown", handleMouseDown); + }, [isOpen, commit]); + + const isActive = Boolean(committedValue); + + return ( +
+ + + {isOpen && ( +
+ setDraft(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") commit(); + if (e.key === "Escape") { setDraft(committedValue); onCommit(committedValue); } + }} + /> + +
+ )} +
+ ); +} + /* ---------------------------------------- Main table ----------------------------------------- */ @@ -59,63 +170,51 @@ export default function PropertyTable({ }) { const [sidebarOpen, setSidebarOpen] = useState(true); - // Quick filter display state (updates immediately) - const [quickAddress, setQuickAddress] = useState(""); - const [quickPostcode, setQuickPostcode] = useState(""); - const [quickPropertyRef, setQuickPropertyRef] = useState(""); + // Committed quick filter values (drives the query) + const [committedAddress, setCommittedAddress] = useState(""); + const [committedPostcode, setCommittedPostcode] = useState(""); + const [committedPropertyRef, setCommittedPropertyRef] = useState(""); - // Debounced quick filter values (drives the query) - const [debouncedAddress, setDebouncedAddress] = useState(""); - const [debouncedPostcode, setDebouncedPostcode] = useState(""); - const [debouncedPropertyRef, setDebouncedPropertyRef] = useState(""); + // Which quick filter dropdown is open + const [openFilter, setOpenFilter] = useState(null); // Advanced filter groups from the sidebar const [filterGroups, setFilterGroups] = useState([]); - const debounceRef = useRef | null>(null); - - function handleQuickFilter( - field: "address" | "postcode" | "propertyRef", - value: string - ) { - if (debounceRef.current) clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => { - if (field === "address") setDebouncedAddress(value); - if (field === "postcode") setDebouncedPostcode(value); - if (field === "propertyRef") setDebouncedPropertyRef(value); - }, 300); + function commitFilter(field: QuickFilterKey, value: string) { + if (field === "address") setCommittedAddress(value); + if (field === "postcode") setCommittedPostcode(value); + if (field === "propertyRef") setCommittedPropertyRef(value); + setOpenFilter(null); } function clearAll() { - if (debounceRef.current) clearTimeout(debounceRef.current); - setQuickAddress(""); - setQuickPostcode(""); - setQuickPropertyRef(""); - setDebouncedAddress(""); - setDebouncedPostcode(""); - setDebouncedPropertyRef(""); + setCommittedAddress(""); + setCommittedPostcode(""); + setCommittedPropertyRef(""); + setOpenFilter(null); setFilterGroups([]); } const allFilterGroups = useMemo((): FilterGroups => { const quick: FilterGroups = []; - if (debouncedAddress) + if (committedAddress) quick.push({ id: "qa", - conditions: [{ id: "qa-c", field: "address", operator: "contains", value: debouncedAddress }], + conditions: [{ id: "qa-c", field: "address", operator: "contains", value: committedAddress }], }); - if (debouncedPostcode) + if (committedPostcode) quick.push({ id: "qp", - conditions: [{ id: "qp-c", field: "postcode", operator: "starts_with", value: debouncedPostcode }], + conditions: [{ id: "qp-c", field: "postcode", operator: "starts_with", value: committedPostcode }], }); - if (debouncedPropertyRef) + if (committedPropertyRef) quick.push({ id: "qr", - conditions: [{ id: "qr-c", field: "propertyRef", operator: "contains", value: debouncedPropertyRef }], + conditions: [{ id: "qr-c", field: "propertyRef", operator: "contains", value: committedPropertyRef }], }); return [...quick, ...filterGroups]; - }, [debouncedAddress, debouncedPostcode, debouncedPropertyRef, filterGroups]); + }, [committedAddress, committedPostcode, committedPropertyRef, filterGroups]); const hasActiveFilters = allFilterGroups.length > 0; @@ -139,13 +238,10 @@ export default function PropertyTable({ const [previewLoading] = useState(false); const [previewError] = useState(null); - const inputClass = - "h-9 rounded-md border border-gray-300 px-3 text-sm focus:outline-none focus:ring-2 focus:ring-black/10 bg-white"; - return (
{/* Quick filters row */} -
+
-
- - { - setQuickAddress(e.target.value); - handleQuickFilter("address", e.target.value); - }} - /> -
+ setOpenFilter(openFilter === "address" ? null : "address")} + onCommit={(v) => commitFilter("address", v)} + onClear={() => { setCommittedAddress(""); setOpenFilter(null); }} + inputWidth="w-52" + /> -
- - { - setQuickPostcode(e.target.value); - handleQuickFilter("postcode", e.target.value); - }} - /> -
+ setOpenFilter(openFilter === "postcode" ? null : "postcode")} + onCommit={(v) => commitFilter("postcode", v)} + onClear={() => { setCommittedPostcode(""); setOpenFilter(null); }} + inputWidth="w-32" + /> -
- - { - setQuickPropertyRef(e.target.value); - handleQuickFilter("propertyRef", e.target.value); - }} - /> -
+ setOpenFilter(openFilter === "propertyRef" ? null : "propertyRef")} + onCommit={(v) => commitFilter("propertyRef", v)} + onClear={() => { setCommittedPropertyRef(""); setOpenFilter(null); }} + inputWidth="w-40" + /> {hasActiveFilters && (