updated quick filters
Some checks failed
Next.js Build Check / build (push) Has been cancelled

This commit is contained in:
Khalim Conn-Kowlessar 2026-04-11 15:47:02 +00:00
parent c70a34f174
commit 8c9cceef77

View file

@ -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<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(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 (
<div ref={containerRef} className="relative">
<button
onClick={onOpen}
className={[
"flex items-center gap-1.5 h-9 px-3 rounded-md border text-sm transition shrink-0",
isActive
? "border-gray-800 bg-gray-800 text-white"
: "border-gray-300 text-gray-600 hover:bg-gray-50",
].join(" ")}
>
<span className="font-medium">{label}</span>
{isActive ? (
<>
<span className="opacity-75 max-w-[120px] truncate">: {committedValue}</span>
<span
role="button"
tabIndex={0}
onClick={(e) => { e.stopPropagation(); onClear(); }}
onKeyDown={(e) => { if (e.key === "Enter") { e.stopPropagation(); onClear(); } }}
className="ml-0.5 rounded-full hover:opacity-75"
>
<XMarkIcon className="h-3.5 w-3.5" />
</span>
</>
) : (
<ChevronDownIcon className="h-3.5 w-3.5 opacity-50" />
)}
</button>
{isOpen && (
<div className="absolute left-0 top-full mt-1 z-30 bg-white border border-gray-200 rounded-md shadow-md p-2 flex gap-1.5 items-center">
<input
ref={inputRef}
className={`h-8 rounded border border-gray-300 px-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-black/10 ${inputWidth}`}
placeholder={placeholder}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") commit();
if (e.key === "Escape") { setDraft(committedValue); onCommit(committedValue); }
}}
/>
<button
onClick={commit}
className="h-8 px-2.5 rounded bg-gray-800 text-white text-sm hover:bg-gray-700 transition whitespace-nowrap"
>
Apply
</button>
</div>
)}
</div>
);
}
/* ----------------------------------------
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<QuickFilterKey | null>(null);
// Advanced filter groups from the sidebar
const [filterGroups, setFilterGroups] = useState<FilterGroups>([]);
const debounceRef = useRef<ReturnType<typeof setTimeout> | 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<string | null>(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 (
<div className="px-4 py-3">
{/* Quick filters row */}
<div className="flex items-end gap-3 mb-3 flex-wrap">
<div className="flex items-center gap-2 mb-3 flex-wrap">
<button
onClick={() => setSidebarOpen((o) => !o)}
className="flex items-center gap-1.5 h-9 px-3 rounded-md border border-gray-300 text-sm text-gray-600 hover:bg-gray-50 transition shrink-0"
@ -155,51 +251,43 @@ export default function PropertyTable({
{sidebarOpen ? "Hide filters" : "Filters"}
</button>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-gray-600">Address</label>
<input
className={`${inputClass} w-52`}
placeholder="Contains…"
value={quickAddress}
onChange={(e) => {
setQuickAddress(e.target.value);
handleQuickFilter("address", e.target.value);
}}
/>
</div>
<QuickFilterDropdown
label="Address"
placeholder="Contains…"
committedValue={committedAddress}
isOpen={openFilter === "address"}
onOpen={() => setOpenFilter(openFilter === "address" ? null : "address")}
onCommit={(v) => commitFilter("address", v)}
onClear={() => { setCommittedAddress(""); setOpenFilter(null); }}
inputWidth="w-52"
/>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-gray-600">Postcode</label>
<input
className={`${inputClass} w-32`}
placeholder="e.g. E17"
value={quickPostcode}
onChange={(e) => {
setQuickPostcode(e.target.value);
handleQuickFilter("postcode", e.target.value);
}}
/>
</div>
<QuickFilterDropdown
label="Postcode"
placeholder="e.g. E17"
committedValue={committedPostcode}
isOpen={openFilter === "postcode"}
onOpen={() => setOpenFilter(openFilter === "postcode" ? null : "postcode")}
onCommit={(v) => commitFilter("postcode", v)}
onClear={() => { setCommittedPostcode(""); setOpenFilter(null); }}
inputWidth="w-32"
/>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-gray-600">
Property Ref
</label>
<input
className={`${inputClass} w-40`}
placeholder="Landlord ref…"
value={quickPropertyRef}
onChange={(e) => {
setQuickPropertyRef(e.target.value);
handleQuickFilter("propertyRef", e.target.value);
}}
/>
</div>
<QuickFilterDropdown
label="Property Ref"
placeholder="Landlord ref…"
committedValue={committedPropertyRef}
isOpen={openFilter === "propertyRef"}
onOpen={() => setOpenFilter(openFilter === "propertyRef" ? null : "propertyRef")}
onCommit={(v) => commitFilter("propertyRef", v)}
onClear={() => { setCommittedPropertyRef(""); setOpenFilter(null); }}
inputWidth="w-40"
/>
{hasActiveFilters && (
<button
onClick={clearAll}
className="self-end h-9 px-3 text-sm text-gray-500 hover:text-gray-700 underline"
className="h-9 px-3 text-sm text-gray-500 hover:text-gray-700 underline"
>
Clear all
</button>