Paginator & SQL Querries where clauses & city search
This commit is contained in:
@@ -11,9 +11,10 @@ import { filter, Observable, Subject, Subscription } from 'rxjs';
|
||||
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { SearchService } from '../../services/search.service';
|
||||
import { SharedService } from '../../services/shared.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaStateObject, getSessionStorageHandlerWrapper, map2User } from '../../utils/utils';
|
||||
import { getCriteriaStateObject, getSessionStorageHandlerWrapper, map2User } from '../../utils/utils';
|
||||
import { DropdownComponent } from '../dropdown/dropdown.component';
|
||||
import { ModalService } from '../search-modal/modal.service';
|
||||
@Component({
|
||||
@@ -46,9 +47,8 @@ export class HeaderComponent {
|
||||
private sharedService: SharedService,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
private modalService: ModalService,
|
||||
) {
|
||||
//this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandlerWrapper(this.activeTabAction));
|
||||
}
|
||||
private searchService: SearchService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
@@ -89,14 +89,18 @@ export class HeaderComponent {
|
||||
}
|
||||
ngAfterViewInit() {}
|
||||
|
||||
openModal() {
|
||||
if (this.isActive('/businessListings')) {
|
||||
this.modalService.showModal(createEmptyBusinessListingCriteria());
|
||||
} else if (this.isActive('/commercialPropertyListings')) {
|
||||
this.modalService.showModal(createEmptyCommercialPropertyListingCriteria());
|
||||
} else if (this.isActive('/brokerListings')) {
|
||||
this.modalService.showModal(createEmptyUserListingCriteria());
|
||||
async openModal() {
|
||||
const accepted = await this.modalService.showModal(this.criteria);
|
||||
if (accepted) {
|
||||
this.searchService.search(this.criteria);
|
||||
}
|
||||
// if (this.isActive('/businessListings')) {
|
||||
// this.modalService.showModal(createEmptyBusinessListingCriteria());
|
||||
// } else if (this.isActive('/commercialPropertyListings')) {
|
||||
// this.modalService.showModal(createEmptyCommercialPropertyListingCriteria());
|
||||
// } else if (this.isActive('/brokerListings')) {
|
||||
// this.modalService.showModal(createEmptyUserListingCriteria());
|
||||
// }
|
||||
}
|
||||
navigateWithState(dest: string, state: any) {
|
||||
this.router.navigate([dest], { state: state });
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<p>paginator works!</p>
|
||||
98
bizmatch/src/app/components/paginator/paginator.component.ts
Normal file
98
bizmatch/src/app/components/paginator/paginator.component.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-paginator',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<nav class="my-2" aria-label="Page navigation">
|
||||
<ul class="flex justify-center items-center -space-x-px h-8 text-sm">
|
||||
<li>
|
||||
<a
|
||||
(click)="onPageChange(currentPage - 1)"
|
||||
[class.pointer-events-none]="currentPage === 1"
|
||||
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
>
|
||||
<span class="sr-only">Previous</span>
|
||||
<svg class="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<ng-container *ngFor="let page of visiblePages">
|
||||
<li *ngIf="page !== '...'">
|
||||
<a
|
||||
(click)="onPageChange(page)"
|
||||
[ngClass]="
|
||||
page === currentPage
|
||||
? 'z-10 flex items-center justify-center px-3 h-8 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white'
|
||||
: 'flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white'
|
||||
"
|
||||
>
|
||||
{{ page }}
|
||||
</a>
|
||||
</li>
|
||||
<li *ngIf="page === '...'">
|
||||
<span class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400">...</span>
|
||||
</li>
|
||||
</ng-container>
|
||||
<li>
|
||||
<a
|
||||
(click)="onPageChange(currentPage + 1)"
|
||||
[class.pointer-events-none]="currentPage === pageCount"
|
||||
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
|
||||
>
|
||||
<span class="sr-only">Next</span>
|
||||
<svg class="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" />
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
`,
|
||||
})
|
||||
export class PaginatorComponent implements OnChanges {
|
||||
@Input() page = 1;
|
||||
@Input() pageCount = 1;
|
||||
@Output() pageChange = new EventEmitter<number>();
|
||||
|
||||
currentPage = 1;
|
||||
visiblePages: (number | string)[] = [];
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['page'] || changes['pageCount']) {
|
||||
this.currentPage = this.page;
|
||||
this.updateVisiblePages();
|
||||
}
|
||||
}
|
||||
|
||||
updateVisiblePages(): void {
|
||||
const totalPages = this.pageCount;
|
||||
const current = this.currentPage;
|
||||
|
||||
if (totalPages <= 6) {
|
||||
this.visiblePages = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
} else {
|
||||
if (current <= 3) {
|
||||
this.visiblePages = [1, 2, 3, 4, '...', totalPages];
|
||||
} else if (current >= totalPages - 2) {
|
||||
this.visiblePages = [1, '...', totalPages - 3, totalPages - 2, totalPages - 1, totalPages];
|
||||
} else {
|
||||
this.visiblePages = [1, '...', current - 1, current, current + 1, '...', totalPages];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPageChange(page: number | string): void {
|
||||
if (typeof page === 'string') {
|
||||
return;
|
||||
}
|
||||
if (page >= 1 && page <= this.pageCount && page !== this.currentPage) {
|
||||
this.currentPage = page;
|
||||
this.pageChange.emit(page);
|
||||
this.updateVisiblePages();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,13 +28,28 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
|
||||
<input
|
||||
<!-- <input
|
||||
type="text"
|
||||
id="city"
|
||||
[(ngModel)]="criteria.city"
|
||||
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. Houston"
|
||||
/>
|
||||
/> -->
|
||||
<ng-select
|
||||
class="custom"
|
||||
[multiple]="false"
|
||||
[hideSelected]="true"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="cityLoading"
|
||||
typeToSearchText="Please enter 2 or more characters"
|
||||
[typeahead]="cityInput$"
|
||||
[(ngModel)]="criteria.city"
|
||||
>
|
||||
@for (city of cities$ | async; track city.id) {
|
||||
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -205,13 +220,28 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
|
||||
<input
|
||||
<!-- <input
|
||||
type="text"
|
||||
id="city"
|
||||
[(ngModel)]="criteria.city"
|
||||
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. Houston"
|
||||
/>
|
||||
/> -->
|
||||
<ng-select
|
||||
class="custom"
|
||||
[multiple]="false"
|
||||
[hideSelected]="true"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="cityLoading"
|
||||
typeToSearchText="Please enter 2 or more characters"
|
||||
[typeahead]="cityInput$"
|
||||
[(ngModel)]="criteria.city"
|
||||
>
|
||||
@for (city of cities$ | async; track city.id) {
|
||||
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label>
|
||||
@@ -265,7 +295,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @if(criteria.criteriaType==='user'){
|
||||
} @if(criteria.criteriaType==='broker'){
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
@@ -280,13 +310,28 @@
|
||||
</div>
|
||||
<div>
|
||||
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
|
||||
<input
|
||||
<!-- <input
|
||||
type="text"
|
||||
id="city"
|
||||
[(ngModel)]="criteria.city"
|
||||
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. Houston"
|
||||
/>
|
||||
/> -->
|
||||
<ng-select
|
||||
class="custom"
|
||||
[multiple]="false"
|
||||
[hideSelected]="true"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="cityLoading"
|
||||
typeToSearchText="Please enter 2 or more characters"
|
||||
[typeahead]="cityInput$"
|
||||
[(ngModel)]="criteria.city"
|
||||
>
|
||||
@for (city of cities$ | async; track city.id) {
|
||||
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
|
||||
@@ -3,4 +3,7 @@
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||
height: 46px;
|
||||
border-radius: 0.5rem;
|
||||
.ng-value-container .ng-input {
|
||||
top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, KeyValue, KeyValueStyle, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, GeoResult, KeyValue, KeyValueStyle, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { GeoService } from '../../services/geo.service';
|
||||
import { SelectOptionsService } from '../../services/select-options.service';
|
||||
import { SharedModule } from '../../shared/shared/shared.module';
|
||||
import { ModalService } from './modal.service';
|
||||
@@ -14,11 +16,15 @@ import { ModalService } from './modal.service';
|
||||
styleUrl: './search-modal.component.scss',
|
||||
})
|
||||
export class SearchModalComponent {
|
||||
constructor(public selectOptions: SelectOptionsService, public modalService: ModalService) {}
|
||||
cities$: Observable<GeoResult[]>;
|
||||
cityLoading = false;
|
||||
cityInput$ = new Subject<string>();
|
||||
constructor(public selectOptions: SelectOptionsService, public modalService: ModalService, private geoService: GeoService) {}
|
||||
ngOnInit() {
|
||||
this.modalService.message$.subscribe(msg => {
|
||||
this.criteria = msg;
|
||||
});
|
||||
this.loadCities();
|
||||
}
|
||||
public criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
|
||||
|
||||
@@ -32,6 +38,25 @@ export class SearchModalComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
private loadCities() {
|
||||
this.cities$ = concat(
|
||||
of([]), // default items
|
||||
this.cityInput$.pipe(
|
||||
distinctUntilChanged(),
|
||||
tap(() => (this.cityLoading = true)),
|
||||
switchMap(term =>
|
||||
this.geoService.findCitiesStartingWith(term).pipe(
|
||||
catchError(() => of([])), // empty list on error
|
||||
// map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names
|
||||
tap(() => (this.cityLoading = false)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
trackByFn(item: GeoResult) {
|
||||
return item.id;
|
||||
}
|
||||
search() {
|
||||
console.log('Search criteria:', this.criteria);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user