Todos os componentes

Property Filters

Property Filters é um componente para React e Tailwind CSS, da biblioteca Bund UI, com licença MIT. Copia e cola no teu projeto.

Bund UI MIT other

O que é

Property Filters é um componente para React e Tailwind CSS, da biblioteca Bund UI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Precisa de

npm i lucide-react

Licença

Este componente vem de Bund UI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Property Filters

examples/blocks/real-estate/property-filters/01/page.tsx
"use client";

import { useState, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Check, Info } from "lucide-react";

const properties = [
  {
    id: 1,
    title: "3 BHK Residential Apartment in Ayyapakkam",
    location: "BBCL Ayyapakkam",
    price: 95000,
    pricePerSqFt: 73,
    area: 1305,
    areaDetails: "(121 sq.m.) Super built-up Area",
    bhk: 3,
    baths: 2,
    description:
      "Residential apartment for sale in BBCL Ayyapakkam, Chennai. This property promises a contented and comfortable lifestyle with modern amenities and excellent connectivity.",
    verified: true,
    postedDate: "April 22, 2020",
    owner: "Inez Freeman",
    image:
      "https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    society: "vijay",
    furnishing: "furnished",
    underConstruction: false,
    readyToMove: true,
    hasPhotos: true
  },
  {
    id: 2,
    title: "3 BHK Residential Apartment in K.K. Nagar",
    location: "NML K.K. Nagar",
    price: 98000,
    pricePerSqFt: 75,
    area: 1305,
    areaDetails: "(121 sq.m.) Super built-up Area",
    bhk: 3,
    baths: 2,
    description:
      "This beautiful east facing, Vastu compliant unit is situated on ground of 2 floors, in a builder floor building with premium finishes and great natural light.",
    verified: false,
    postedDate: "May 13, 2020",
    owner: "Carrie Reyes",
    image:
      "https://images.unsplash.com/photo-1580587771525-78b9dba3b914?q=80&w=2574&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    society: "olympia",
    furnishing: "Semi furnished",
    underConstruction: true,
    readyToMove: false,
    hasPhotos: true
  },
  {
    id: 3,
    title: "3 BHK Residential Apartment in Lancor Abode Valley",
    location: "Lancor Abode Valley, Potheri",
    price: 92000,
    pricePerSqFt: 70,
    area: 1305,
    areaDetails: "(121 sq.m.) Super built-up Area",
    bhk: 3,
    baths: 2,
    description:
      "This builder floor is a semi-furnished unit and consists of one wardrobe, AC, fans, lights and modular kitchen with excellent amenities in a gated community.",
    verified: true,
    postedDate: "June 13, 2020",
    owner: "Mitchell Spencer",
    image:
      "https://plus.unsplash.com/premium_photo-1689609950112-d66095626efb?q=80&w=1287&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    society: "alliance",
    furnishing: "Semi furnished",
    underConstruction: false,
    readyToMove: true,
    hasPhotos: true
  },
  {
    id: 4,
    title: "2 BHK Apartment in Provident Cosmo City",
    location: "Provident Cosmo City",
    price: 65000,
    pricePerSqFt: 68,
    area: 950,
    areaDetails: "(88 sq.m.) Super built-up Area",
    bhk: 2,
    baths: 2,
    description:
      "Spacious 2 bedroom apartment with modern amenities, gym, swimming pool and children's play area. Perfect for small families looking for quality living.",
    verified: true,
    postedDate: "March 15, 2020",
    owner: "Sarah Johnson",
    image:
      "https://plus.unsplash.com/premium_photo-1689609950069-2961f80b1e70?q=80&w=1287&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    society: "provident",
    furnishing: "Fully furnished",
    underConstruction: false,
    readyToMove: true,
    hasPhotos: true
  },
  {
    id: 5,
    title: "4 BHK Luxury Villa in KG Signature City",
    location: "KG Signature City",
    price: 185000,
    pricePerSqFt: 92,
    area: 2010,
    areaDetails: "(187 sq.m.) Super built-up Area",
    bhk: 4,
    baths: 3,
    description:
      "Luxurious 4 bedroom villa with premium specifications, private garden, and exclusive clubhouse facilities. Experience elevated living in a prime location.",
    verified: true,
    postedDate: "July 8, 2020",
    owner: "David Williams",
    image:
      "https://images.unsplash.com/photo-1605146769289-440113cc3d00?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
    society: "kg",
    furnishing: "Unfurnished",
    underConstruction: false,
    readyToMove: false,
    hasPhotos: true
  }
];

const societies = [
  { id: "provident", name: "Provident Cosmo City" },
  { id: "olympia", name: "Olympia Opaline" },
  { id: "vijay", name: "Vijay Shanthi Lotus Pond", checked: true },
  { id: "kg", name: "KG Signature City" },
  { id: "alliance", name: "Alliance Orchid Springs" }
];

export default function RealEstateFilterPage() {
  const [budget, setBudget] = useState([20, 200]);
  const [selectedBedrooms, setSelectedBedrooms] = useState<number[]>([]);
  const [selectedFurnishing, setSelectedFurnishing] = useState<string[]>([]);
  const [selectedSocieties, setSelectedSocieties] = useState<string[]>([]);
  const [ownerVerified, setOwnerVerified] = useState(false);
  const [underConstruction, setUnderConstruction] = useState(false);
  const [readyToMove, setReadyToMove] = useState(false);
  const [withPhotos, setWithPhotos] = useState(false);
  const [familyMode, setFamilyMode] = useState(false);
  const [sortBy, setSortBy] = useState("price");

  const filteredAndSortedProperties = useMemo(() => {
    const filtered = properties.filter((property) => {
      // Budget filter (price in thousands)
      if (property.price < budget[0] * 1000 || property.price > budget[1] * 1000) return false;

      // Bedroom filter
      if (selectedBedrooms.length > 0 && !selectedBedrooms.includes(property.bhk)) return false;

      // Society filter
      if (selectedSocieties.length > 0 && !selectedSocieties.includes(property.society))
        return false;

      // Furnishing filter
      if (selectedFurnishing.length > 0 && !selectedFurnishing.includes(property.furnishing))
        return false;

      // Top filter badges
      if (ownerVerified && !property.verified) return false;
      if (underConstruction && !property.underConstruction) return false;
      if (readyToMove && !property.readyToMove) return false;
      if (withPhotos && !property.hasPhotos) return false;

      return true;
    });

    // Sort filtered results
    filtered.sort((a, b) => {
      if (sortBy === "price") return a.price - b.price;
      if (sortBy === "area") return b.area - a.area;
      if (sortBy === "date")
        return new Date(b.postedDate).getTime() - new Date(a.postedDate).getTime();
      return 0;
    });

    return filtered;
  }, [
    budget,
    selectedBedrooms,
    selectedSocieties,
    selectedFurnishing,
    ownerVerified,
    underConstruction,
    readyToMove,
    withPhotos,
    sortBy
  ]);

  const toggleBedroom = (bedroom: number) => {
    setSelectedBedrooms((prev) =>
      prev.includes(bedroom) ? prev.filter((b) => b !== bedroom) : [...prev, bedroom]
    );
  };

  const toggleFurnishing = (furnishing: string) => {
    setSelectedFurnishing((prev) =>
      prev.includes(furnishing) ? prev.filter((f) => f !== furnishing) : [...prev, furnishing]
    );
  };

  const toggleSociety = (society: string) => {
    setSelectedSocieties((prev) =>
      prev.includes(society) ? prev.filter((s) => s !== society) : [...prev, society]
    );
  };

  const clearAllFilters = () => {
    setBudget([20, 200]);
    setSelectedBedrooms([]);
    setSelectedFurnishing([]);
    setSelectedSocieties([]);
    setOwnerVerified(false);
    setUnderConstruction(false);
    setReadyToMove(false);
    setWithPhotos(false);
  };

  const formatPrice = (price: number) => {
    return new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
      maximumFractionDigits: 0
    }).format(price);
  };

  return (
    <>
      {/* Header */}
      <header className="border-b">
        <div className="mx-auto max-w-7xl px-4 py-4">
          <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
            <div className="flex items-center justify-between">
              <div>
                <h1 className="text-xl font-semibold">
                  {filteredAndSortedProperties.length} search results
                </h1>
              </div>
            </div>
          </div>
        </div>
      </header>

      {/* Filters Bar */}
      <div className="border-b">
        <div className="mx-auto max-w-7xl px-4 py-4">
          <div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
            <div className="flex flex-wrap gap-2">
              <Badge
                variant={ownerVerified ? "default" : "secondary"}
                className="cursor-pointer px-3 py-1"
                onClick={() => setOwnerVerified(!ownerVerified)}>
                Owner Verified
              </Badge>
              <Badge
                variant={underConstruction ? "default" : "secondary"}
                className="cursor-pointer px-3 py-1"
                onClick={() => setUnderConstruction(!underConstruction)}>
                Under Construction
              </Badge>
              <Badge
                variant={readyToMove ? "default" : "secondary"}
                className="cursor-pointer px-3 py-1"
                onClick={() => setReadyToMove(!readyToMove)}>
                Ready To Move
              </Badge>
              <Badge
                variant={withPhotos ? "default" : "secondary"}
                className="cursor-pointer px-3 py-1"
                onClick={() => setWithPhotos(!withPhotos)}>
                With Photos
              </Badge>
            </div>
            <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
              <div className="flex items-center gap-2">
                <Switch id="family-mode" checked={familyMode} onCheckedChange={setFamilyMode} />
                <Label htmlFor="family-mode" className="flex items-center gap-1 text-sm">
                  Family mode <Info className="text-muted-foreground size-4" />
                </Label>
              </div>
              <div className="flex items-center gap-2">
                <Select value={sortBy} onValueChange={setSortBy}>
                  <SelectTrigger id="sort" className="">
                    Sort by: <SelectValue />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="price">Price</SelectItem>
                    <SelectItem value="date">Date</SelectItem>
                    <SelectItem value="area">Area</SelectItem>
                  </SelectContent>
                </Select>
              </div>
            </div>
          </div>
        </div>
      </div>

      {/* Main Content */}
      <div className="mx-auto max-w-7xl px-4 py-6">
        <div className="grid gap-6 lg:grid-cols-[300px_1fr]">
          {/* Sidebar Filters */}
          <aside className="space-y-4">
            <Card className="shadow-none">
              <CardHeader className="flex items-center justify-between">
                <CardTitle>Applied Filters</CardTitle>
                <CardAction>
                  <Button
                    variant="ghost"
                    size="sm"
                    className="text-primary"
                    onClick={clearAllFilters}>
                    Clear All
                  </Button>
                </CardAction>
              </CardHeader>
              <CardContent className="space-y-6">
                {/* Budget Filter */}
                <div className="space-y-3">
                  <Label className="font-semibold">Budget</Label>
                  <div className="px-2 pt-2">
                    <Slider
                      value={budget}
                      onValueChange={setBudget}
                      min={20}
                      max={200}
                      step={5}
                      className="mb-4"
                    />
                    <div className="text-muted-foreground flex justify-between text-sm">
                      <span>${budget[0]}k</span>
                      <span>${budget[1]}k</span>
                    </div>
                  </div>
                </div>

                {/* Bedrooms Filter */}
                <div className="space-y-3">
                  <Label className="font-semibold">No. of Bedrooms</Label>
                  <div className="grid grid-cols-3 gap-2">
                    {[1, 2, 3, 4, 6, 7, 8].map((bedroom) => (
                      <Button
                        key={bedroom}
                        variant={selectedBedrooms.includes(bedroom) ? "default" : "outline"}
                        size="sm"
                        onClick={() => toggleBedroom(bedroom)}>
                        {bedroom}BHK
                      </Button>
                    ))}
                  </div>
                </div>

                {/* Societies Filter */}
                <div className="space-y-3">
                  <Label className="font-semibold">New Projects / Societies</Label>
                  <div className="space-y-2">
                    {societies.map((society) => (
                      <div key={society.id} className="flex items-center space-x-2">
                        <Checkbox
                          id={society.id}
                          checked={selectedSocieties.includes(society.id)}
                          onCheckedChange={() => toggleSociety(society.id)}
                        />
                        <Label htmlFor={society.id} className="cursor-pointer text-sm font-normal">
                          {society.name}
                        </Label>
                      </div>
                    ))}
                  </div>
                </div>

                {/* Furnishing Status Filter */}
                <div className="space-y-3">
                  <Label className="font-semibold">Furnishing Status</Label>
                  <div className="grid grid-cols-2 gap-2">
                    {["Unfurnished", "Semi furnished", "furnished", "Fully furnished"].map(
                      (furnishing) => (
                        <Button
                          key={furnishing}
                          variant={selectedFurnishing.includes(furnishing) ? "default" : "outline"}
                          size="sm"
                          onClick={() => toggleFurnishing(furnishing)}
                          className="text-xs">
                          {furnishing}
                        </Button>
                      )
                    )}
                  </div>
                </div>
              </CardContent>
            </Card>
          </aside>

          {/* Property Listings */}
          <main className="space-y-6">
            {filteredAndSortedProperties.length === 0 ? (
              <Card>
                <CardContent className="p-12 text-center">
                  <p className="text-muted-foreground text-lg">
                    No properties found matching your filters. Try adjusting your search criteria.
                  </p>
                </CardContent>
              </Card>
            ) : (
              filteredAndSortedProperties.map((property) => (
                <Card key={property.id} className="py-0 shadow-none">
                  <CardContent className="p-0">
                    <div className="grid md:grid-cols-[250px_1fr]">
                      {/* Property Image */}
                      <div className="relative">
                        <img
                          src={property.image || "/placeholder.svg"}
                          alt={property.title}
                          className="aspect-square h-full w-full object-cover md:rounded-l-lg"
                        />
                        {property.verified && (
                          <Badge className="absolute top-2 right-2 bg-white text-green-600 hover:bg-white">
                            <Check /> Verified
                          </Badge>
                        )}
                      </div>

                      {/* Property Details */}
                      <div className="px-6 py-4">
                        <div className="space-y-4">
                          <div>
                            <h3 className="text-foreground text-lg font-semibold">
                              {property.title}
                            </h3>
                            <p className="text-muted-foreground text-sm">{property.location}</p>
                          </div>

                          <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
                            <div>
                              <p className="text-xl font-bold">{formatPrice(property.price)}</p>
                              <p className="text-muted-foreground text-xs">
                                ${property.pricePerSqFt}/sq.ft.
                              </p>
                            </div>
                            <div>
                              <p className="text-xl font-bold">{property.area} sq.ft.</p>
                              <p className="text-muted-foreground text-xs">
                                {property.areaDetails}
                              </p>
                            </div>
                            <div>
                              <p className="text-xl font-bold">{property.bhk} BHK</p>
                              <p className="text-muted-foreground text-xs">
                                {property.baths} Baths
                              </p>
                            </div>
                          </div>

                          <p className="text-muted-foreground line-clamp-2 text-sm">
                            {property.description}{" "}
                            <span className="text-primary cursor-pointer">More</span>
                          </p>

                          <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
                            <Button className="w-full sm:w-auto">Contact Owner</Button>
                            <div className="text-muted-foreground text-xs sm:text-right">
                              <p>
                                Posted on {property.postedDate} by Owner {property.owner}
                              </p>
                            </div>
                          </div>
                        </div>
                      </div>
                    </div>
                  </CardContent>
                </Card>
              ))
            )}
          </main>
        </div>
      </div>
    </>
  );
}

Componentes parecidos