Skip to content

Commit

Permalink
Merge pull request #52 from Kleostro/feat/tu-01-10/station-coordinate…
Browse files Browse the repository at this point in the history
…s-input

feat(tu-01-10): station coordinates input
  • Loading branch information
Kleostro authored Aug 17, 2024
2 parents 05bc402 + b410544 commit eb9a35d
Show file tree
Hide file tree
Showing 24 changed files with 468 additions and 43 deletions.
1 change: 1 addition & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"styles": [
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
"node_modules/primeng/resources/primeng.min.css",
"node_modules/maplibre-gl/dist/maplibre-gl.css",
"src/styles.scss"
],
"stylePreprocessorOptions": {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@
"@ngrx/store": "^18.0.2",
"@ngrx/store-devtools": "^18.0.1",
"@planess/train-a-backend": "^0.0.3",
"@types/geojson": "^7946.0.14",
"@types/jest": "^29.5.12",
"@types/maplibre-gl": "^1.14.0",
"eslint-plugin-unused-imports": "^4.1.3",
"express": "^4.18.2",
"jest": "^29.7.0",
"jest-preset-angular": "^14.2.2",
"maplibre-gl": "^4.5.2",
"modern-normalize": "^3.0.0",
"primeicons": "^7.0.0",
"primeng": "^17.18.9",
Expand Down
8 changes: 8 additions & 0 deletions public/styles/common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ body {

body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans',
'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}

.hidden {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<form [formGroup]="createStationForm" (ngSubmit)="submitForm()" class="form">
<h1>Create Station</h1>
<div class="controls">
<label for="city" class="p-float-label control-label">
<span class="p-float-label__label">City</span>
<input
id="city"
type="text"
pInputText
formControlName="city"
placeholder="Bristol"
class="p-inputtext control"
autocomplete="off"
/>
@if (createStationForm.controls.city; as city) {
@if (city.invalid && city.dirty) {
<ng-container>
@if (city.hasError('required')) {
<small class="p-error">Please enter a city!</small>
}
</ng-container>
}
}
</label>

<label for="latitude" class="p-float-label control-label">
<span class="p-float-label__label">Latitude</span>
<input
id="latitude"
type="number"
pInputText
formControlName="latitude"
placeholder="Latitude"
class="p-inputtext control"
/>
@if (createStationForm.controls.latitude; as latitude) {
@if (latitude.invalid && latitude.dirty) {
<ng-container>
@if (latitude.hasError('required')) {
<small class="p-error">Please enter a latitude!</small>
}
</ng-container>
}
}
</label>

<label for="longitude" class="p-float-label control-label">
<span class="p-float-label__label">Longitude</span>
<input
id="longitude"
type="number"
pInputText
formControlName="longitude"
placeholder="Longitude"
class="p-inputtext control"
/>
@if (createStationForm.controls.longitude; as longitude) {
@if (longitude.invalid && longitude.dirty) {
<ng-container>
@if (longitude.hasError('required')) {
<small class="p-error">Please enter a longitude!</small>
}
</ng-container>
}
}
</label>

<button
pButton
pRipple
type="submit"
class="p-button submit"
[loading]="isStationCreated()"
[disabled]="createStationForm.invalid"
>
Create
</button>
</div>
<p-toast position="bottom-left" />
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.form {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 4.813rem;
}

.controls {
display: flex;
flex-direction: column;
gap: 1rem;
}

.control {
width: 100%;

&-label {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
}

.submit {
gap: 0.5rem;
justify-content: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';

import { MessageService } from 'primeng/api';
import { ButtonModule } from 'primeng/button';
import { FloatLabelModule } from 'primeng/floatlabel';
import { InputNumberModule } from 'primeng/inputnumber';
import { RippleModule } from 'primeng/ripple';
import { ToastModule } from 'primeng/toast';
import { map, Observable, of, Subscription, switchMap } from 'rxjs';

import { StationsService } from '@/app/api/stationsService/stations.service';
import MESSAGE_STATUS from '@/app/shared/constants/message-status';

import { MapService } from '../../services/map/map.service';

@Component({
selector: 'app-create-station-form',
standalone: true,
imports: [FloatLabelModule, InputNumberModule, ReactiveFormsModule, RippleModule, ButtonModule, ToastModule],
providers: [MessageService],
templateUrl: './create-station-form.component.html',
styleUrl: './create-station-form.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CreateStationFormComponent implements OnInit, OnDestroy {
private fb = inject(FormBuilder);
private mapService = inject(MapService);
private stationsService = inject(StationsService);
private messageService = inject(MessageService);
private subscription = new Subscription();

public isStationCreated = signal(false);

private createStation(city: string, latitude: number, longitude: number): Observable<{ id: number }> {
return this.stationsService.createNewStation({
city,
latitude,
longitude,
relations: [],
});
}

private createMarker(city: string, latitude: number, longitude: number): void {
this.mapService.createNewMarker({
city,
lat: latitude,
lng: longitude,
});
}

public createStationForm = this.fb.nonNullable.group({
city: ['', [Validators.required.bind(this)]],
latitude: [0, [Validators.required.bind(this)]],
longitude: [0, [Validators.required.bind(this)]],
});

public submitForm(): void {
this.createStationForm.markAllAsTouched();
this.createStationForm.updateValueAndValidity();

if (this.createStationForm.valid) {
this.isStationCreated.set(true);
const { city, latitude, longitude } = this.createStationForm.getRawValue();
this.subscription.add(
this.stationsService
.isStationInCity(city)
.pipe(
switchMap((exists) =>
exists
? of(null)
: this.createStation(city, latitude, longitude).pipe(
map((id) => {
this.isStationCreated.set(false);
this.createStationForm.reset();
this.messageService.add({
severity: MESSAGE_STATUS.SUCCESS,
summary: 'Success!',
detail: `Station created with id: ${id.id}`,
});
}),
),
),
map((exists) => (exists ? of(null) : this.createMarker(city, latitude, longitude))),
)
.subscribe({
error: (error: Error) => {
this.isStationCreated.set(false);
this.messageService.add({ severity: MESSAGE_STATUS.ERROR, summary: error.name, detail: error.message });
},
}),
);
}
}

public ngOnInit(): void {
this.subscription.add(
this.mapService.getLngLat().subscribe((lngLat) => {
this.createStationForm.patchValue({ longitude: lngLat.lng, latitude: lngLat.lat });
}),
);
}

public ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
5 changes: 5 additions & 0 deletions src/app/admin/components/map/map.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="map-wrap">
<div class="map" #map>
<p-skeleton width="100%" height="100%" [hidden]="isMapLoaded()" />
</div>
</div>
16 changes: 16 additions & 0 deletions src/app/admin/components/map/map.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
:host {
display: flex;
}

.map-wrap {
position: relative;
width: 100%;
height: calc(100vh - 4.813rem);
margin-top: 4.813rem;
}

.map {
position: absolute;
width: 100%;
height: 100%;
}
83 changes: 83 additions & 0 deletions src/app/admin/components/map/map.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
inject,
OnDestroy,
OnInit,
signal,
ViewChild,
} from '@angular/core';

import { Map, NavigationControl } from 'maplibre-gl';
import { SkeletonModule } from 'primeng/skeleton';
import { Subscription } from 'rxjs';

import ENVIRONMENTS from '@/environment/environment';

import { INITIAL_MAP_STATE } from '../../constants/initial-map-state';
import { MapService } from '../../services/map/map.service';

@Component({
selector: 'app-map',
standalone: true,
imports: [SkeletonModule],
templateUrl: './map.component.html',
styleUrl: './map.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MapComponent implements OnInit, AfterViewInit, OnDestroy {
private mapService = inject(MapService);
private subscription = new Subscription();
private map!: Map;

@ViewChild('map')
private mapContainer!: ElementRef<HTMLElement>;

public isMapLoaded = signal(false);

private initMap(): void {
this.map = new Map({
container: this.mapContainer.nativeElement,
style: `https://api.maptiler.com/maps/streets-v2/style.json?key=${ENVIRONMENTS.MAP_KEY}`,
center: [INITIAL_MAP_STATE.lng, INITIAL_MAP_STATE.lat],
zoom: INITIAL_MAP_STATE.zoom,
});

this.map.addControl(new NavigationControl({}));
}

private initMapClickHandler(): void {
this.map.on('click', ({ lngLat }) => {
this.mapService.setLngLat({ lng: lngLat.lng, lat: lngLat.lat });
});

this.map.on('load', () => {
this.isMapLoaded.set(true);
});
}

public ngOnInit(): void {
this.subscription.add(
this.mapService.getNewMarker().subscribe((marker) => {
marker.addTo(this.map);
marker.togglePopup();
this.map.flyTo({
center: [marker.getLngLat().lng, marker.getLngLat().lat],
zoom: INITIAL_MAP_STATE.zoom,
});
}),
);
}

public ngAfterViewInit(): void {
this.initMap();
this.initMapClickHandler();
}

public ngOnDestroy(): void {
this.map.remove();
this.subscription.unsubscribe();
}
}
Loading

0 comments on commit eb9a35d

Please sign in to comment.