Menu

Change language

GitHub repo

Property Amenities Grid

blockreal-estate

Building and luxury residential amenities matrix featuring categorized lifestyle zones, interactive category tabs, instant keyword search, and detailed feature cards with pool photography thumbnail.

Installation

$npx uipkge-ng@latest add property-amenities-grid
Manifest:https://uipkge.dev/r/angular/property-amenities-grid.jsonInstalls to:src/app/components/blocks/property-amenities-grid/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

AmenityCategory
interface AmenityCategory {
  id: CategoryId
  label: string
  count: number
  icon: typeof Building2
  description: string
}

Files installed (2)

  • src/app/components/blocks/property-amenities-grid/property-amenities-grid.component.ts46.9 kB
    // Building and luxury residential amenities matrix: categorized lifestyle zones,
    // interactive category tabs, instant keyword search, and detailed feature cards with a
    // pool photography thumbnail. Port of the Vue block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      ArrowRight,
      Building2,
      Calendar,
      Car,
      Check,
      CircleDot,
      Clock,
      Compass,
      ConciergeBell,
      Dumbbell,
      FileText,
      Flame,
      HeartPulse,
      Laptop,
      LucideAngularModule,
      MapPin,
      Package,
      PawPrint,
      Search,
      ShieldCheck,
      Shirt,
      Video,
      Wifi,
      X,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    type CategoryId = 'all' | 'wellness' | 'work' | 'services' | 'parking'
    
    interface AmenityCategory {
      id: CategoryId
      label: string
      count: number
      icon: typeof Building2
      description: string
    }
    
    const categories: AmenityCategory[] = [
      {
        id: 'all',
        label: 'All Amenities',
        count: 24,
        icon: Building2,
        description: 'Explore the full suite of resident services, wellness facilities, and private lounges.',
      },
      {
        id: 'wellness',
        label: 'Wellness & Recreation',
        count: 4,
        icon: HeartPulse,
        description: 'Resort-inspired spaces to rejuvenate body and mind, featuring sky-high fitness and hydrotherapy.',
      },
      {
        id: 'work',
        label: 'Work & Productivity',
        count: 3,
        icon: Laptop,
        description: 'High-speed professional environments for focused execution, remote meetings, and collaboration.',
      },
      {
        id: 'services',
        label: 'Convenience & Services',
        count: 3,
        icon: ConciergeBell,
        description: 'White-glove 24/7 concierge, contactless cold storage lockers, and on-demand valet care.',
      },
      {
        id: 'parking',
        label: 'Parking & Pet Care',
        count: 2,
        icon: Car,
        description: 'Subterranean EV charging and a dedicated pet-care suite.',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-property-amenities-grid, [ui-property-amenities-grid]',
      standalone: true,
      // Vue renders the root <section> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiSeparatorComponent,
      ],
      template: `
        <section data-slot="property-amenities-grid" [class]="rootClass">
          <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
            <!-- Top Section Header -->
            <div class="flex flex-col items-start justify-between gap-6 lg:flex-row lg:items-end">
              <div class="max-w-2xl space-y-3">
                <div class="flex flex-wrap items-center gap-2">
                  <span
                    ui-badge
                    variant="outline"
                    class="border-primary/30 bg-primary/5 text-primary gap-1.5 px-3 py-1 text-xs font-medium"
                  >
                    <lucide-icon [img]="Compass" class="size-3.5" />
                    {{ badge }}
                  </span>
                  <span ui-badge variant="secondary" class="gap-1 px-2.5 py-1 text-xs font-medium">
                    <lucide-icon [img]="Building2" class="size-3" />
                    24 Total Amenities
                  </span>
                </div>
                <h2 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl">
                  {{ title }}
                </h2>
                <p class="text-muted-foreground text-base sm:text-lg">
                  {{ subtitle }}
                </p>
              </div>
    
              <!-- Quick Summary Stats Bar -->
              <div class="grid grid-cols-2 gap-3 sm:grid-cols-4 sm:gap-4">
                <div class="bg-card border-border rounded-xl border p-3 shadow-xs">
                  <p class="text-foreground font-mono text-xl font-bold">24</p>
                  <p class="text-muted-foreground text-xs font-medium">Curated Amenities</p>
                </div>
                <div class="bg-card border-border rounded-xl border p-3 shadow-xs">
                  <p class="text-foreground font-mono text-xl font-bold">4</p>
                  <p class="text-muted-foreground text-xs font-medium">Lifestyle Zones</p>
                </div>
                <div class="bg-card border-border rounded-xl border p-3 shadow-xs">
                  <p class="text-foreground font-mono text-xl font-bold">24/7</p>
                  <p class="text-muted-foreground text-xs font-medium">Concierge Access</p>
                </div>
                <div class="bg-card border-border rounded-xl border p-3 shadow-xs">
                  <p class="text-foreground font-mono text-xl font-bold">100%</p>
                  <p class="text-muted-foreground text-xs font-medium">Pet Friendly</p>
                </div>
              </div>
            </div>
    
            <div ui-separator class="my-8"></div>
    
            <!-- Interactive Controls: Category Filter Tabs & Quick Search -->
            <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
              <!-- Category Filter Pills -->
              <div class="flex flex-wrap items-center gap-2">
                @for (cat of categoryList; track cat.id) {
                  <button
                    ui-button
                    size="sm"
                    [variant]="activeCategory() === cat.id ? 'default' : 'outline'"
                    class="h-8 gap-1.5 rounded-full px-3.5 text-xs font-medium"
                    (click)="activeCategory.set(cat.id)"
                  >
                    <lucide-icon [img]="cat.icon" class="size-3.5" />
                    <span>{{ cat.label }}</span>
                    <span [class]="pillCountClass(cat.id)">
                      {{ cat.count }}
                    </span>
                  </button>
                }
              </div>
    
              <!-- Keyword Search Input -->
              <div class="relative w-full sm:w-72">
                <lucide-icon
                  [img]="Search"
                  class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2"
                />
                <input
                  [value]="searchQuery()"
                  (input)="searchQuery.set($any($event.target).value)"
                  type="text"
                  placeholder="Search amenities..."
                  class="bg-card border-border placeholder:text-muted-foreground focus-visible:ring-primary h-9 w-full min-w-0 rounded-lg border pr-8 pl-9 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none"
                />
                @if (searchQuery()) {
                  <button
                    type="button"
                    class="text-muted-foreground hover:text-foreground absolute top-1/2 right-2.5 -translate-y-1/2"
                    aria-label="Clear search"
                    (click)="searchQuery.set('')"
                  >
                    <lucide-icon [img]="X" class="size-3.5" />
                  </button>
                }
              </div>
            </div>
    
            <!-- Search Results Indicator -->
            @if (isSearching()) {
              <div
                class="bg-muted/40 border-border mt-4 flex items-center justify-between rounded-lg border px-4 py-2 text-xs"
              >
                <span class="text-muted-foreground">
                  Showing matching results for <strong class="text-foreground">"{{ searchQuery() }}"</strong>
                </span>
                <button
                  type="button"
                  class="text-primary font-medium hover:underline"
                  (click)="searchQuery.set('')"
                >
                  Reset Filter
                </button>
              </div>
            }
    
            <!-- Categories Container -->
            <div class="mt-10 space-y-14">
              <!-- CATEGORY 1: WELLNESS & RECREATION -->
              @if (showWellness()) {
                <div class="space-y-6">
                  <div class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center">
                    <div class="flex items-center gap-3">
                      <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                        <lucide-icon [img]="HeartPulse" class="size-5" />
                      </div>
                      <div>
                        <h3 class="text-foreground text-xl font-bold tracking-tight">Wellness & Recreation</h3>
                        <p class="text-muted-foreground text-xs sm:text-sm">
                          Resort-inspired spaces to rejuvenate body and mind with skyline views and recovery hydrotherapy.
                        </p>
                      </div>
                    </div>
                    <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs font-medium">
                      4 Featured Amenities
                    </span>
                  </div>
    
                  <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
                    @if (matchesQuery('Heated Rooftop Infinity Pool pool skyline swim cabana')) {
                      <div
                        ui-card
                        class="group border-border bg-card relative flex flex-col justify-between overflow-hidden shadow-xs transition-colors duration-300 hover:shadow-md md:col-span-2 lg:col-span-2"
                      >
                        <div class="relative h-64 w-full overflow-hidden sm:h-72">
                          <img
                            src="https://images.unsplash.com/photo-1576013551627-0cc20b96c2a7?w=1200&auto=format&fit=crop&q=80"
                            alt="Heated Rooftop Infinity Pool"
                            class="size-full object-cover transition-transform duration-500 ease-out group-hover:scale-105"
                          />
                          <div class="absolute inset-0 bg-gradient-to-t from-black/85 via-black/35 to-transparent"></div>
                          <div class="absolute top-4 left-4 flex flex-wrap gap-2">
                            <span
                              ui-badge
                              class="bg-primary text-primary-foreground border-transparent text-xs font-medium shadow-xs"
                            >
                              Featured Highlight
                            </span>
                            <span
                              ui-badge
                              variant="secondary"
                              class="bg-background/80 text-foreground border-white/20 text-xs font-medium backdrop-blur-md"
                            >
                              Year-Round Heated (84°F)
                            </span>
                          </div>
                          <div class="absolute right-4 bottom-4 left-4 text-white">
                            <div class="flex items-center gap-1.5 text-xs text-zinc-300">
                              <lucide-icon [img]="MapPin" class="size-3.5 text-zinc-300" />
                              <span>Level 42 Sky Deck</span>
                              <span class="mx-1.5">·</span>
                              <lucide-icon [img]="Clock" class="size-3.5 text-zinc-300" />
                              <span>6:00 AM – 10:00 PM Daily</span>
                            </div>
                          </div>
                        </div>
    
                        <div ui-card-content class="flex flex-1 flex-col justify-between p-6">
                          <div class="space-y-2">
                            <h4 ui-card-title class="text-foreground text-xl font-bold">Heated Rooftop Infinity Pool</h4>
                            <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                              50-meter temperature-controlled infinity pool on the 42nd floor featuring private cabanas,
                              teak daybeds, outdoor sun deck, and poolside refreshment bar with 360-degree panoramic skyline
                              views.
                            </p>
                          </div>
    
                          <div class="border-border mt-6 flex flex-wrap items-center gap-2 border-t pt-4">
                            <span ui-badge variant="outline" class="bg-muted/30 text-xs font-normal">
                              <lucide-icon [img]="Check" class="text-success mr-1 size-3" />
                              Panoramic Views
                            </span>
                            <span ui-badge variant="outline" class="bg-muted/30 text-xs font-normal">
                              <lucide-icon [img]="Check" class="text-success mr-1 size-3" />
                              Private Cabanas
                            </span>
                            <span ui-badge variant="outline" class="bg-muted/30 text-xs font-normal">
                              <lucide-icon [img]="Check" class="text-success mr-1 size-3" />
                              Towel Valet
                            </span>
                            <span ui-badge variant="outline" class="bg-muted/30 text-xs font-normal">
                              <lucide-icon [img]="Check" class="text-success mr-1 size-3" />
                              Poolside Bar
                            </span>
                          </div>
                        </div>
                      </div>
                    }
    
                    @if (matchesQuery('Equinox-style Fitness Center & Yoga Studio gym pilates workout')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="Dumbbell" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">24/7 Access</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Levels 3 & 4
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">
                              Equinox-style Fitness Center & Yoga Studio
                            </h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            Multi-level 4,500 sq ft strength and conditioning training facility outfitted with Woodway
                            treadmills, Peloton bikes, Olympic lifting racks, and a dedicated sunlit yoga and reformer
                            Pilates studio.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Woodway & Peloton</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Reformer Pilates</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Personal Trainers</span>
                          </div>
                        </div>
                      </div>
                    }
    
                    @if (matchesQuery('Nordic Sauna & Cold Plunge Spa steam therapy recovery')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="Flame" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">Hydrotherapy</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Level 3 Spa Suite
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">Nordic Sauna & Cold Plunge Spa</h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            Therapeutic cedarwood dry sauna, aromatic eucalyptus steam room, and dual 48°F contrast therapy
                            cold plunge pools engineered for post-workout muscle recovery and immune resilience.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">48°F Cold Plunge</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Cedar Dry Sauna</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Eucalyptus Steam</span>
                          </div>
                        </div>
                      </div>
                    }
    
                    @if (matchesQuery('Private Tennis & Pickleball Court sports rooftop court')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md md:col-span-2 lg:col-span-2"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="CircleDot" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">Rooftop Court</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Level 43 Sports Deck
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">
                              Private Tennis & Pickleball Court
                            </h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            Championship-grade rooftop regulation court with multi-sport cushioned acrylic surface,
                            tournament-spec LED night illumination, automated Spinshot ball machines, and resident league
                            reservations.
                          </p>
    
                          <div class="flex flex-wrap gap-2 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Tournament LEDs</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Ball Machine Loan</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">App Reservation</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Skyline Backdrop</span>
                          </div>
                        </div>
                      </div>
                    }
                  </div>
                </div>
              }
    
              <!-- CATEGORY 2: WORK & PRODUCTIVITY -->
              @if (showWork()) {
                <div class="space-y-6">
                  <div class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center">
                    <div class="flex items-center gap-3">
                      <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                        <lucide-icon [img]="Laptop" class="size-5" />
                      </div>
                      <div>
                        <h3 class="text-foreground text-xl font-bold tracking-tight">Work & Productivity</h3>
                        <p class="text-muted-foreground text-xs sm:text-sm">
                          Executive business facilities, private acoustic pods, and ultra-high-speed fiber network.
                        </p>
                      </div>
                    </div>
                    <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs font-medium">
                      3 Featured Amenities
                    </span>
                  </div>
    
                  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
                    @if (matchesQuery('Resident Co-Working Lounge with Private Pods workspace zoom office')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="Laptop" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">Quiet Focus</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Level 2 Mezzanine
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">
                              Resident Co-Working Lounge with Private Pods
                            </h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            Spacious communal workspace with Herman Miller ergonomic seating, acoustic focus booths, private
                            soundproof Zoom phone pods, and complimentary artisan espresso and nitro cold brew bar.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Soundproof Pods</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Herman Miller Chairs</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Espresso Bar</span>
                          </div>
                        </div>
                      </div>
                    }
    
                    @if (matchesQuery('Executive Conference Room with AV boardroom meeting video presentation')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="Video" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">App Booking</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Level 2 North Wing
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">
                              Executive Conference Room with AV
                            </h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            16-person board room equipped with 85-inch 4K HDR video conferencing displays, spatial
                            beamforming microphone arrays, digital whiteboard, and integrated presentation hub.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">16-Seat Capacity</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">4K Video Suite</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Digital Whiteboard</span>
                          </div>
                        </div>
                      </div>
                    }
    
                    @if (matchesQuery('High-Speed 10 Gbps Fiber WiFi throughout wifi internet mesh')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md sm:col-span-2 lg:col-span-1"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="Wifi" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">Ultra-Fast</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Building-wide Coverage
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">
                              High-Speed 10 Gbps Fiber WiFi throughout
                            </h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            Enterprise-grade symmetrical 10 Gbps redundant fiber backbone with Wi-Fi 7 access points
                            delivering uninterrupted zero-drop roaming across all indoor lounges and outdoor terraces.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Wi-Fi 7 Mesh</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">10 Gbps Symmetrical</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Zero Roam Handoff</span>
                          </div>
                        </div>
                      </div>
                    }
                  </div>
                </div>
              }
    
              <!-- CATEGORY 3: CONVENIENCE & SERVICES -->
              @if (showServices()) {
                <div class="space-y-6">
                  <div class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center">
                    <div class="flex items-center gap-3">
                      <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                        <lucide-icon [img]="ConciergeBell" class="size-5" />
                      </div>
                      <div>
                        <h3 class="text-foreground text-xl font-bold tracking-tight">Convenience & Services</h3>
                        <p class="text-muted-foreground text-xs sm:text-sm">
                          White-glove hospitality, smart package handling, and premium garment care at your fingertips.
                        </p>
                      </div>
                    </div>
                    <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs font-medium">
                      3 Featured Amenities
                    </span>
                  </div>
    
                  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
                    @if (matchesQuery('24/7 Doorman & Concierge Desk doorman front desk hospitality security')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="ConciergeBell" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">24/7 Attended</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Main Lobby Entrance
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">24/7 Doorman & Concierge Desk</h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            Round-the-clock uniform concierge team providing white-glove resident hospitality, verified
                            visitor check-in, key holding, luggage assistance, taxi dispatch, and dining reservations.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">White-Glove Staff</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Key Holding</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Guest Escort</span>
                          </div>
                        </div>
                      </div>
                    }
    
                    @if (matchesQuery('Automated Package Lockers & Cold Storage Delivery parcel grocery refrigerated')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="Package" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">Smart Storage</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Lobby Concourse
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">
                              Automated Package Lockers & Cold Storage Delivery
                            </h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            Contactless Luxer One smart package room with digital PIN and barcode pickup alerts, secure
                            oversized parcel rooms, and commercial-grade refrigerated storage for fresh grocery deliveries.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Cold Grocery Lockers</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">24/7 App QR Pickup</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Oversized Item Vault</span>
                          </div>
                        </div>
                      </div>
                    }
    
                    @if (matchesQuery('Dry Cleaning Valet & Laundry Services garment tailoring wash')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md sm:col-span-2 lg:col-span-1"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="Shirt" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">Valet Care</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Service Concourse B1
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">
                              Dry Cleaning Valet & Laundry Services
                            </h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            On-demand eco-friendly dry cleaning drop-off lockers with 24-hour turnaround, garment pressing,
                            shoe shining, tailored alterations, and in-locker contactless delivery return.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">24h Turnaround</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Eco-Friendly Cleaning</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Tailored Alterations</span>
                          </div>
                        </div>
                      </div>
                    }
                  </div>
                </div>
              }
    
              <!-- CATEGORY 4: PARKING & PET CARE -->
              @if (showParking()) {
                <div class="space-y-6">
                  <div class="flex flex-col items-start justify-between gap-2 sm:flex-row sm:items-center">
                    <div class="flex items-center gap-3">
                      <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                        <lucide-icon [img]="Car" class="size-5" />
                      </div>
                      <div>
                        <h3 class="text-foreground text-xl font-bold tracking-tight">Parking & Pet Care</h3>
                        <p class="text-muted-foreground text-xs sm:text-sm">
                          Subterranean EV charging bays and private rooftop pet facilities for four-legged residents.
                        </p>
                      </div>
                    </div>
                    <span ui-badge variant="outline" class="border-border text-muted-foreground text-xs font-medium">
                      2 Featured Amenities
                    </span>
                  </div>
    
                  <div class="grid grid-cols-1 gap-6 md:grid-cols-2">
                    @if (matchesQuery('Secure Underground Parking with EV Chargers electric vehicle garage stall')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="Car" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">EV Ready</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Levels P1 – P3 Subterranean
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">
                              Secure Underground Parking with EV Chargers
                            </h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            Gated subterranean multi-level garage featuring automated license plate recognition entry gates,
                            assigned private stalls, 48 universal Level 2 EV charging stations, and dedicated bike storage.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">48 EV Charging Ports</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">LPR Auto-Gate</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Covered Bike Vault</span>
                          </div>
                        </div>
                      </div>
                    }
    
                    @if (matchesQuery('Private Rooftop Dog Park & Pet Spa Station canine grooming wash animal')) {
                      <div
                        ui-card
                        class="border-border bg-card hover:border-primary/30 flex flex-col justify-between shadow-xs transition-colors duration-300 hover:shadow-md"
                      >
                        <div ui-card-header class="space-y-3 p-6 pb-2">
                          <div class="flex items-center justify-between">
                            <div class="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
                              <lucide-icon [img]="PawPrint" class="size-5" />
                            </div>
                            <span ui-badge variant="secondary" class="text-xs font-medium">Pet Friendly</span>
                          </div>
                          <div class="space-y-1">
                            <div class="text-muted-foreground flex items-center gap-1 text-xs">
                              <lucide-icon [img]="MapPin" class="text-primary size-3" />
                              Level 44 Sky Deck
                            </div>
                            <h4 ui-card-title class="text-foreground text-lg font-bold">
                              Private Rooftop Dog Park & Pet Spa Station
                            </h4>
                          </div>
                        </div>
    
                        <div ui-card-content class="space-y-4 p-6 pt-2">
                          <p ui-card-description class="text-muted-foreground text-sm leading-relaxed">
                            Enclosed rooftop canine park with anti-microbial synthetic turf and agility obstacles, paired
                            with an indoor heated pet wash spa featuring stainless steel wash tubs and pro blow dry station.
                          </p>
    
                          <div class="flex flex-wrap gap-1.5 pt-2">
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Anti-Microbial Turf</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Heated Wash Tubs</span>
                            <span ui-badge variant="outline" class="bg-muted/20 text-xs">Pro Blow Dryers</span>
                          </div>
                        </div>
                      </div>
                    }
                  </div>
                </div>
              }
            </div>
    
            <!-- Bottom Callout Card: Resident Keycard App & Tour Booking -->
            <div ui-card class="bg-muted/30 border-border mt-16 overflow-hidden border shadow-xs">
              <div
                ui-card-content
                class="flex flex-col items-start justify-between gap-6 p-6 sm:p-8 md:flex-row md:items-center"
              >
                <div class="flex items-start gap-4">
                  <div class="bg-primary/10 text-primary flex size-12 shrink-0 items-center justify-center rounded-xl">
                    <lucide-icon [img]="ShieldCheck" class="size-6" />
                  </div>
                  <div class="space-y-1">
                    <h4 class="text-foreground text-base font-semibold sm:text-lg">
                      Keyless Resident Access & Mobile App Integration
                    </h4>
                    <p class="text-muted-foreground max-w-2xl text-xs sm:text-sm">
                      All 24 building amenities are digitally accessible via NFC mobile key, offering instant cabana and
                      conference room reservations, real-time fitness occupancy, and automated guest passes.
                    </p>
                  </div>
                </div>
    
                <div class="flex flex-wrap items-center gap-3">
                  <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs">
                    <lucide-icon [img]="FileText" class="size-3.5" />
                    Amenity Guide PDF
                  </button>
                  <button ui-button size="sm" class="gap-1.5 text-xs">
                    <lucide-icon [img]="Calendar" class="size-3.5" />
                    Schedule a Tour
                    <lucide-icon [img]="ArrowRight" class="size-3.5" />
                  </button>
                </div>
              </div>
            </div>
          </div>
        </section>
      `,
    })
    export class UiPropertyAmenitiesGridComponent {
      protected readonly ArrowRight = ArrowRight
      protected readonly Building2 = Building2
      protected readonly Calendar = Calendar
      protected readonly Car = Car
      protected readonly Check = Check
      protected readonly CircleDot = CircleDot
      protected readonly Clock = Clock
      protected readonly Compass = Compass
      protected readonly ConciergeBell = ConciergeBell
      protected readonly Dumbbell = Dumbbell
      protected readonly FileText = FileText
      protected readonly Flame = Flame
      protected readonly HeartPulse = HeartPulse
      protected readonly Laptop = Laptop
      protected readonly MapPin = MapPin
      protected readonly Package = Package
      protected readonly PawPrint = PawPrint
      protected readonly Search = Search
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Shirt = Shirt
      protected readonly Video = Video
      protected readonly Wifi = Wifi
      protected readonly X = X
      protected readonly cn = cn
    
      protected readonly categoryList = categories
    
      @Input() title = 'World-Class Amenities & Services'
      @Input() subtitle = 'Designed for wellness, productivity, and quiet luxury living.'
      @Input() badge = 'Building Features & Lifestyle'
      @Input('class') className?: string
    
      readonly activeCategory = signal<CategoryId>('all')
      readonly searchQuery = signal('')
    
      readonly isSearching = computed(() => this.searchQuery().trim().length > 0)
    
      get rootClass(): string {
        return cn('bg-background text-foreground w-full py-12 sm:py-16 lg:py-20', this.className)
      }
    
      pillCountClass(id: CategoryId): string {
        return cn(
          'py-0.2 ml-1 rounded-full px-1.5 font-mono text-xs',
          this.activeCategory() === id
            ? 'bg-primary-foreground/20 text-primary-foreground'
            : 'bg-muted text-muted-foreground',
        )
      }
    
      matchesQuery(text: string): boolean {
        if (!this.isSearching()) return true
        return text.toLowerCase().includes(this.searchQuery().toLowerCase().trim())
      }
    
      readonly showWellness = computed(() => {
        if (this.activeCategory() !== 'all' && this.activeCategory() !== 'wellness') return false
        if (!this.isSearching()) return true
        return (
          this.matchesQuery('Heated Rooftop Infinity Pool') ||
          this.matchesQuery('Equinox-style Fitness Center & Yoga Studio') ||
          this.matchesQuery('Nordic Sauna & Cold Plunge Spa') ||
          this.matchesQuery('Private Tennis & Pickleball Court') ||
          this.matchesQuery('wellness recreation pool gym sauna')
        )
      })
    
      readonly showWork = computed(() => {
        if (this.activeCategory() !== 'all' && this.activeCategory() !== 'work') return false
        if (!this.isSearching()) return true
        return (
          this.matchesQuery('Resident Co-Working Lounge with Private Pods') ||
          this.matchesQuery('Executive Conference Room with AV') ||
          this.matchesQuery('High-Speed 10 Gbps Fiber WiFi throughout') ||
          this.matchesQuery('work productivity office wifi conference')
        )
      })
    
      readonly showServices = computed(() => {
        if (this.activeCategory() !== 'all' && this.activeCategory() !== 'services') return false
        if (!this.isSearching()) return true
        return (
          this.matchesQuery('24/7 Doorman & Concierge Desk') ||
          this.matchesQuery('Automated Package Lockers & Cold Storage Delivery') ||
          this.matchesQuery('Dry Cleaning Valet & Laundry Services') ||
          this.matchesQuery('convenience services concierge doorman package laundry')
        )
      })
    
      readonly showParking = computed(() => {
        if (this.activeCategory() !== 'all' && this.activeCategory() !== 'parking') return false
        if (!this.isSearching()) return true
        return (
          this.matchesQuery('Secure Underground Parking with EV Chargers') ||
          this.matchesQuery('Private Rooftop Dog Park & Pet Spa Station') ||
          this.matchesQuery('parking ev charger pet care dog park')
        )
      })
    }
    
  • src/app/components/blocks/property-amenities-grid/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/property-amenities-grid.json