Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ <h1>No listing found for this category</h1>
<div class="flex flex-column align-items-center justify-content-center h-10rem">
<h1>No match for your search !</h1>
<div class="text-xl">Try with different parameters</div>
<button class="p-button mt-3" (click)="onResetSearchFilter()">Reset all the filters</button>
<button class="p-button mt-3 bg-blue-500 border-blue-500" (click)="onResetSearchFilter()">Reset all the filters</button>
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface City {
name: string;
code: string;
countryCode: string;
region?: string;
latitude?: number;
longitude?: number;
}

export type Cities = City[];
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {computed, inject, Injectable, signal, WritableSignal} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {City} from "./city.model";
import {State} from "../../../../core/model/state.model";
import {catchError, tap, shareReplay} from "rxjs";

@Injectable({
providedIn: 'root'
})
export class CityService {

http = inject(HttpClient);

private cities$: WritableSignal<State<Array<City>>> =
signal(State.Builder<Array<City>>().forInit());
cities = computed(() => this.cities$());

constructor() {
this.initFetchGetAllCities();
}

initFetchGetAllCities(): void {
this.http.get<Array<City>>("/assets/cities-morocco.json")
.pipe(
tap(cities => {
console.log('Cities loaded:', cities); // Pour débugger
this.cities$.set(State.Builder<Array<City>>().forSuccess(cities));
}),
catchError((err) => {
console.error('Error loading cities:', err); // Pour débugger
this.cities$.set(State.Builder<Array<City>>().forError(err));
throw err;
}),
shareReplay(1)
)
.subscribe({
next: (data) => console.log('Subscribe success', data),
error: (err) => console.error('Subscribe error', err),
complete: () => console.log('Cities loaded complete')
});
}

public getCityByCode(code: string): City | undefined {
const citiesState = this.cities$();
if (citiesState.status === "OK" && citiesState.value) {
return citiesState.value.find(city => city.code === code);
}
return undefined;
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<div class="my-4 flex justify-content-center align-items-center">
<p-autoComplete
[styleClass]="'autocomplete-nbat'"
[panelStyleClass]="'country-panel'"
[panelStyleClass]="'city-panel'"
[ngModel]="currentLocation"
(onSelect)="onLocationChange($event)"
[suggestions]="filteredCountries"
[suggestions]="filteredCities()"
(completeMethod)="search($event)"
[dropdown]="true"
[optionLabel]="formatLabel"
[placeholder]="'📍 ' + placeholder()"></p-autoComplete>
[placeholder]="placeholder()"></p-autoComplete>
</div>
<div leaflet
[leafletOptions]="options"
[leafletLayersControl]="layersControl"
(leafletMapReady)="onMapReady($event)"
class="map-location z-0"></div>
class="map-location z-0"></div>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
width: 400px!important;
}

::ng-deep .country-panel {
::ng-deep .city-panel {
width: 350px;
}

Expand All @@ -20,4 +20,4 @@
font-family: $fontFamily;
font-size: 1rem;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,136 +1,156 @@
import {Component, effect, EventEmitter, inject, input, Output} from '@angular/core';
import {Component, effect, EventEmitter, inject, input, OnInit, Output, signal, WritableSignal, ChangeDetectionStrategy, ChangeDetectorRef, DestroyRef} from '@angular/core';
import * as L from 'leaflet';
import {ControlOptions, LatLng, Layer, Map, MapOptions, marker, tileLayer} from 'leaflet';
import {LeafletModule} from "@asymmetrik/ngx-leaflet";
import {FormsModule} from "@angular/forms";
import {AutoCompleteCompleteEvent, AutoCompleteModule, AutoCompleteSelectEvent} from "primeng/autocomplete";
import {CountryService} from "../country.service";
import {ToastService} from "../../../../../layout/toast.service";
import {OpenStreetMapProvider} from "leaflet-geosearch";
import {Country} from "../country.model";
import L, {circle, latLng, polygon, tileLayer} from "leaflet";
import {filter} from "rxjs";
import {FormsModule} from "@angular/forms";
import {CityService} from "../city.service";
import {City} from "../city.model";

@Component({
selector: 'app-location-map',
standalone: true,
imports: [
LeafletModule,
FormsModule,
AutoCompleteModule
AutoCompleteModule,
FormsModule
],
templateUrl: './location-map.component.html',
styleUrl: './location-map.component.scss'
styleUrl: './location-map.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LocationMapComponent {

countryService = inject(CountryService);
toastService = inject(ToastService);

private map: L.Map | undefined;
private provider: OpenStreetMapProvider | undefined;
export class LocationMapComponent implements OnInit {

placeholder = input<string>("Search for a city");
location = input.required<string>();
placeholder = input<string>("Select your home country");

currentLocation: Country | undefined;

@Output()
locationChange = new EventEmitter<string>();

formatLabel = (country: Country) => "📍" + country.name.common;
cityService = inject(CityService);
changeDetectorRef = inject(ChangeDetectorRef);
destroyRef = inject(DestroyRef);

map!: Map;
currentLocation: City | undefined;
cities: WritableSignal<Array<City>> = signal([]);
filteredCities: WritableSignal<Array<City>> = signal([]);
// filteredCities: Array<City> = [];
markerLayer: Layer | undefined;

options = {
options: MapOptions = {
layers: [
tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
{maxZoom: 18,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'}),
tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
opacity: 0.9,
maxZoom: 19,
detectRetina: true,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
})
],
zoom: 6,
center: latLng(31.7917, -7.0926)
}
center: new LatLng(31.7917, -7.0926)
};

layersControl = {
baseLayers: {
"Open Street Map": tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 18,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}),
'OpenStreetMap': tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
opacity: 0.9,
maxZoom: 19,
detectRetina: true,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
})
},
overlays: {
"Big Circle": circle([46.95, -122], {radius: 5000}),
"Big square": polygon([[46.8, -121.55], [46.8, -121.55], [46.8, -121.55], [46.8, -121.55]])
"Big circle": L.circle([46.95, -122], {radius: 5000}),
"Big square": L.polygon([[46.8, -121.55], [46.8, -121.55], [46.8, -121.55], [46.8, -121.55]])
}
}

countries: Array<Country> = [];
filteredCountries: Array<Country> = [];

};

constructor() {
this.listenToLocation();
}
effect(() => {
const citiesState = this.cityService.cities();
console.log('Cities state from service:', citiesState);
if (citiesState.status === "OK" && citiesState.value) {
console.log('Setting cities:', citiesState.value);
this.cities.set(citiesState.value);
this.changeDetectorRef.markForCheck();
if (this.location()) {
this.currentLocation = citiesState.value.find(city => city.code === this.location());
if (this.currentLocation) {
this.centerMapOnCity(this.currentLocation);
}
}
}
}, { allowSignalWrites: true });

onMapReady(map: L.Map) {
this.map = map;
this.configSearchControl();
effect(() => {
const newLocation = this.location();
const city = this.cities().find(c => c.code === newLocation);
if (city) {
this.currentLocation = city;
this.centerMapOnCity(city);
}
});
}

private configSearchControl() {
this.provider = new OpenStreetMapProvider();
ngOnInit(): void {
}

onLocationChange(newEvent: AutoCompleteSelectEvent) {
const newCountry = newEvent.value as Country;
this.locationChange.emit(newCountry.cca3);
search(event: AutoCompleteCompleteEvent): void {
console.log('Search query:', event.query);
console.log('All cities:', this.cities());

if (!event.query || event.query.trim() === '') {
this.filteredCities.set(this.cities());
} else {
const filtered = this.cities()
.filter(city => city.name.toLowerCase().includes(event.query.toLowerCase()));
console.log('Filtered cities:', filtered);
this.filteredCities.set(filtered);
}
this.changeDetectorRef.markForCheck();
}

private listenToLocation() {
effect(() => {
const countriesState = this.countryService.countries();
if (countriesState.status === "OK" && countriesState.value) {
this.countries = countriesState.value;
this.filteredCountries = countriesState.value;
this.changeMapLocation(this.location())
} else if (countriesState.status === "ERROR") {
this.toastService.send({
severity: "error", summary: "Error",
detail: "Something went wrong when loading countries on change location"
});
}
});
onMapReady(map: Map) {
this.map = map;
if (this.currentLocation) {
this.centerMapOnCity(this.currentLocation);
}
}

private changeMapLocation(term: string) {
this.currentLocation = this.countries.find(country => country.cca3 === term);
if (this.currentLocation) {
this.provider!.search({query: this.currentLocation.name.common})
.then((results) => {
if (results && results.length > 0) {
const firstResult = results[0];
this.map!.setView(new L.LatLng(firstResult.y, firstResult.x), 6);
const customIcon = L.divIcon({
className: 'custom-marker',
html: `
<div style="
font-size: 32px;
text-align: center;
">📍</div>
`,
iconSize: [32, 32],
iconAnchor: [16, 32]
});
L.marker([firstResult.y, firstResult.x], { icon: customIcon })
.addTo(this.map!)
.bindPopup(firstResult.label)
.openPopup();
}
})
onLocationChange(newLocationEvt: AutoCompleteSelectEvent): void {
const newLocation = newLocationEvt.value as City;
this.locationChange.emit(newLocation.code);
this.centerMapOnCity(newLocation);
}
}

search(newCompleteEvent: AutoCompleteCompleteEvent): void {
this.filteredCountries =
this.countries.filter(country => country.name.common.toLowerCase().startsWith(newCompleteEvent.query))
private centerMapOnCity(city: City): void {
if (this.map && city.latitude !== undefined && city.longitude !== undefined) {
const latLng = new LatLng(city.latitude, city.longitude);
this.map.setView(latLng, 12);


if (this.markerLayer) {
this.map.removeLayer(this.markerLayer);
}


this.markerLayer = marker(latLng, {
icon: L.icon({
iconUrl: 'assets/images/marker-icon.png',
shadowUrl: 'assets/images/marker-shadow.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
})
}).addTo(this.map);

this.markerLayer.bindPopup(`<b>${city.name}</b><br>${city.region || ''}`).openPopup();
}
}

protected readonly filter = filter;
}
formatLabel = (city: City) => {
return `${city.name} - ${city.region}`;
};
}
4 changes: 2 additions & 2 deletions src/app/shared/footer-step/footer-step.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
}
@if (currentStep().idNext != null) {
<button [disabled]="!currentStep().isValid" [class.p-disabled]="!currentStep().isValid"
class="p-button p-button-secondary" id="next" (click)="onNext()">Next
class="p-button p-button-secondary bg-blue-500 border-blue-500" id="next" (click)="onNext()">Next
</button>
} @else {
<button class="p-button flex align-items-center" id="create-listing"
<button class="p-button flex align-items-center bg-blue-500 border-blue-500" id="create-listing"
(click)="onFinish()" [disabled]="!isAllStepsValid() || loading()"
[class.p-disabled]="!isAllStepsValid() || loading()">
<div class="btn-create">{{ labelFinishedBtn() }}</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/tenant/search/search.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
@case (LOCATION) {
<h1>Where do you want to go?</h1>
<app-location-map [location]="newSearch.location"
[placeholder]="'Choose your country'"
[placeholder]="'Choose your city'"
(locationChange)="onNewLocation($event)"></app-location-map>
}
@case (DATES) {
Expand Down
Loading