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

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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"develop": "ng serve --proxy-config proxy.json",
"develop": "ng serve --port 5600 --proxy-config proxy.conf.js",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"build:gh": "ng build --base-href /angular-http-project/",
Expand Down
25 changes: 25 additions & 0 deletions proxy.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// var defaultTarget = 'http://localhost:3000';
var defaultTarget = 'https://3000-actionanand-angularhttp-six5y8k89a8.ws-us116.gitpod.io';

module.exports = [
{
context: ['/api/v2/*'],
target: defaultTarget,
secure: false,
logLevel: 'debug',
changeOrigin: true,
pathRewrite: {
'^/api/v2/': '/',
},
},
{
context: ['/api/v2/**'],
target: defaultTarget,
secure: false,
logLevel: 'debug',
changeOrigin: true,
pathRewrite: {
'^/api/v2/': '/',
},
},
];
6 changes: 3 additions & 3 deletions proxy.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
{
"/api/v2/*": {
"target": "https://3000-actionanand-angularhttp-six5y8k89a8.ws-us116.gitpod.io",
"target": "http://localhost:3000",
"secure": false,
"logLevel": "debug",
"changeOrigin": true,
"pathRewrite": {
"^/api/v2/": "/"
}
},
"/api/v1/*": {
"/api/v2/**": {
"target": "http://localhost:3000",
"secure": false,
"logLevel": "debug",
"changeOrigin": true,
"pathRewrite": {
"^/api/v1/": "/"
"^/api/v2/": "/"
}
}
}
9 changes: 5 additions & 4 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
@if (error()) {
<app-error-modal title="An Error has occurred!" [message]="error()" />
}

<header>
<img src="logo.png" alt="Stylized globe" />
<h1>PlacePicker</h1>
<p>
Create your personal collection of places you would like to visit or you
have visited.
</p>
<p>Create your personal collection of places you would like to visit or you have visited.</p>
</header>

<main>
Expand Down
10 changes: 7 additions & 3 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { Component } from '@angular/core';
import { Component, inject } from '@angular/core';

import { AvailablePlacesComponent } from './places/available-places/available-places.component';
import { UserPlacesComponent } from './places/user-places/user-places.component';
import { ErrorService } from './services/error.service';
import { ErrorModalComponent } from './shared/modal/error-modal/error-modal.component';

@Component({
selector: 'app-root',
standalone: true,
templateUrl: './app.component.html',
styleUrl: './app.component.css',
imports: [AvailablePlacesComponent, UserPlacesComponent],
imports: [AvailablePlacesComponent, UserPlacesComponent, ErrorModalComponent],
})
export class AppComponent {
styles = ['color: indigo', 'background: #90EE90', 'font-weight: bold', 'font-size: 18px'].join(';');
private styles = ['color: indigo', 'background: #90EE90', 'font-weight: bold', 'font-size: 18px'].join(';');
private errorServ = inject(ErrorService);
protected error = this.errorServ.error;

constructor() {
console.log(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class AvailablePlacesComponent implements OnInit {
}

onSelectPlaces(selectedPlace: Place) {
const selectPlaceSub = this.placeServ.addPlaceToUserPlaces(selectedPlace.id).subscribe({
const selectPlaceSub = this.placeServ.addPlaceToUserPlaces(selectedPlace).subscribe({
next: resp => console.log('Place added. ', resp),
});

Expand Down
4 changes: 2 additions & 2 deletions src/app/places/user-places/user-places.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
}

@if (places()) {
<app-places [places]="places()!" />
} @else if (places()?.length === 0) {
<app-places [places]="places()!" (selectPlace)="onSelectPlaceRemove($event)" />
} @else if (places().length === 0) {
<p class="fallback-text">Unfortunately, no places could be found.</p>
}
</app-places-container>
17 changes: 11 additions & 6 deletions src/app/places/user-places/user-places.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core';

import { PlacesContainerComponent } from '../places-container/places-container.component';
import { PlacesComponent } from '../places.component';
import { Place } from '../../models/place.model';
import { PlacesService } from '../../services/places.service';
import { Place } from '../../models/place.model';

@Component({
selector: 'app-user-places',
Expand All @@ -13,21 +13,18 @@ import { PlacesService } from '../../services/places.service';
imports: [PlacesContainerComponent, PlacesComponent],
})
export class UserPlacesComponent implements OnInit {
places = signal<Place[] | undefined>(undefined);
isFetching = signal(false);
errorMsg = signal('');

private placeServ = inject(PlacesService);
private destroyRef = inject(DestroyRef);

places = this.placeServ.loadedUserPlaces;

ngOnInit(): void {
this.isFetching.set(true);

const availablePlaceSub = this.placeServ.loadUserPlaces().subscribe({
next: resp => {
this.places.set(resp?.places);
this.errorMsg.set('');
},
complete: () => {
this.isFetching.set(false);
},
Expand All @@ -39,4 +36,12 @@ export class UserPlacesComponent implements OnInit {

this.destroyRef.onDestroy(() => availablePlaceSub.unsubscribe());
}

onSelectPlaceRemove(place: Place) {
const removePlSub = this.placeServ.removeUserPlace(place).subscribe({
next: resp => console.log('Place renoved! ', resp),
});

this.destroyRef.onDestroy(() => removePlSub.unsubscribe());
}
}
19 changes: 19 additions & 0 deletions src/app/services/error.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Injectable, signal } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class ErrorService {
private _error = signal('');

error = this._error.asReadonly();

showError(message: string) {
console.error(message);
this._error.set(message);
}

clearError() {
this._error.set('');
}
}
52 changes: 46 additions & 6 deletions src/app/services/places.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { catchError, map, tap } from 'rxjs/operators';
import { throwError } from 'rxjs';

import { Place } from '../models/place.model';
import { ErrorService } from './error.service';

@Injectable({
providedIn: 'root',
Expand All @@ -15,22 +16,61 @@ export class PlacesService {
loadedUserPlaces = this.userPlaces.asReadonly();

private http = inject(HttpClient);
private errorServ = inject(ErrorService);

loadAvailablePlaces() {
return this.fetchPlaces('/api/v2/places', 'Error loading available places!');
}

loadUserPlaces() {
return this.fetchPlaces('/api/v2/user-places', 'Error loading user places!');
return this.fetchPlaces('/api/v2/user-places', 'Error loading user places!').pipe(
tap({
next: resp => {
if (resp) {
this.userPlaces.set(resp.places);
}
},
}),
);
}

addPlaceToUserPlaces(placeId: string) {
return this.http.put('/api/v2/user-places', {
placeId,
});
addPlaceToUserPlaces(place: Place) {
const prevPlaces = this.userPlaces();

if (!prevPlaces.some(p => p.id === place.id)) {
// optimistic update
this.userPlaces.set([...prevPlaces, place]);
}

return this.http
.put('/api/v2/user-places', {
placeId: place.id,
})
.pipe(
catchError(err => {
this.userPlaces.set(prevPlaces);
this.errorServ.showError('Unable to store the selected place!');
return throwError(() => new Error('Unable to store the selected place!'));
}),
);
}

removeUserPlace(place: Place) {}
removeUserPlace(place: Place) {
const prevPlaces = this.userPlaces();

if (prevPlaces.some(p => p.id === place.id)) {
// optimistic update
this.userPlaces.set(prevPlaces.filter(pl => pl.id !== place.id));
}

return this.http.delete('/api/v2/user-places/' + place.id).pipe(
catchError(err => {
this.userPlaces.set(prevPlaces);
this.errorServ.showError('Unable to remove the selected place!');
return throwError(() => new Error('Unable to remove the selected place!'));
}),
);
}

private fetchPlaces(url: string, errMsg: string) {
return this.http
Expand Down
31 changes: 31 additions & 0 deletions src/app/shared/modal/error-modal/error-modal.component.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.error {
max-width: 40rem;
margin: auto;
padding: 1rem;
background-color: #f4c7c7;
color: #3e0505;
}

.confirmation-actions {
margin-top: 1rem;
display: flex;
justify-content: flex-end;
gap: 1rem;
}

button {
cursor: pointer;
font-family: 'Raleway', sans-serif;
font-size: 1rem;
padding: 0.5rem 1.5rem;
border: none;
border-radius: 4px;
background-color: #5d0909;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
color: #fff;
}

button:hover,
button:focus {
background-color: #3e0505;
}
10 changes: 10 additions & 0 deletions src/app/shared/modal/error-modal/error-modal.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<app-modal>
<div class="error">
<h2>{{ title() }}</h2>
<p>{{ message() }}</p>

<div class="confirmation-actions">
<button class="button" (click)="onClearError()">Okay</button>
</div>
</div>
</app-modal>
21 changes: 21 additions & 0 deletions src/app/shared/modal/error-modal/error-modal.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Component, inject, input } from '@angular/core';
import { ModalComponent } from '../modal.component';
import { ErrorService } from '../../../services/error.service';

@Component({
selector: 'app-error-modal',
standalone: true,
templateUrl: './error-modal.component.html',
styleUrl: './error-modal.component.css',
imports: [ModalComponent],
})
export class ErrorModalComponent {
title = input<string>();
message = input<string>();

private errorServ = inject(ErrorService);

onClearError() {
this.errorServ.clearError();
}
}
31 changes: 31 additions & 0 deletions src/app/shared/modal/modal.component.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
dialog {
min-width: 30rem;
padding: 0;
z-index: 2;
background: #d5c7bc;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
animation: slide-down-fade-in 0.3s ease-out forwards;
}

dialog::backdrop {
position: fixed;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.6);
}

@keyframes slide-down-fade-in {
from {
opacity: 0;
transform: translateY(-3rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
3 changes: 3 additions & 0 deletions src/app/shared/modal/modal.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<dialog #dialog>
<ng-content />
</dialog>
16 changes: 16 additions & 0 deletions src/app/shared/modal/modal.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AfterViewInit, Component, ElementRef, viewChild } from '@angular/core';

@Component({
selector: 'app-modal',
standalone: true,
imports: [],
templateUrl: './modal.component.html',
styleUrl: './modal.component.css',
})
export class ModalComponent implements AfterViewInit {
private dialogEl = viewChild.required<ElementRef<HTMLDialogElement>>('dialog');

ngAfterViewInit(): void {
this.dialogEl().nativeElement.showModal();
}
}
1 change: 1 addition & 0 deletions src/environments/environment.development.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const environment = {
backendUrl: 'https://3000-actionanand-angularhttp-six5y8k89a8.ws-us116.gitpod.io/',
// backendUrl: 'http://localhost:3000/',
};
2 changes: 1 addition & 1 deletion src/environments/environment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const environment = {
backendUrl: 'https://3000-actionanand-angularhttp-six5y8k89a8.ws-us116.gitpod.io/',
backendUrl: 'http://localhost:3000/',
};
Loading