Filter for commercial properties
This commit is contained in:
parent
7d336f975d
commit
c62af8746f
|
|
@ -41,5 +41,6 @@
|
||||||
|
|
||||||
<app-message-container></app-message-container>
|
<app-message-container></app-message-container>
|
||||||
<app-search-modal></app-search-modal>
|
<app-search-modal></app-search-modal>
|
||||||
|
<app-search-modal-commercial></app-search-modal-commercial>
|
||||||
<app-confirmation></app-confirmation>
|
<app-confirmation></app-confirmation>
|
||||||
<app-email></app-email>
|
<app-email></app-email>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { EMailComponent } from './components/email/email.component';
|
||||||
import { FooterComponent } from './components/footer/footer.component';
|
import { FooterComponent } from './components/footer/footer.component';
|
||||||
import { HeaderComponent } from './components/header/header.component';
|
import { HeaderComponent } from './components/header/header.component';
|
||||||
import { MessageContainerComponent } from './components/message/message-container.component';
|
import { MessageContainerComponent } from './components/message/message-container.component';
|
||||||
|
import { SearchModalCommercialComponent } from './components/search-modal/search-modal-commercial.component';
|
||||||
import { SearchModalComponent } from './components/search-modal/search-modal.component';
|
import { SearchModalComponent } from './components/search-modal/search-modal.component';
|
||||||
import { AuditService } from './services/audit.service';
|
import { AuditService } from './services/audit.service';
|
||||||
import { GeoService } from './services/geo.service';
|
import { GeoService } from './services/geo.service';
|
||||||
|
|
@ -19,7 +20,7 @@ import { UserService } from './services/user.service';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent, EMailComponent],
|
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, SearchModalCommercialComponent, ConfirmationComponent, EMailComponent],
|
||||||
providers: [],
|
providers: [],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// 1. Shared Service (modal.service.ts)
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, ModalResult, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
import { BusinessListingCriteria, CommercialPropertyListingCriteria, ModalResult, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
|
|
@ -7,16 +6,16 @@ import { BusinessListingCriteria, CommercialPropertyListingCriteria, ModalResult
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ModalService {
|
export class ModalService {
|
||||||
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
|
private modalVisibleSubject = new BehaviorSubject<{ visible: boolean; type?: string }>({ visible: false });
|
||||||
private messageSubject = new BehaviorSubject<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria>(null);
|
private messageSubject = new BehaviorSubject<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria>(null);
|
||||||
private resolvePromise!: (value: ModalResult) => void;
|
private resolvePromise!: (value: ModalResult) => void;
|
||||||
|
|
||||||
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
|
modalVisible$: Observable<{ visible: boolean; type?: string }> = this.modalVisibleSubject.asObservable();
|
||||||
message$: Observable<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria> = this.messageSubject.asObservable();
|
message$: Observable<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria> = this.messageSubject.asObservable();
|
||||||
|
|
||||||
showModal(message: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): Promise<ModalResult> {
|
showModal(message: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): Promise<ModalResult> {
|
||||||
this.messageSubject.next(message);
|
this.messageSubject.next(message);
|
||||||
this.modalVisibleSubject.next(true);
|
this.modalVisibleSubject.next({ visible: true, type: message.criteriaType });
|
||||||
return new Promise<ModalResult>(resolve => {
|
return new Promise<ModalResult>(resolve => {
|
||||||
this.resolvePromise = resolve;
|
this.resolvePromise = resolve;
|
||||||
});
|
});
|
||||||
|
|
@ -28,12 +27,12 @@ export class ModalService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
accept(): void {
|
accept(): void {
|
||||||
this.modalVisibleSubject.next(false);
|
this.modalVisibleSubject.next({ visible: false });
|
||||||
this.resolvePromise({ accepted: true });
|
this.resolvePromise({ accepted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
reject(backupCriteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): void {
|
reject(backupCriteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): void {
|
||||||
this.modalVisibleSubject.next(false);
|
this.modalVisibleSubject.next({ visible: false });
|
||||||
this.resolvePromise({ accepted: false, criteria: backupCriteria });
|
this.resolvePromise({ accepted: false, criteria: backupCriteria });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
<div
|
||||||
|
*ngIf="isModal && (modalService.modalVisible$ | async)?.visible && (modalService.modalVisible$ | async)?.type === 'commercialPropertyListings'"
|
||||||
|
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
|
<div class="relative w-full h-screen max-h-screen">
|
||||||
|
<div class="relative bg-white rounded-lg shadow h-full">
|
||||||
|
<div class="flex items-start justify-between p-4 border-b rounded-t bg-blue-600">
|
||||||
|
<h3 class="text-xl font-semibold text-white p-2 rounded">Commercial Property Listing Search</h3>
|
||||||
|
<button (click)="closeAndSearch()" type="button" class="text-white bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||||
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Close Modal</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="p-6 space-y-6">
|
||||||
|
<div class="flex space-x-4 mb-4">
|
||||||
|
<button class="text-blue-600 font-medium border-b-2 border-blue-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||||
|
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-blue-500" (click)="clearFilter()"></i>
|
||||||
|
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||||
|
Clear all Filter
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Display active filters as tags -->
|
||||||
|
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||||
|
<span *ngIf="criteria.state" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="criteria.city" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="criteria.types.length" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="criteria.title" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||||
|
<div class="grid grid-cols-1 gap-6">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
|
||||||
|
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="criteria.city">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" (ngModelChange)="onCriteriaChange()" value="exact" />
|
||||||
|
<span class="ml-2">Exact City</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" (ngModelChange)="onCriteriaChange()" value="radius" />
|
||||||
|
<span class="ml-2">Radius Search</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Select Radius (in miles)</label>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-center border border-gray-200 hover:bg-gray-500 hover:text-white"
|
||||||
|
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-gray-900 bg-white'"
|
||||||
|
(click)="criteria.radius = radius"
|
||||||
|
>
|
||||||
|
{{ radius }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<app-validated-price name="price-from" (ngModelChange)="debouncedSearch()" [(ngModel)]="criteria.minPrice" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||||
|
<span>-</span>
|
||||||
|
<app-validated-price name="price-to" (ngModelChange)="debouncedSearch()" [(ngModel)]="criteria.maxPrice" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="title" class="block mb-2 text-sm font-medium text-gray-900">Title / Description (Free Search)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
[(ngModel)]="criteria.title"
|
||||||
|
(ngModelChange)="debouncedSearch()"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
|
placeholder="e.g. Office Space"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Category</label>
|
||||||
|
<ng-select
|
||||||
|
class="custom"
|
||||||
|
[items]="selectOptions.typesOfCommercialProperty"
|
||||||
|
bindLabel="name"
|
||||||
|
bindValue="value"
|
||||||
|
[ngModel]="criteria.types"
|
||||||
|
(ngModelChange)="onCategoryChange($event)"
|
||||||
|
[multiple]="true"
|
||||||
|
[closeOnSelect]="true"
|
||||||
|
placeholder="Select categories"
|
||||||
|
></ng-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!isModal" class="space-y-6 pb-10">
|
||||||
|
<div class="flex space-x-4 mb-4">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||||
|
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-blue-500" (click)="clearFilter()"></i>
|
||||||
|
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||||
|
Clear all Filter
|
||||||
|
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Display active filters as tags -->
|
||||||
|
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||||
|
<span *ngIf="criteria.state" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="criteria.city" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="criteria.types.length" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="criteria.title" class="bg-gray-200 text-gray-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||||
|
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
|
||||||
|
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="criteria.city">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" (ngModelChange)="onCriteriaChange()" value="exact" />
|
||||||
|
<span class="ml-2">Exact City</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" (ngModelChange)="onCriteriaChange()" value="radius" />
|
||||||
|
<span class="ml-2">Radius Search</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Select Radius (in miles)</label>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-center border border-gray-200 hover:bg-gray-500 hover:text-white"
|
||||||
|
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-gray-900 bg-white'"
|
||||||
|
(click)="setRadius(radius)"
|
||||||
|
>
|
||||||
|
{{ radius }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Category</label>
|
||||||
|
<ng-select
|
||||||
|
class="custom"
|
||||||
|
[items]="selectOptions.typesOfCommercialProperty"
|
||||||
|
bindLabel="name"
|
||||||
|
bindValue="value"
|
||||||
|
[ngModel]="criteria.types"
|
||||||
|
(ngModelChange)="onCategoryChange($event)"
|
||||||
|
[multiple]="true"
|
||||||
|
[closeOnSelect]="true"
|
||||||
|
placeholder="Select categories"
|
||||||
|
></ng-select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<app-validated-price name="price-from" (ngModelChange)="debouncedSearch()" [(ngModel)]="criteria.minPrice" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||||
|
<span>-</span>
|
||||||
|
<app-validated-price name="price-to" (ngModelChange)="debouncedSearch()" [(ngModel)]="criteria.maxPrice" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="title" class="block mb-2 text-sm font-medium text-gray-900">Title / Description (Free Search)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
[(ngModel)]="criteria.title"
|
||||||
|
(ngModelChange)="debouncedSearch()"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
|
placeholder="e.g. Office Space"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { NgSelectModule } from '@ng-select/ng-select';
|
||||||
|
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||||
|
import { catchError, concat, debounceTime, distinctUntilChanged, map, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs';
|
||||||
|
import { CommercialPropertyListingCriteria, CountyResult, GeoResult } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
|
import { CriteriaChangeService } from '../../services/criteria-change.service';
|
||||||
|
import { GeoService } from '../../services/geo.service';
|
||||||
|
import { ListingsService } from '../../services/listings.service';
|
||||||
|
import { SearchService } from '../../services/search.service';
|
||||||
|
import { SelectOptionsService } from '../../services/select-options.service';
|
||||||
|
import { UserService } from '../../services/user.service';
|
||||||
|
import { getCriteriaStateObject, resetCommercialPropertyListingCriteria } from '../../utils/utils';
|
||||||
|
import { ValidatedCityComponent } from '../validated-city/validated-city.component';
|
||||||
|
import { ValidatedPriceComponent } from '../validated-price/validated-price.component';
|
||||||
|
import { ModalService } from './modal.service';
|
||||||
|
|
||||||
|
@UntilDestroy()
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search-modal-commercial',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, FormsModule, NgSelectModule, ValidatedCityComponent, ValidatedPriceComponent],
|
||||||
|
templateUrl: './search-modal-commercial.component.html',
|
||||||
|
styleUrls: ['./search-modal.component.scss'],
|
||||||
|
})
|
||||||
|
export class SearchModalCommercialComponent {
|
||||||
|
@Input()
|
||||||
|
isModal: boolean = true;
|
||||||
|
// cities$: Observable<GeoResult[]>;
|
||||||
|
counties$: Observable<CountyResult[]>;
|
||||||
|
// cityLoading = false;
|
||||||
|
countyLoading = false;
|
||||||
|
// cityInput$ = new Subject<string>();
|
||||||
|
countyInput$ = new Subject<string>();
|
||||||
|
private criteriaChangeSubscription: Subscription;
|
||||||
|
public criteria: CommercialPropertyListingCriteria;
|
||||||
|
private debounceTimeout: any;
|
||||||
|
public backupCriteria: CommercialPropertyListingCriteria = getCriteriaStateObject('businessListings');
|
||||||
|
numberOfResults$: Observable<number>;
|
||||||
|
cancelDisable = false;
|
||||||
|
constructor(
|
||||||
|
public selectOptions: SelectOptionsService,
|
||||||
|
public modalService: ModalService,
|
||||||
|
private geoService: GeoService,
|
||||||
|
private criteriaChangeService: CriteriaChangeService,
|
||||||
|
private listingService: ListingsService,
|
||||||
|
private userService: UserService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
) {}
|
||||||
|
// Define property type options
|
||||||
|
|
||||||
|
selectedPropertyType: string | null = null;
|
||||||
|
selectedPropertyTypeName: string | null = null;
|
||||||
|
ngOnInit() {
|
||||||
|
this.setupCriteriaChangeListener();
|
||||||
|
|
||||||
|
this.modalService.message$.pipe(untilDestroyed(this)).subscribe(msg => {
|
||||||
|
this.criteria = msg as CommercialPropertyListingCriteria;
|
||||||
|
this.backupCriteria = JSON.parse(JSON.stringify(msg));
|
||||||
|
this.setTotalNumberOfResults();
|
||||||
|
});
|
||||||
|
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
|
||||||
|
if (val.visible) {
|
||||||
|
this.criteria.page = 1;
|
||||||
|
this.criteria.start = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// this.loadCities();
|
||||||
|
this.loadCounties();
|
||||||
|
this.modalService.sendCriteria(this.criteria);
|
||||||
|
}
|
||||||
|
hasActiveFilters(): boolean {
|
||||||
|
return !!(this.criteria.state || this.criteria.city || this.criteria.minPrice || this.criteria.maxPrice || this.criteria.types.length || this.criteria.title);
|
||||||
|
}
|
||||||
|
removeFilter(filterType: string) {
|
||||||
|
switch (filterType) {
|
||||||
|
case 'state':
|
||||||
|
this.criteria.state = null;
|
||||||
|
this.setCity(null);
|
||||||
|
break;
|
||||||
|
case 'city':
|
||||||
|
this.criteria.city = null;
|
||||||
|
this.criteria.radius = null;
|
||||||
|
this.criteria.searchType = 'exact';
|
||||||
|
break;
|
||||||
|
case 'price':
|
||||||
|
this.criteria.minPrice = null;
|
||||||
|
this.criteria.maxPrice = null;
|
||||||
|
break;
|
||||||
|
case 'types':
|
||||||
|
this.criteria.types = [];
|
||||||
|
break;
|
||||||
|
case 'title':
|
||||||
|
this.criteria.title = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}
|
||||||
|
clearFilter() {
|
||||||
|
resetCommercialPropertyListingCriteria(this.criteria);
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}
|
||||||
|
// Handle category change
|
||||||
|
onCategoryChange(event: any[]) {
|
||||||
|
this.criteria.types = event;
|
||||||
|
this.onCriteriaChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryClicked(checked: boolean, value: string) {
|
||||||
|
if (checked) {
|
||||||
|
this.criteria.types.push(value);
|
||||||
|
} else {
|
||||||
|
const index = this.criteria.types.findIndex(t => t === value);
|
||||||
|
if (index > -1) {
|
||||||
|
this.criteria.types.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}
|
||||||
|
private loadCounties() {
|
||||||
|
this.counties$ = concat(
|
||||||
|
of([]), // default items
|
||||||
|
this.countyInput$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
tap(() => (this.countyLoading = true)),
|
||||||
|
switchMap(term =>
|
||||||
|
this.geoService.findCountiesStartingWith(term).pipe(
|
||||||
|
catchError(() => of([])), // empty list on error
|
||||||
|
map(counties => counties.map(county => county.name)), // transform the list of objects to a list of city names
|
||||||
|
tap(() => (this.countyLoading = false)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
onCriteriaChange() {
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}
|
||||||
|
setCity(city) {
|
||||||
|
if (city) {
|
||||||
|
this.criteria.city = city;
|
||||||
|
this.criteria.state = city.state;
|
||||||
|
} else {
|
||||||
|
this.criteria.city = null;
|
||||||
|
this.criteria.radius = null;
|
||||||
|
this.criteria.searchType = 'exact';
|
||||||
|
}
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}
|
||||||
|
setState(state: string) {
|
||||||
|
if (state) {
|
||||||
|
this.criteria.state = state;
|
||||||
|
} else {
|
||||||
|
this.criteria.state = null;
|
||||||
|
this.setCity(null);
|
||||||
|
}
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}
|
||||||
|
setRadius(radius: number) {
|
||||||
|
this.criteria.radius = radius;
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}
|
||||||
|
private setupCriteriaChangeListener() {
|
||||||
|
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => {
|
||||||
|
this.setTotalNumberOfResults();
|
||||||
|
this.cancelDisable = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
trackByFn(item: GeoResult) {
|
||||||
|
return item.id;
|
||||||
|
}
|
||||||
|
search() {
|
||||||
|
console.log('Search criteria:', this.criteria);
|
||||||
|
}
|
||||||
|
getCounties() {
|
||||||
|
this.geoService.findCountiesStartingWith('');
|
||||||
|
}
|
||||||
|
closeModal() {
|
||||||
|
console.log('Closing modal');
|
||||||
|
}
|
||||||
|
closeAndSearch() {
|
||||||
|
this.modalService.accept();
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
setTotalNumberOfResults() {
|
||||||
|
if (this.criteria) {
|
||||||
|
console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
|
||||||
|
if (this.criteria.criteriaType === 'commercialPropertyListings') {
|
||||||
|
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria, 'commercialProperty');
|
||||||
|
} else {
|
||||||
|
this.numberOfResults$ = of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.modalService.reject(this.backupCriteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
debouncedSearch() {
|
||||||
|
clearTimeout(this.debounceTimeout);
|
||||||
|
this.debounceTimeout = setTimeout(() => {
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
<div *ngIf="isModal && (modalService.modalVisible$ | async)" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50">
|
<div
|
||||||
|
*ngIf="isModal && (modalService.modalVisible$ | async)?.visible && (modalService.modalVisible$ | async)?.type === 'businessListings'"
|
||||||
|
class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
<div class="relative w-full max-h-full">
|
<div class="relative w-full max-h-full">
|
||||||
<div class="relative bg-white rounded-lg shadow">
|
<div class="relative bg-white rounded-lg shadow">
|
||||||
<div class="flex items-start justify-between p-4 border-b rounded-t bg-blue-600">
|
<div class="flex items-start justify-between p-4 border-b rounded-t bg-blue-600">
|
||||||
@if(criteria.criteriaType==='businessListings') {
|
|
||||||
<h3 class="text-xl font-semibold text-white p-2 rounded">Business Listing Search</h3>
|
<h3 class="text-xl font-semibold text-white p-2 rounded">Business Listing Search</h3>
|
||||||
} @else if (criteria.criteriaType==='commercialPropertyListings') {
|
|
||||||
<h3 class="text-xl font-semibold text-gray-900">Property Listing Search</h3>
|
|
||||||
} @else {
|
|
||||||
<h3 class="text-xl font-semibold text-gray-900">Professional Listing Search</h3>
|
|
||||||
}
|
|
||||||
<button (click)="closeAndSearch()" type="button" class="text-white bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
<button (click)="closeAndSearch()" type="button" class="text-white bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export class SearchModalComponent {
|
||||||
this.setTotalNumberOfResults();
|
this.setTotalNumberOfResults();
|
||||||
});
|
});
|
||||||
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
|
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
|
||||||
if (val) {
|
if (val.visible) {
|
||||||
this.criteria.page = 1;
|
this.criteria.page = 1;
|
||||||
this.criteria.start = 0;
|
this.criteria.start = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,107 @@
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="flex flex-col md:flex-row">
|
||||||
@if(listings?.length>0){
|
<!-- Filter Panel for Desktop -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="hidden md:block w-full md:w-1/4 h-full bg-white shadow-lg p-6 overflow-y-auto z-10">
|
||||||
@for (listing of listings; track listing.id) {
|
<app-search-modal-commercial [isModal]="false"></app-search-modal-commercial>
|
||||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden flex flex-col h-full">
|
</div>
|
||||||
@if (listing.imageOrder?.length>0){
|
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}" alt="Image" class="w-full h-48 object-cover" />
|
<!-- Main Content -->
|
||||||
} @else {
|
<div class="w-full p-4">
|
||||||
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="w-full h-48 object-cover" />
|
<div class="container mx-auto">
|
||||||
}
|
@if(listings?.length > 0) {
|
||||||
<div class="p-4 flex flex-col flex-grow">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<span [class]="selectOptions.getTextColorTypeOfCommercial(listing.type)" class="text-sm font-semibold"
|
@for (listing of listings; track listing.id) {
|
||||||
><i [class]="selectOptions.getIconAndTextColorTypeOfCommercials(listing.type)" class="mr-1"></i> {{ selectOptions.getCommercialProperty(listing.type) }}</span
|
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden flex flex-col h-full">
|
||||||
>
|
@if (listing.imageOrder?.length>0){
|
||||||
<div class="flex items-center justify-between my-2">
|
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}" alt="Image" class="w-full h-48 object-cover" />
|
||||||
<span class="bg-gray-200 text-gray-700 text-xs font-semibold px-2 py-1 rounded">{{ selectOptions.getState(listing.location.state) }}</span>
|
} @else {
|
||||||
<p class="text-sm text-gray-600 mb-4">
|
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="w-full h-48 object-cover" />
|
||||||
<strong>{{ getDaysListed(listing) }} days listed</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-semibold mb-2">
|
|
||||||
{{ listing.title }}
|
|
||||||
@if(listing.draft){
|
|
||||||
<span class="bg-red-100 text-red-800 text-sm font-medium me-2 ml-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">Draft</span>
|
|
||||||
}
|
}
|
||||||
</h3>
|
<div class="p-4 flex flex-col flex-grow">
|
||||||
<p class="text-gray-600 mb-2">{{ listing.location.name ? listing.location.name : listing.location.county }}</p>
|
<span [class]="selectOptions.getTextColorTypeOfCommercial(listing.type)" class="text-sm font-semibold"
|
||||||
<p class="text-xl font-bold mb-4">{{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
|
><i [class]="selectOptions.getIconAndTextColorTypeOfCommercials(listing.type)" class="mr-1"></i> {{ selectOptions.getCommercialProperty(listing.type) }}</span
|
||||||
<div class="flex-grow"></div>
|
>
|
||||||
<button [routerLink]="['/details-commercial-property-listing', listing.id]" class="bg-green-500 text-white px-4 py-2 rounded-full w-full hover:bg-green-600 transition duration-300 mt-auto">
|
<div class="flex items-center justify-between my-2">
|
||||||
View Full Listing <i class="fas fa-arrow-right ml-1"></i>
|
<span class="bg-gray-200 text-gray-700 text-xs font-semibold px-2 py-1 rounded">{{ selectOptions.getState(listing.location.state) }}</span>
|
||||||
</button>
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
|
<strong>{{ getDaysListed(listing) }} days listed</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold mb-2">
|
||||||
|
{{ listing.title }}
|
||||||
|
@if(listing.draft){
|
||||||
|
<span class="bg-red-100 text-red-800 text-sm font-medium me-2 ml-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">Draft</span>
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-600 mb-2">{{ listing.location.name ? listing.location.name : listing.location.county }}</p>
|
||||||
|
<p class="text-xl font-bold mb-4">{{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
|
||||||
|
<div class="flex-grow"></div>
|
||||||
|
<button [routerLink]="['/details-commercial-property-listing', listing.id]" class="bg-green-500 text-white px-4 py-2 rounded-full w-full hover:bg-green-600 transition duration-300 mt-auto">
|
||||||
|
View Full Listing <i class="fas fa-arrow-right ml-1"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
} @else if (listings?.length === 0){
|
||||||
|
<div class="w-full flex items-center flex-wrap justify-center gap-10">
|
||||||
|
<div class="grid gap-4 w-60">
|
||||||
|
<svg class="mx-auto" xmlns="http://www.w3.org/2000/svg" width="154" height="161" viewBox="0 0 154 161" fill="none">
|
||||||
|
<path
|
||||||
|
d="M0.0616455 84.4268C0.0616455 42.0213 34.435 7.83765 76.6507 7.83765C118.803 7.83765 153.224 42.0055 153.224 84.4268C153.224 102.42 147.026 118.974 136.622 132.034C122.282 150.138 100.367 161 76.6507 161C52.7759 161 30.9882 150.059 16.6633 132.034C6.25961 118.974 0.0616455 102.42 0.0616455 84.4268Z"
|
||||||
|
fill="#EEF2FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M96.8189 0.632498L96.8189 0.632384L96.8083 0.630954C96.2034 0.549581 95.5931 0.5 94.9787 0.5H29.338C22.7112 0.5 17.3394 5.84455 17.3394 12.4473V142.715C17.3394 149.318 22.7112 154.662 29.338 154.662H123.948C130.591 154.662 135.946 149.317 135.946 142.715V38.9309C135.946 38.0244 135.847 37.1334 135.648 36.2586L135.648 36.2584C135.117 33.9309 133.874 31.7686 132.066 30.1333C132.066 30.1331 132.065 30.1329 132.065 30.1327L103.068 3.65203C103.068 3.6519 103.067 3.65177 103.067 3.65164C101.311 2.03526 99.1396 0.995552 96.8189 0.632498Z"
|
||||||
|
fill="white"
|
||||||
|
stroke="#E5E7EB"
|
||||||
|
/>
|
||||||
|
<ellipse cx="80.0618" cy="81" rx="28.0342" ry="28.0342" fill="#EEF2FF" />
|
||||||
|
<path
|
||||||
|
d="M99.2393 61.3061L99.2391 61.3058C88.498 50.5808 71.1092 50.5804 60.3835 61.3061C49.6423 72.0316 49.6422 89.4361 60.3832 100.162C71.109 110.903 88.4982 110.903 99.2393 100.162C109.965 89.4363 109.965 72.0317 99.2393 61.3061ZM105.863 54.6832C120.249 69.0695 120.249 92.3985 105.863 106.785C91.4605 121.171 68.1468 121.171 53.7446 106.785C39.3582 92.3987 39.3582 69.0693 53.7446 54.683C68.1468 40.2965 91.4605 40.2966 105.863 54.6832Z"
|
||||||
|
stroke="#E5E7EB"
|
||||||
|
/>
|
||||||
|
<path d="M110.782 119.267L102.016 110.492C104.888 108.267 107.476 105.651 109.564 102.955L118.329 111.729L110.782 119.267Z" stroke="#E5E7EB" />
|
||||||
|
<path
|
||||||
|
d="M139.122 125.781L139.122 125.78L123.313 109.988C123.313 109.987 123.313 109.987 123.312 109.986C121.996 108.653 119.849 108.657 118.521 109.985L118.871 110.335L118.521 109.985L109.047 119.459C107.731 120.775 107.735 122.918 109.044 124.247L109.047 124.249L124.858 140.06C128.789 143.992 135.191 143.992 139.122 140.06C143.069 136.113 143.069 129.728 139.122 125.781Z"
|
||||||
|
fill="#A5B4FC"
|
||||||
|
stroke="#818CF8"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M83.185 87.2285C82.5387 87.2285 82.0027 86.6926 82.0027 86.0305C82.0027 83.3821 77.9987 83.3821 77.9987 86.0305C77.9987 86.6926 77.4627 87.2285 76.8006 87.2285C76.1543 87.2285 75.6183 86.6926 75.6183 86.0305C75.6183 80.2294 84.3831 80.2451 84.3831 86.0305C84.3831 86.6926 83.8471 87.2285 83.185 87.2285Z"
|
||||||
|
fill="#4F46E5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.3528 77.0926H88.403C87.7409 77.0926 87.2049 76.5567 87.2049 75.8946C87.2049 75.2483 87.7409 74.7123 88.403 74.7123H93.3528C94.0149 74.7123 94.5509 75.2483 94.5509 75.8946C94.5509 76.5567 94.0149 77.0926 93.3528 77.0926Z"
|
||||||
|
fill="#4F46E5"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M71.5987 77.0925H66.6488C65.9867 77.0925 65.4507 76.5565 65.4507 75.8945C65.4507 75.2481 65.9867 74.7122 66.6488 74.7122H71.5987C72.245 74.7122 72.781 75.2481 72.781 75.8945C72.781 76.5565 72.245 77.0925 71.5987 77.0925Z"
|
||||||
|
fill="#4F46E5"
|
||||||
|
/>
|
||||||
|
<rect x="38.3522" y="21.5128" width="41.0256" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||||
|
<rect x="38.3522" y="133.65" width="54.7009" height="5.47009" rx="2.73504" fill="#A5B4FC" />
|
||||||
|
<rect x="38.3522" y="29.7179" width="13.6752" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
||||||
|
<circle cx="56.13" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||||
|
<circle cx="61.6001" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||||
|
<circle cx="67.0702" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-center text-black text-xl font-semibold leading-loose pb-2">There’s no listing here</h2>
|
||||||
|
<p class="text-center text-black text-base font-normal leading-relaxed pb-4">Try changing your filters to <br />see listings</p>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button (click)="clearAllFilters()" class="w-full px-3 py-2 rounded-full border border-gray-300 text-gray-900 text-xs font-semibold leading-4">Clear Filter</button>
|
||||||
|
<button (click)="openFilterModal()" class="w-full px-3 py-2 bg-indigo-600 hover:bg-indigo-700 transition-all duration-500 rounded-full text-white text-xs font-semibold leading-4">Change Filter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if(pageCount > 1) {
|
||||||
|
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else if (listings?.length===0){
|
|
||||||
<div class="w-full flex items-center flex-wrap justify-center gap-10">
|
<!-- Filter Button for Mobile -->
|
||||||
<div class="grid gap-4 w-60">
|
<button (click)="openFilterModal()" class="md:hidden fixed bottom-4 right-4 bg-blue-500 text-white p-3 rounded-full shadow-lg z-20"><i class="fas fa-filter"></i> Filter</button>
|
||||||
<svg class="mx-auto" xmlns="http://www.w3.org/2000/svg" width="154" height="161" viewBox="0 0 154 161" fill="none">
|
|
||||||
<path
|
|
||||||
d="M0.0616455 84.4268C0.0616455 42.0213 34.435 7.83765 76.6507 7.83765C118.803 7.83765 153.224 42.0055 153.224 84.4268C153.224 102.42 147.026 118.974 136.622 132.034C122.282 150.138 100.367 161 76.6507 161C52.7759 161 30.9882 150.059 16.6633 132.034C6.25961 118.974 0.0616455 102.42 0.0616455 84.4268Z"
|
|
||||||
fill="#EEF2FF"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M96.8189 0.632498L96.8189 0.632384L96.8083 0.630954C96.2034 0.549581 95.5931 0.5 94.9787 0.5H29.338C22.7112 0.5 17.3394 5.84455 17.3394 12.4473V142.715C17.3394 149.318 22.7112 154.662 29.338 154.662H123.948C130.591 154.662 135.946 149.317 135.946 142.715V38.9309C135.946 38.0244 135.847 37.1334 135.648 36.2586L135.648 36.2584C135.117 33.9309 133.874 31.7686 132.066 30.1333C132.066 30.1331 132.065 30.1329 132.065 30.1327L103.068 3.65203C103.068 3.6519 103.067 3.65177 103.067 3.65164C101.311 2.03526 99.1396 0.995552 96.8189 0.632498Z"
|
|
||||||
fill="white"
|
|
||||||
stroke="#E5E7EB"
|
|
||||||
/>
|
|
||||||
<ellipse cx="80.0618" cy="81" rx="28.0342" ry="28.0342" fill="#EEF2FF" />
|
|
||||||
<path
|
|
||||||
d="M99.2393 61.3061L99.2391 61.3058C88.498 50.5808 71.1092 50.5804 60.3835 61.3061C49.6423 72.0316 49.6422 89.4361 60.3832 100.162C71.109 110.903 88.4982 110.903 99.2393 100.162C109.965 89.4363 109.965 72.0317 99.2393 61.3061ZM105.863 54.6832C120.249 69.0695 120.249 92.3985 105.863 106.785C91.4605 121.171 68.1468 121.171 53.7446 106.785C39.3582 92.3987 39.3582 69.0693 53.7446 54.683C68.1468 40.2965 91.4605 40.2966 105.863 54.6832Z"
|
|
||||||
stroke="#E5E7EB"
|
|
||||||
/>
|
|
||||||
<path d="M110.782 119.267L102.016 110.492C104.888 108.267 107.476 105.651 109.564 102.955L118.329 111.729L110.782 119.267Z" stroke="#E5E7EB" />
|
|
||||||
<path
|
|
||||||
d="M139.122 125.781L139.122 125.78L123.313 109.988C123.313 109.987 123.313 109.987 123.312 109.986C121.996 108.653 119.849 108.657 118.521 109.985L118.871 110.335L118.521 109.985L109.047 119.459C107.731 120.775 107.735 122.918 109.044 124.247L109.047 124.249L124.858 140.06C128.789 143.992 135.191 143.992 139.122 140.06C143.069 136.113 143.069 129.728 139.122 125.781Z"
|
|
||||||
fill="#A5B4FC"
|
|
||||||
stroke="#818CF8"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M83.185 87.2285C82.5387 87.2285 82.0027 86.6926 82.0027 86.0305C82.0027 83.3821 77.9987 83.3821 77.9987 86.0305C77.9987 86.6926 77.4627 87.2285 76.8006 87.2285C76.1543 87.2285 75.6183 86.6926 75.6183 86.0305C75.6183 80.2294 84.3831 80.2451 84.3831 86.0305C84.3831 86.6926 83.8471 87.2285 83.185 87.2285Z"
|
|
||||||
fill="#4F46E5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M93.3528 77.0926H88.403C87.7409 77.0926 87.2049 76.5567 87.2049 75.8946C87.2049 75.2483 87.7409 74.7123 88.403 74.7123H93.3528C94.0149 74.7123 94.5509 75.2483 94.5509 75.8946C94.5509 76.5567 94.0149 77.0926 93.3528 77.0926Z"
|
|
||||||
fill="#4F46E5"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M71.5987 77.0925H66.6488C65.9867 77.0925 65.4507 76.5565 65.4507 75.8945C65.4507 75.2481 65.9867 74.7122 66.6488 74.7122H71.5987C72.245 74.7122 72.781 75.2481 72.781 75.8945C72.781 76.5565 72.245 77.0925 71.5987 77.0925Z"
|
|
||||||
fill="#4F46E5"
|
|
||||||
/>
|
|
||||||
<rect x="38.3522" y="21.5128" width="41.0256" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
|
||||||
<rect x="38.3522" y="133.65" width="54.7009" height="5.47009" rx="2.73504" fill="#A5B4FC" />
|
|
||||||
<rect x="38.3522" y="29.7179" width="13.6752" height="2.73504" rx="1.36752" fill="#4F46E5" />
|
|
||||||
<circle cx="56.13" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
|
||||||
<circle cx="61.6001" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
|
||||||
<circle cx="67.0702" cy="31.0854" r="1.36752" fill="#4F46E5" />
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-center text-black text-xl font-semibold leading-loose pb-2">There’s no listing here</h2>
|
|
||||||
<p class="text-center text-black text-base font-normal leading-relaxed pb-4">Try changing your filters to <br />see listings</p>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button (click)="clearAllFilters()" class="w-full px-3 py-2 rounded-full border border-gray-300 text-gray-900 text-xs font-semibold leading-4">Clear Filter</button>
|
|
||||||
<button (click)="openFilterModal()" class="w-full px-3 py-2 bg-indigo-600 hover:bg-indigo-700 transition-all duration-500 rounded-full text-white text-xs font-semibold leading-4">Change Filter</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
@if(pageCount>1){
|
|
||||||
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,19 @@ import { CommercialPropertyListingCriteria, LISTINGS_PER_PAGE, ResponseCommercia
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
|
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
|
||||||
import { ModalService } from '../../../components/search-modal/modal.service';
|
import { ModalService } from '../../../components/search-modal/modal.service';
|
||||||
|
import { SearchModalCommercialComponent } from '../../../components/search-modal/search-modal-commercial.component';
|
||||||
import { CriteriaChangeService } from '../../../services/criteria-change.service';
|
import { CriteriaChangeService } from '../../../services/criteria-change.service';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { SearchService } from '../../../services/search.service';
|
import { SearchService } from '../../../services/search.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { assignProperties, getCriteriaProxy, resetCommercialPropertyListingCriteria } from '../../../utils/utils';
|
import { assignProperties, getCriteriaProxy, resetCommercialPropertyListingCriteria } from '../../../utils/utils';
|
||||||
|
|
||||||
@UntilDestroy()
|
@UntilDestroy()
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-commercial-property-listings',
|
selector: 'app-commercial-property-listings',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent],
|
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, SearchModalCommercialComponent],
|
||||||
templateUrl: './commercial-property-listings.component.html',
|
templateUrl: './commercial-property-listings.component.html',
|
||||||
styleUrls: ['./commercial-property-listings.component.scss', '../../pages.scss'],
|
styleUrls: ['./commercial-property-listings.component.scss', '../../pages.scss'],
|
||||||
})
|
})
|
||||||
|
|
@ -41,6 +43,7 @@ export class CommercialPropertyListingsComponent {
|
||||||
page = 1;
|
page = 1;
|
||||||
pageCount = 1;
|
pageCount = 1;
|
||||||
ts = new Date().getTime();
|
ts = new Date().getTime();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public selectOptions: SelectOptionsService,
|
public selectOptions: SelectOptionsService,
|
||||||
private listingsService: ListingsService,
|
private listingsService: ListingsService,
|
||||||
|
|
@ -54,6 +57,7 @@ export class CommercialPropertyListingsComponent {
|
||||||
private criteriaChangeService: CriteriaChangeService,
|
private criteriaChangeService: CriteriaChangeService,
|
||||||
) {
|
) {
|
||||||
this.criteria = getCriteriaProxy('commercialPropertyListings', this) as CommercialPropertyListingCriteria;
|
this.criteria = getCriteriaProxy('commercialPropertyListings', this) as CommercialPropertyListingCriteria;
|
||||||
|
this.modalService.sendCriteria(this.criteria);
|
||||||
this.init();
|
this.init();
|
||||||
this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(criteria => {
|
this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(criteria => {
|
||||||
if (criteria && criteria.criteriaType === 'commercialPropertyListings') {
|
if (criteria && criteria.criteriaType === 'commercialPropertyListings') {
|
||||||
|
|
@ -62,10 +66,13 @@ export class CommercialPropertyListingsComponent {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {}
|
async ngOnInit() {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
|
|
||||||
async search() {
|
async search() {
|
||||||
const listingReponse = await this.listingsService.getListings(this.criteria, 'commercialProperty');
|
const listingReponse = await this.listingsService.getListings(this.criteria, 'commercialProperty');
|
||||||
this.listings = (<ResponseCommercialPropertyListingArray>listingReponse).results;
|
this.listings = (<ResponseCommercialPropertyListingArray>listingReponse).results;
|
||||||
|
|
@ -75,21 +82,26 @@ export class CommercialPropertyListingsComponent {
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
onPageChange(page: any) {
|
onPageChange(page: any) {
|
||||||
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE;
|
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE;
|
||||||
this.criteria.length = LISTINGS_PER_PAGE;
|
this.criteria.length = LISTINGS_PER_PAGE;
|
||||||
this.criteria.page = page;
|
this.criteria.page = page;
|
||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.criteria.title = null;
|
this.criteria.title = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTS() {
|
getTS() {
|
||||||
return new Date().getTime();
|
return new Date().getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
getDaysListed(listing: CommercialPropertyListing) {
|
getDaysListed(listing: CommercialPropertyListing) {
|
||||||
return dayjs().diff(listing.created, 'day');
|
return dayjs().diff(listing.created, 'day');
|
||||||
}
|
}
|
||||||
|
|
||||||
// New methods for filter actions
|
// New methods for filter actions
|
||||||
clearAllFilters() {
|
clearAllFilters() {
|
||||||
// Reset criteria to default values
|
// Reset criteria to default values
|
||||||
|
|
@ -106,12 +118,10 @@ export class CommercialPropertyListingsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
async openFilterModal() {
|
async openFilterModal() {
|
||||||
// Open the search modal with current criteria
|
|
||||||
const modalResult = await this.modalService.showModal(this.criteria);
|
const modalResult = await this.modalService.showModal(this.criteria);
|
||||||
if (modalResult.accepted) {
|
if (modalResult.accepted) {
|
||||||
this.searchService.search(this.criteria);
|
this.criteria = assignProperties(this.criteria, modalResult.criteria); // Update criteria with modal result
|
||||||
} else {
|
this.searchService.search(this.criteria); // Trigger search with updated criteria
|
||||||
this.criteria = assignProperties(this.criteria, modalResult.criteria);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue