landing page finished
This commit is contained in:
parent
6348af8862
commit
55e800009e
|
|
@ -10,6 +10,10 @@ export class GeoController {
|
|||
findByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix);
|
||||
}
|
||||
@Get('citiesandstates/:prefix')
|
||||
findByCitiesAndStatesByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesAndStatesStartingWith(prefix);
|
||||
}
|
||||
|
||||
@Get(':prefix/:state')
|
||||
findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,43 @@ export class GeoService {
|
|||
});
|
||||
return state ? result.filter(e => e.state_code.toLowerCase() === state.toLowerCase()) : result;
|
||||
}
|
||||
findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<{ id: string; name: string; type: 'city' | 'state'; state_code: string }> {
|
||||
const results: Array<{ id: string; name: string; type: 'city' | 'state'; state_code: string }> = [];
|
||||
|
||||
const lowercasePrefix = prefix.toLowerCase();
|
||||
|
||||
//for (const country of this.geo) {
|
||||
// Suche nach passenden Staaten
|
||||
for (const state of this.geo.states) {
|
||||
if (state.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: state.id.toString(),
|
||||
name: state.name,
|
||||
type: 'state',
|
||||
state_code: state.state_code,
|
||||
});
|
||||
}
|
||||
|
||||
// Suche nach passenden Städten
|
||||
for (const city of state.cities) {
|
||||
if (city.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: city.id.toString(),
|
||||
name: city.name,
|
||||
type: 'city',
|
||||
state_code: state.state_code,
|
||||
});
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
return results.sort((a, b) => {
|
||||
if (a.type === 'state' && b.type === 'city') return -1;
|
||||
if (a.type === 'city' && b.type === 'state') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
getCityWithCoords(state: string, city: string): City {
|
||||
return this.geo.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,6 +230,12 @@ export interface GeoResult {
|
|||
state: string;
|
||||
state_code: string;
|
||||
}
|
||||
export interface CityAndStateResult {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
state_code: string;
|
||||
}
|
||||
export interface CountyResult {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@ng-select/ng-select": "^13.4.1",
|
||||
"@ngneat/until-destroy": "^10.0.0",
|
||||
"@types/cropperjs": "^1.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"browser-bunyan": "^1.8.0",
|
||||
|
|
@ -69,4 +70,4 @@
|
|||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "~5.4.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,18 +77,18 @@
|
|||
</ul>
|
||||
</div>
|
||||
@if(criteria){
|
||||
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row border border-gray-300">
|
||||
<div class="md:flex-none md:w-40 flex-1 border-r border-gray-300 overflow-hidden">
|
||||
<div class="relative">
|
||||
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-gray-300">
|
||||
<div class="md:flex-none md:w-48 flex-1 md:border-r border-gray-300 overflow-hidden mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-gray-300 rounded-md">
|
||||
<select
|
||||
class="appearance-none bg-transparent w-full py-3 px-4 pr-8 focus:outline-none"
|
||||
class="appearance-none bg-transparent w-full py-3 px-4 pr-8 focus:outline-none md:border-none rounded-md md:rounded-none"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onTypesChange($event)"
|
||||
[ngClass]="{ 'placeholder-selected': criteria.types.length === 0 }"
|
||||
>
|
||||
<option [value]="[]">Business Type</option>
|
||||
@for(tob of selectOptions.typesOfBusiness; track tob){
|
||||
<option [value]="tob.value">{{ tob.name }}</option>
|
||||
<option [value]="[]">{{ getPlaceholderLabel() }}</option>
|
||||
@for(type of getTypes(); track type){
|
||||
<option [value]="type.value">{{ type.name }}</option>
|
||||
}
|
||||
</select>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||
|
|
@ -96,29 +96,34 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:flex-auto md:w-36 lex-grow border-b md:border-b-0 md:border-r border-gray-300">
|
||||
<ng-select
|
||||
class="custom"
|
||||
[multiple]="false"
|
||||
[hideSelected]="true"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="cityLoading"
|
||||
typeToSearchText="Please enter 2 or more characters"
|
||||
[typeahead]="cityInput$"
|
||||
[ngModel]="cityOrState"
|
||||
(ngModelChange)="setCity($event)"
|
||||
placeholder="Enter City or State ..."
|
||||
>
|
||||
@for (city of cities$ | async; track city.id) {
|
||||
<ng-option [value]="city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
|
||||
<div class="md:flex-auto md:w-36 flex-grow md:border-r border-gray-300 mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-gray-300 rounded-md">
|
||||
<ng-select
|
||||
class="custom md:border-none rounded-md md:rounded-none"
|
||||
[multiple]="false"
|
||||
[hideSelected]="true"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="cityLoading"
|
||||
typeToSearchText="Please enter 2 or more characters"
|
||||
[typeahead]="cityInput$"
|
||||
[ngModel]="cityOrState"
|
||||
(ngModelChange)="setCityOrState($event)"
|
||||
placeholder="Enter City or State ..."
|
||||
groupBy="type"
|
||||
>
|
||||
@for (city of cities$ | async; track city.id) {
|
||||
<ng-option [value]="city">{{ city.name }} - {{ city.state_code }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:flex-none md:w-36 flex-1 border-b md:border-b-0 md:border-r border-gray-300">
|
||||
<div class="relative">
|
||||
@if (criteria.radius){
|
||||
<div class="md:flex-none md:w-36 flex-1 md:border-r border-gray-300 mb-2 md:mb-0">
|
||||
<div class="relative max-sm:border border-gray-300 rounded-md">
|
||||
<select
|
||||
class="appearance-none bg-transparent w-full py-3 px-4 pr-8 focus:outline-none"
|
||||
class="appearance-none bg-transparent w-full py-3 px-4 pr-8 focus:outline-none md:border-none rounded-md md:rounded-none"
|
||||
(ngModelChange)="onRadiusChange($event)"
|
||||
[ngModel]="criteria.radius"
|
||||
[ngClass]="{ 'placeholder-selected': !criteria.radius }"
|
||||
|
|
@ -133,9 +138,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-600 hover:bg-blue-500 transition-colors duration-200">
|
||||
<button class="w-full h-full text-white font-semibold py-3 px-6 focus:outline-none" (click)="search()">Suchen</button>
|
||||
}
|
||||
<div class="bg-blue-600 hover:bg-blue-500 transition-colors duration-200 max-sm:rounded-md">
|
||||
@if(getNumberOfFiltersSet()>0 && numberOfResults$){
|
||||
<button class="w-full h-full text-white font-semibold py-3 px-6 focus:outline-none rounded-md md:rounded-none" (click)="search()">Search ({{ numberOfResults$ | async }})</button>
|
||||
}@else {
|
||||
<button class="w-full h-full text-white font-semibold py-3 px-6 focus:outline-none rounded-md md:rounded-none" (click)="search()">Search</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ select {
|
|||
}
|
||||
:host ::ng-deep .ng-select.ng-select-single .ng-select-container {
|
||||
height: 48px;
|
||||
border: unset;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
.ng-value-container .ng-input {
|
||||
top: 10px;
|
||||
}
|
||||
|
|
@ -43,13 +44,6 @@ select {
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
.flex-1-1-2 {
|
||||
flex: 1 1 2%;
|
||||
}
|
||||
// .light {
|
||||
// color: #999;
|
||||
// }
|
||||
// component.css
|
||||
select {
|
||||
color: #000; /* Standard-Textfarbe für das Dropdown */
|
||||
// background-color: #fff; /* Hintergrundfarbe für das Dropdown */
|
||||
|
|
|
|||
|
|
@ -3,17 +3,20 @@ import { ChangeDetectorRef, Component } from '@angular/core';
|
|||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import onChange from 'on-change';
|
||||
import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { catchError, concat, debounceTime, distinctUntilChanged, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs';
|
||||
import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { ModalService } from '../../components/search-modal/modal.service';
|
||||
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 { getCriteriaStateObject, map2User } from '../../utils/utils';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaStateObject, map2User } from '../../utils/utils';
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
|
|
@ -31,10 +34,12 @@ export class HomeComponent {
|
|||
isMenuOpen = false;
|
||||
user: KeycloakUser;
|
||||
prompt: string;
|
||||
cities$: Observable<GeoResult[]>;
|
||||
cities$: Observable<CityAndStateResult[]>;
|
||||
cityLoading = false;
|
||||
cityInput$ = new Subject<string>();
|
||||
cityOrState = undefined;
|
||||
private criteriaChangeSubscription: Subscription;
|
||||
numberOfResults$: Observable<number>;
|
||||
public constructor(
|
||||
private router: Router,
|
||||
private modalService: ModalService,
|
||||
|
|
@ -42,10 +47,11 @@ export class HomeComponent {
|
|||
private activatedRoute: ActivatedRoute,
|
||||
public selectOptions: SelectOptionsService,
|
||||
public keycloakService: KeycloakService,
|
||||
private listingsService: ListingsService,
|
||||
private criteriaChangeService: CriteriaChangeService,
|
||||
private geoService: GeoService,
|
||||
public cdRef: ChangeDetectorRef,
|
||||
private listingService: ListingsService,
|
||||
private userService: UserService,
|
||||
) {}
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
|
|
@ -55,6 +61,7 @@ export class HomeComponent {
|
|||
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('business'));
|
||||
this.user = map2User(token);
|
||||
this.loadCities();
|
||||
this.setupCriteriaChangeListener();
|
||||
}
|
||||
async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
|
||||
this.activeTabAction = tabname;
|
||||
|
|
@ -85,10 +92,11 @@ export class HomeComponent {
|
|||
});
|
||||
}
|
||||
search() {
|
||||
const data = { keep: true };
|
||||
this.router.navigate([`${this.activeTabAction}Listings`]);
|
||||
}
|
||||
|
||||
private setupCriteriaChangeListener() {
|
||||
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(untilDestroyed(this), debounceTime(400)).subscribe(() => this.setTotalNumberOfResults());
|
||||
}
|
||||
login() {
|
||||
this.keycloakService.login({
|
||||
redirectUri: window.location.href,
|
||||
|
|
@ -113,7 +121,7 @@ export class HomeComponent {
|
|||
// Wenn keine Option ausgewählt ist, setzen Sie types zurück auf ein leeres Array
|
||||
this.criteria.radius = null;
|
||||
} else {
|
||||
this.criteria.radius = value;
|
||||
this.criteria.radius = parseInt(value);
|
||||
}
|
||||
}
|
||||
async openModal() {
|
||||
|
|
@ -130,7 +138,8 @@ export class HomeComponent {
|
|||
distinctUntilChanged(),
|
||||
tap(() => (this.cityLoading = true)),
|
||||
switchMap(term =>
|
||||
this.geoService.findCitiesStartingWith(term).pipe(
|
||||
//this.geoService.findCitiesStartingWith(term).pipe(
|
||||
this.geoService.findCitiesAndStatesStartingWith(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)),
|
||||
|
|
@ -142,14 +151,61 @@ export class HomeComponent {
|
|||
trackByFn(item: GeoResult) {
|
||||
return item.id;
|
||||
}
|
||||
setCity(city) {
|
||||
if (city) {
|
||||
this.criteria.city = city.city;
|
||||
this.criteria.state = city.state_code;
|
||||
setCityOrState(cityOrState: CityAndStateResult) {
|
||||
if (cityOrState) {
|
||||
if (cityOrState.type === 'state') {
|
||||
this.criteria.state = cityOrState.state_code;
|
||||
} else {
|
||||
this.criteria.city = cityOrState.name;
|
||||
this.criteria.state = cityOrState.state_code;
|
||||
this.criteria.searchType = 'radius';
|
||||
this.criteria.radius = 20;
|
||||
}
|
||||
} else {
|
||||
this.criteria.city = null;
|
||||
this.criteria.radius = null;
|
||||
this.criteria.searchType = 'exact';
|
||||
}
|
||||
}
|
||||
getTypes() {
|
||||
if (this.criteria.criteriaType === 'business') {
|
||||
return this.selectOptions.typesOfBusiness;
|
||||
} else if (this.criteria.criteriaType === 'commercialProperty') {
|
||||
return this.selectOptions.typesOfCommercialProperty;
|
||||
} else {
|
||||
return this.selectOptions.customerSubTypes;
|
||||
}
|
||||
}
|
||||
getPlaceholderLabel() {
|
||||
if (this.criteria.criteriaType === 'business') {
|
||||
return 'Business Type';
|
||||
} else if (this.criteria.criteriaType === 'commercialProperty') {
|
||||
return 'Property Type';
|
||||
} else {
|
||||
return 'Professional Type';
|
||||
}
|
||||
}
|
||||
setTotalNumberOfResults() {
|
||||
if (this.criteria) {
|
||||
console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
|
||||
if (this.criteria.criteriaType === 'business' || this.criteria.criteriaType === 'commercialProperty') {
|
||||
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria, this.criteria.criteriaType);
|
||||
} else if (this.criteria.criteriaType === 'broker') {
|
||||
this.numberOfResults$ = this.userService.getNumberOfBroker(this.criteria);
|
||||
} else {
|
||||
this.numberOfResults$ = of();
|
||||
}
|
||||
}
|
||||
}
|
||||
getNumberOfFiltersSet() {
|
||||
if (this.criteria?.criteriaType === 'broker') {
|
||||
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||
} else if (this.criteria?.criteriaType === 'business') {
|
||||
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||
} else if (this.criteria?.criteriaType === 'commercialProperty') {
|
||||
return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { CountyResult, GeoResult } from '../../../../bizmatch-server/src/models/main.model';
|
||||
import { CityAndStateResult, CountyResult, GeoResult } from '../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable({
|
||||
|
|
@ -15,6 +15,9 @@ export class GeoService {
|
|||
const stateString = state ? `/${state}` : '';
|
||||
return this.http.get<GeoResult[]>(`${this.apiBaseUrl}/bizmatch/geo/${prefix}${stateString}`);
|
||||
}
|
||||
findCitiesAndStatesStartingWith(prefix: string): Observable<CityAndStateResult[]> {
|
||||
return this.http.get<CityAndStateResult[]>(`${this.apiBaseUrl}/bizmatch/geo/citiesandstates/${prefix}`);
|
||||
}
|
||||
findCountiesStartingWith(prefix: string, states?: string[]): Observable<CountyResult[]> {
|
||||
return this.http.post<CountyResult[]>(`${this.apiBaseUrl}/bizmatch/geo/counties`, { prefix, states });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue