mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
doesn't mount straight away and reload data - keeps it stale with cache
This commit is contained in:
parent
03a87adfd2
commit
cc6cdebeeb
5 changed files with 196 additions and 96 deletions
|
|
@ -22,4 +22,4 @@ export async function POST(req: NextRequest) {
|
|||
filters
|
||||
);
|
||||
return NextResponse.json(properties);
|
||||
}
|
||||
}
|
||||
|
|
@ -222,3 +222,8 @@
|
|||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(300%); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "New Property" 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue