175 lines
6.9 KiB
TypeScript
175 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useSearchParams } from "next/navigation";
|
|
import Footer from "@/components/Footer";
|
|
import InnerBanner from "@/components/InnerBanner";
|
|
import PropertyFilters, { FilterState } from "@/components/PropertyFilters";
|
|
import PropertyCard from "@/components/PropertyCard";
|
|
import { properties } from "@/data/properties";
|
|
import { motion } from "framer-motion";
|
|
|
|
interface PropertiesClientProps {
|
|
initialFilters?: Partial<FilterState>;
|
|
headerRenderer?: (props: { filteredCount: number; totalCount: number }) => React.ReactNode;
|
|
bannerTitle?: string;
|
|
bannerSubtitle?: string;
|
|
bannerBackgroundImage?: string;
|
|
}
|
|
|
|
export default function PropertiesClient({
|
|
initialFilters,
|
|
headerRenderer,
|
|
bannerTitle = "Our Properties",
|
|
bannerSubtitle = "Discover your dream home from our exclusive collection of premium properties",
|
|
bannerBackgroundImage = "/assets/images/projects/projects-banner.webp"
|
|
}: PropertiesClientProps = {}) {
|
|
const searchParams = useSearchParams();
|
|
const [filteredProperties, setFilteredProperties] = useState(properties);
|
|
const [initializedFilters, setInitializedFilters] = useState<Partial<FilterState> | undefined>(undefined);
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const search = searchParams.get("search") || "";
|
|
const type = searchParams.get("type") || "all";
|
|
|
|
const initial: Partial<FilterState> = {
|
|
search,
|
|
type,
|
|
budgetMin: "",
|
|
budgetMax: "",
|
|
bhk: "all",
|
|
sortBy: "popularity"
|
|
};
|
|
|
|
setInitializedFilters(initial);
|
|
|
|
// Initial filter run
|
|
const filters = {
|
|
search,
|
|
type,
|
|
budgetMin: "",
|
|
budgetMax: "",
|
|
bhk: "all",
|
|
sortBy: "popularity"
|
|
};
|
|
|
|
handleFilterChange(filters);
|
|
setIsInitialized(true);
|
|
}, [searchParams]);
|
|
|
|
const handleFilterChange = (filters: FilterState) => {
|
|
let filtered = [...properties];
|
|
|
|
// Search filter
|
|
if (filters.search) {
|
|
filtered = filtered.filter(p =>
|
|
p.title.toLowerCase().includes(filters.search.toLowerCase()) ||
|
|
p.location.toLowerCase().includes(filters.search.toLowerCase()) ||
|
|
p.builder?.name.toLowerCase().includes(filters.search.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// Type filter
|
|
if (filters.type !== "all") {
|
|
filtered = filtered.filter(p => p.category === filters.type);
|
|
}
|
|
|
|
// BHK filter
|
|
if (filters.bhk !== "all") {
|
|
filtered = filtered.filter(p => p.overview.bhk.includes(filters.bhk));
|
|
}
|
|
|
|
// Budget filter
|
|
if (filters.budgetMin || filters.budgetMax) {
|
|
filtered = filtered.filter(p => {
|
|
const priceStr = p.price.replace(/[^0-9.]/g, "");
|
|
const price = parseFloat(priceStr);
|
|
const min = filters.budgetMin ? parseFloat(filters.budgetMin) : 0;
|
|
const max = filters.budgetMax ? parseFloat(filters.budgetMax) : Infinity;
|
|
return price >= min && price <= max;
|
|
});
|
|
}
|
|
|
|
// Sort
|
|
if (filters.sortBy === "price-low") {
|
|
filtered.sort((a, b) => {
|
|
const priceA = parseFloat(a.price.replace(/[^0-9.]/g, ""));
|
|
const priceB = parseFloat(b.price.replace(/[^0-9.]/g, ""));
|
|
return priceA - priceB;
|
|
});
|
|
} else if (filters.sortBy === "price-high") {
|
|
filtered.sort((a, b) => {
|
|
const priceA = parseFloat(a.price.replace(/[^0-9.]/g, ""));
|
|
const priceB = parseFloat(b.price.replace(/[^0-9.]/g, ""));
|
|
return priceB - priceA;
|
|
});
|
|
}
|
|
|
|
setFilteredProperties(filtered);
|
|
};
|
|
|
|
if (!isInitialized) return null; // Logic to wait for params
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 dark:bg-black">
|
|
|
|
<InnerBanner
|
|
title={bannerTitle}
|
|
subtitle={bannerSubtitle}
|
|
breadcrumbs={[
|
|
{ label: "Home", href: "/" },
|
|
{ label: "Properties" }
|
|
]}
|
|
backgroundImage={bannerBackgroundImage}
|
|
/>
|
|
|
|
<div>
|
|
<PropertyFilters onFilterChange={handleFilterChange} initialFilters={initializedFilters} />
|
|
|
|
<div className="max-w-7xl mx-auto px-6 py-12">
|
|
{headerRenderer ? (
|
|
headerRenderer({ filteredCount: filteredProperties.length, totalCount: properties.length })
|
|
) : (
|
|
<motion.h2
|
|
className="text-3xl font-bold text-foreground mb-8"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.5 }}
|
|
>
|
|
Browse Our Properties
|
|
</motion.h2>
|
|
)}
|
|
|
|
{/* Property Grid */}
|
|
{filteredProperties.length > 0 ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{filteredProperties.map((property, index) => (
|
|
<motion.div
|
|
key={property.id}
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: "-50px" }}
|
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
|
>
|
|
<PropertyCard property={property} />
|
|
</motion.div>
|
|
))} </div>
|
|
) : (
|
|
<div className="text-center py-20">
|
|
<svg className="w-20 h-20 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
<h3 className="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">No properties found</h3>
|
|
<p className="text-gray-500 dark:text-gray-400">Try adjusting your filters to see more results</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<Footer />
|
|
</div >
|
|
);
|
|
}
|