doesn't mount straight away and reload data - keeps it stale with cache

This commit is contained in:
Jun-te Kim 2026-01-07 11:33:20 +00:00
parent 03a87adfd2
commit cc6cdebeeb
5 changed files with 196 additions and 96 deletions

View file

@ -22,4 +22,4 @@ export async function POST(req: NextRequest) {
filters
);
return NextResponse.json(properties);
}
}

View file

@ -222,3 +222,8 @@
display: none !important;
}
}
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(300%); }
}

View file

@ -31,7 +31,6 @@ export default function PropertyFilters({
----------------------------------------- */
useEffect(() => {
if (currentEpc && expectedEpc) {
// expected must be BETTER than current
if (epcIndex(expectedEpc) >= epcIndex(currentEpc)) {
setExpectedEpc("");
}
@ -69,83 +68,118 @@ export default function PropertyFilters({
});
}
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === "Enter") {
e.preventDefault();
apply();
}
}
return (
<div className="flex gap-4 bg-gray-50 p-4 rounded-lg items-end">
{/* Address */}
<input
className="border rounded px-3 py-2"
placeholder="Address contains…"
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
{/* Postcode */}
<input
className="border rounded px-3 py-2"
placeholder="Postcode"
value={postcode}
onChange={(e) => setPostcode(e.target.value)}
/>
{/* Current EPC */}
<select
className="border rounded px-3 py-2"
value={currentEpc}
onChange={(e) =>
setCurrentEpc(e.target.value as any)
}
<div className="border-b bg-white">
<div
className="grid grid-cols-12 gap-4 p-4"
onKeyDown={handleKeyDown}
>
<option value="">Current EPC is</option>
{["C", "D", "E", "F", "G"].map((epc) => (
<option
key={epc}
value={epc}
disabled={
expectedEpc &&
epcIndex(epc) <= epcIndex(expectedEpc)
}
{/* Address */}
<div className="col-span-4">
<label className="block text-xs font-medium text-gray-600 mb-1">
Address
</label>
<input
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm
focus:outline-none focus:ring-2 focus:ring-black/10"
placeholder="Contains…"
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
</div>
{/* Postcode */}
<div className="col-span-2">
<label className="block text-xs font-medium text-gray-600 mb-1">
Postcode
</label>
<input
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm
focus:outline-none focus:ring-2 focus:ring-black/10"
placeholder="e.g. E17"
value={postcode}
onChange={(e) => setPostcode(e.target.value)}
/>
</div>
{/* Current EPC */}
<div className="col-span-2">
<label className="block text-xs font-medium text-gray-600 mb-1">
Current EPC
</label>
<select
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm
bg-white focus:outline-none focus:ring-2 focus:ring-black/10"
value={currentEpc}
onChange={(e) => setCurrentEpc(e.target.value as any)}
>
{epc} or below
</option>
))}
</select>
<option value="">Any</option>
{["C", "D", "E", "F", "G"].map((epc) => (
<option
key={epc}
value={epc}
disabled={
expectedEpc &&
epcIndex(epc) <= epcIndex(expectedEpc)
}
>
{epc} or below
</option>
))}
</select>
</div>
{/* Expected EPC */}
<select
className="border rounded px-3 py-2"
value={expectedEpc}
onChange={(e) =>
setExpectedEpc(e.target.value as any)
}
>
<option value="">Expected EPC at least</option>
{["A", "B", "C", "D"].map((epc) => (
<option
key={epc}
value={epc}
disabled={
currentEpc &&
epcIndex(epc) >= epcIndex(currentEpc)
}
{/* Expected EPC */}
<div className="col-span-2">
<label className="block text-xs font-medium text-gray-600 mb-1">
Expected EPC
</label>
<select
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm
bg-white focus:outline-none focus:ring-2 focus:ring-black/10"
value={expectedEpc}
onChange={(e) => setExpectedEpc(e.target.value as any)}
>
{epc} or above
</option>
))}
</select>
<option value="">Any</option>
{["A", "B", "C", "D"].map((epc) => (
<option
key={epc}
value={epc}
disabled={
currentEpc &&
epcIndex(epc) >= epcIndex(currentEpc)
}
>
{epc} or above
</option>
))}
</select>
</div>
<button
onClick={apply}
className="bg-black text-white px-4 py-2 rounded"
>
Apply
</button>
<button
onClick={clear}
className="text-gray-500 px-2"
>
Clear
</button>
{/* Actions */}
<div className="col-span-2 flex items-end gap-2">
<button
onClick={apply}
className="h-10 w-full rounded-md bg-black text-sm font-medium text-white
hover:bg-black/90 transition"
>
Apply
</button>
<button
onClick={clear}
className="h-10 px-3 text-sm text-gray-500 hover:text-gray-700"
>
Clear
</button>
</div>
</div>
</div>
);
}

View file

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useMemo } from "react";
import { useProperties } from "./useProperties";
import DataTable from "./dataTable";
import PropertyFilters, {
@ -9,8 +9,11 @@ import PropertyFilters, {
import { PropertyFilter } from "@/app/utils/propertyFilters";
import { HomeIcon } from "@heroicons/react/24/outline";
import { columns } from "@/app/portfolio/[slug]/components/propertyTableColumns";
import { useMemo } from "react";
import clsx from "clsx";
/* ----------------------------------------
Filter parsing
----------------------------------------- */
export function parsePropertyFilters(
filters: PropertyFilterValues
): PropertyFilter[] {
@ -47,26 +50,35 @@ export function parsePropertyFilters(
value: filters.expected_epc_at_least,
});
}
console.log(parsed)
return parsed;
}
/* ----------------------------------------
Empty portfolio state
----------------------------------------- */
function EmptyPropertyState() {
return (
<div className="flex justify-center h-1/2">
<div className="bg-white rounded-lg w-full">
<p className="text-center text-gray-400 pt-6">
Hover over &quot;New Property&quot; to start adding properties to your
Portfolio
<HomeIcon className="h-20 w-20 mx-auto mt-4 text-gray-200" />
<div className="flex justify-center py-16">
<div className="text-center text-gray-400">
<HomeIcon className="h-16 w-16 mx-auto mb-4 text-gray-200" />
<p>
Hover over <strong>New Property</strong> to start adding properties
to your portfolio.
</p>
</div>
</div>
);
}
export default function PropertyTable({ portfolioId }: { portfolioId: string }) {
/* ----------------------------------------
Main table
----------------------------------------- */
export default function PropertyTable({
portfolioId,
}: {
portfolioId: string;
}) {
const [filters, setFilters] = useState<PropertyFilterValues>({
address: "",
postcode: "",
@ -79,31 +91,74 @@ export default function PropertyTable({ portfolioId }: { portfolioId: string })
[filters]
);
const { data = [], isLoading, isFetching, isError } = useProperties({
const hasActiveFilters = parsedFilters.length > 0;
const {
data = [],
isLoading,
isFetching,
isError,
} = useProperties({
portfolioId,
filters: parsedFilters,
filters: parsedFilters,
});
return (
<div className="flex justify-center">
<div className="grid grid-cols-11 w-full max-w-8xl">
<div className="col-span-11 bg-white">
<div className="col-span-11 bg-white rounded-md border">
{/* Filters */}
<PropertyFilters onApply={setFilters} />
{/* Loading bar (HubSpot-style) */}
{isFetching && (
<div className="h-1 w-full bg-gray-100 overflow-hidden">
<div className="h-full w-1/3 bg-black animate-[loading_1.2s_infinite]" />
</div>
)}
{/* Filter info */}
{hasActiveFilters && !isFetching && (
<div className="px-4 py-2 text-xs text-gray-500 border-b">
Filters applied ({parsedFilters.length})
</div>
)}
{/* Content */}
{isLoading ? (
<div className="p-6 text-gray-400">Loading properties</div>
) : isError || data.length === 0 ? (
<div className="p-6 text-gray-400">
Loading properties
</div>
) : isError ? (
<div className="p-6 text-red-500">
Failed to load properties.
</div>
) : data.length === 0 && hasActiveFilters ? (
<div className="p-10 text-center text-gray-500">
<p>No properties match your filters.</p>
<button
onClick={() =>
setFilters({
address: "",
postcode: "",
current_epc_at_most: "",
expected_epc_at_least: "",
})
}
className="mt-3 text-sm text-black underline"
>
Clear filters
</button>
</div>
) : data.length === 0 ? (
<EmptyPropertyState />
) : (
<>
{isFetching && (
<div className="text-sm text-gray-400 px-4">Updating</div>
)}
<DataTable data={data} columns={columns} />
</>
<DataTable data={data} columns={columns} />
)}
</div>
</div>
</div>
);
}
}

View file

@ -25,6 +25,12 @@ export function useProperties({ portfolioId, filters }: Params) {
if (!res.ok) throw new Error("Failed to fetch properties");
return res.json();
},
staleTime: 1000 * 60 * 5, // 5 minutes
cacheTime: 1000 * 60 * 30, // 30 minutes
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
keepPreviousData: true,
});
}