Filter for commercial properties
This commit is contained in:
parent
7d336f975d
commit
c62af8746f
|
|
@ -41,5 +41,6 @@
|
|||
|
||||
<app-message-container></app-message-container>
|
||||
<app-search-modal></app-search-modal>
|
||||
<app-search-modal-commercial></app-search-modal-commercial>
|
||||
<app-confirmation></app-confirmation>
|
||||
<app-email></app-email>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { EMailComponent } from './components/email/email.component';
|
|||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { HeaderComponent } from './components/header/header.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 { AuditService } from './services/audit.service';
|
||||
import { GeoService } from './services/geo.service';
|
||||
|
|
@ -19,7 +20,7 @@ import { UserService } from './services/user.service';
|
|||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent, EMailComponent],
|
||||
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, SearchModalCommercialComponent, ConfirmationComponent, EMailComponent],
|
||||
providers: [],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// 1. Shared Service (modal.service.ts)
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, ModalResult, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
|
|
@ -7,16 +6,16 @@ import { BusinessListingCriteria, CommercialPropertyListingCriteria, ModalResult
|
|||
providedIn: 'root',
|
||||
})
|
||||
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 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();
|
||||
|
||||
showModal(message: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): Promise<ModalResult> {
|
||||
this.messageSubject.next(message);
|
||||
this.modalVisibleSubject.next(true);
|
||||
this.modalVisibleSubject.next({ visible: true, type: message.criteriaType });
|
||||
return new Promise<ModalResult>(resolve => {
|
||||
this.resolvePromise = resolve;
|
||||
});
|
||||
|
|
@ -28,12 +27,12 @@ export class ModalService {
|
|||
});
|
||||
}
|
||||
accept(): void {
|
||||
this.modalVisibleSubject.next(false);
|
||||
this.modalVisibleSubject.next({ visible: false });
|
||||
this.resolvePromise({ accepted: true });
|
||||
}
|
||||
|
||||
reject(backupCriteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): void {
|
||||
this.modalVisibleSubject.next(false);
|
||||
this.modalVisibleSubject.next({ visible: false });
|
||||
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 bg-white rounded-lg shadow">
|
||||
<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>
|
||||
} @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">
|
||||
<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" />
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export class SearchModalComponent {
|
|||
this.setTotalNumberOfResults();
|
||||
});
|
||||
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
|
||||
if (val) {
|
||||
if (val.visible) {
|
||||
this.criteria.page = 1;
|
||||
this.criteria.start = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
<div class="container mx-auto px-4 py-8">
|
||||
@if(listings?.length>0){
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<!-- Filter Panel for Desktop -->
|
||||
<div class="hidden md:block w-full md:w-1/4 h-full bg-white shadow-lg p-6 overflow-y-auto z-10">
|
||||
<app-search-modal-commercial [isModal]="false"></app-search-modal-commercial>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="w-full p-4">
|
||||
<div class="container mx-auto">
|
||||
@if(listings?.length > 0) {
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@for (listing of listings; track listing.id) {
|
||||
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden flex flex-col h-full">
|
||||
|
|
@ -34,7 +42,7 @@
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (listings?.length===0){
|
||||
} @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">
|
||||
|
|
@ -88,7 +96,12 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if(pageCount > 1) {
|
||||
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Filter Button for Mobile -->
|
||||
<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>
|
||||
</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 { PaginatorComponent } from '../../../components/paginator/paginator.component';
|
||||
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 { ImageService } from '../../../services/image.service';
|
||||
import { ListingsService } from '../../../services/listings.service';
|
||||
import { SearchService } from '../../../services/search.service';
|
||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||
import { assignProperties, getCriteriaProxy, resetCommercialPropertyListingCriteria } from '../../../utils/utils';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-commercial-property-listings',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent],
|
||||
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, SearchModalCommercialComponent],
|
||||
templateUrl: './commercial-property-listings.component.html',
|
||||
styleUrls: ['./commercial-property-listings.component.scss', '../../pages.scss'],
|
||||
})
|
||||
|
|
@ -41,6 +43,7 @@ export class CommercialPropertyListingsComponent {
|
|||
page = 1;
|
||||
pageCount = 1;
|
||||
ts = new Date().getTime();
|
||||
|
||||
constructor(
|
||||
public selectOptions: SelectOptionsService,
|
||||
private listingsService: ListingsService,
|
||||
|
|
@ -54,6 +57,7 @@ export class CommercialPropertyListingsComponent {
|
|||
private criteriaChangeService: CriteriaChangeService,
|
||||
) {
|
||||
this.criteria = getCriteriaProxy('commercialPropertyListings', this) as CommercialPropertyListingCriteria;
|
||||
this.modalService.sendCriteria(this.criteria);
|
||||
this.init();
|
||||
this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(criteria => {
|
||||
if (criteria && criteria.criteriaType === 'commercialPropertyListings') {
|
||||
|
|
@ -62,10 +66,13 @@ export class CommercialPropertyListingsComponent {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {}
|
||||
|
||||
async init() {
|
||||
this.search();
|
||||
}
|
||||
|
||||
async search() {
|
||||
const listingReponse = await this.listingsService.getListings(this.criteria, 'commercialProperty');
|
||||
this.listings = (<ResponseCommercialPropertyListingArray>listingReponse).results;
|
||||
|
|
@ -75,21 +82,26 @@ export class CommercialPropertyListingsComponent {
|
|||
this.cdRef.markForCheck();
|
||||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
onPageChange(page: any) {
|
||||
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE;
|
||||
this.criteria.length = LISTINGS_PER_PAGE;
|
||||
this.criteria.page = page;
|
||||
this.search();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.criteria.title = null;
|
||||
}
|
||||
|
||||
getTS() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
getDaysListed(listing: CommercialPropertyListing) {
|
||||
return dayjs().diff(listing.created, 'day');
|
||||
}
|
||||
|
||||
// New methods for filter actions
|
||||
clearAllFilters() {
|
||||
// Reset criteria to default values
|
||||
|
|
@ -106,12 +118,10 @@ export class CommercialPropertyListingsComponent {
|
|||
}
|
||||
|
||||
async openFilterModal() {
|
||||
// Open the search modal with current criteria
|
||||
const modalResult = await this.modalService.showModal(this.criteria);
|
||||
if (modalResult.accepted) {
|
||||
this.searchService.search(this.criteria);
|
||||
} else {
|
||||
this.criteria = assignProperties(this.criteria, modalResult.criteria);
|
||||
this.criteria = assignProperties(this.criteria, modalResult.criteria); // Update criteria with modal result
|
||||
this.searchService.search(this.criteria); // Trigger search with updated criteria
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue