Add bookmarks
This commit is contained in:
parent
587e231e05
commit
9c3f70d36b
@ -4,12 +4,14 @@ import {HomeComponent} from './containers/home/home.component';
|
||||
import {NotfoundComponent} from './containers/notfound/notfound.component';
|
||||
import {SearchComponent} from './containers/search/search.component';
|
||||
import {RegionDetailsComponent} from './containers/region-details/region-details.component';
|
||||
import {BookmarkListComponent} from './containers/bookmark-list/bookmark-list.component';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{path: 'home', component: HomeComponent},
|
||||
{path: 'search', component: SearchComponent},
|
||||
{path: 'region/:id', component: RegionDetailsComponent},
|
||||
{path: 'bookmark', component: BookmarkListComponent},
|
||||
{path: '', redirectTo: 'home', pathMatch: 'full'},
|
||||
{path: '**', component: NotfoundComponent}
|
||||
];
|
||||
|
||||
@ -9,10 +9,16 @@
|
||||
<div class="nav">
|
||||
<a mat-button routerLink="home" (click)="drawer.close()">
|
||||
<mat-icon>home</mat-icon>
|
||||
<span>Home</span></a>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a (click)="drawer.close()" mat-button routerLink="bookmark">
|
||||
<mat-icon>bookmark</mat-icon>
|
||||
<span>Your bookmarks</span>
|
||||
</a>
|
||||
<a mat-button routerLink="impressum" (click)="drawer.close()">
|
||||
<mat-icon>subject</mat-icon>
|
||||
<span>Impressum</span></a>
|
||||
<span>Impressum</span>
|
||||
</a>
|
||||
</div>
|
||||
</mat-drawer>
|
||||
<div class="routed-component-wrapper">
|
||||
|
||||
@ -21,13 +21,15 @@ import {TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
// @ts-ignore
|
||||
import * as enLang from '../assets/i18n/en.json';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {MatButtonToggleModule, MatCheckboxModule, MatDividerModule} from '@angular/material';
|
||||
import {MatButtonToggleModule, MatCheckboxModule, MatDividerModule, MatTooltipModule} from '@angular/material';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {RegionComponent} from './components/region/region.component';
|
||||
import {ResultComponent} from './components/result/result.component';
|
||||
import {RegionDetailsComponent} from './containers/region-details/region-details.component';
|
||||
import {GraphComponent} from './components/graph/graph.component';
|
||||
import {RegionStatsComponent} from './components/region-stats/region-stats.component';
|
||||
import {BookmarkButtonComponent} from './components/bookmark-button/bookmark-button.component';
|
||||
import {BookmarkListComponent} from './containers/bookmark-list/bookmark-list.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@ -41,7 +43,9 @@ import {RegionStatsComponent} from './components/region-stats/region-stats.compo
|
||||
ResultComponent,
|
||||
RegionDetailsComponent,
|
||||
GraphComponent,
|
||||
RegionStatsComponent
|
||||
RegionStatsComponent,
|
||||
BookmarkButtonComponent,
|
||||
BookmarkListComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@ -61,7 +65,8 @@ import {RegionStatsComponent} from './components/region-stats/region-stats.compo
|
||||
MatCheckboxModule,
|
||||
FormsModule,
|
||||
MatButtonToggleModule,
|
||||
MatDividerModule
|
||||
MatDividerModule,
|
||||
MatTooltipModule
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
<button (click)="onToggle($event)"
|
||||
[color]="isBookmarked ? 'accent' : 'primary'"
|
||||
mat-icon-button
|
||||
matTooltip="{{isBookmarked ? 'Remove from bookmarks' : 'Add to bookmarks'}}"
|
||||
>
|
||||
<mat-icon>{{isBookmarked ? 'bookmark' : 'bookmark_border'}}</mat-icon>
|
||||
</button>
|
||||
@ -0,0 +1,25 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {BookmarkButtonComponent} from './bookmark-button.component';
|
||||
|
||||
describe('BookmarkButtonComponent', () => {
|
||||
let component: BookmarkButtonComponent;
|
||||
let fixture: ComponentFixture<BookmarkButtonComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BookmarkButtonComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BookmarkButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,40 @@
|
||||
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Region} from '../../interfaces/region.interface';
|
||||
import {BookmarkService} from '../../services/bookmark.service';
|
||||
import {Subject} from 'rxjs';
|
||||
import {takeUntil} from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bookmark-button',
|
||||
templateUrl: './bookmark-button.component.html',
|
||||
styleUrls: ['./bookmark-button.component.scss']
|
||||
})
|
||||
export class BookmarkButtonComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input()
|
||||
region: Region;
|
||||
|
||||
isBookmarked = false;
|
||||
|
||||
private destroyed$ = new Subject<void>();
|
||||
|
||||
constructor(private bs: BookmarkService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.bs.isMarked(this.region.region_id).pipe(
|
||||
takeUntil(this.destroyed$)
|
||||
).subscribe(val => this.isBookmarked = val);
|
||||
}
|
||||
|
||||
onToggle(event: Event) {
|
||||
event.stopPropagation();
|
||||
this.bs.toggleRegion(this.region.region_id);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,13 +1,12 @@
|
||||
<div class="region-mat-card">
|
||||
<img class="region-img" src="https://travopti.de/api/v1/regions/{{region.region_id}}/image">
|
||||
<img alt="Picture of {{region.name}}" class="region-img"
|
||||
src="https://travopti.de/api/v1/regions/{{region.region_id}}/image">
|
||||
<div class="region-footer">
|
||||
<div class="region-title">
|
||||
<span class="region-name">{{region.name}}</span>
|
||||
<span class="region-country">| {{region.country}}</span>
|
||||
</div>
|
||||
<button mat-icon-button>
|
||||
<mat-icon>bookmark</mat-icon>
|
||||
</button>
|
||||
<app-bookmark-button [region]="region"></app-bookmark-button>
|
||||
<button mat-icon-button>
|
||||
<mat-icon>share</mat-icon>
|
||||
</button>
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
<div class="result-mat-card">
|
||||
<img class="result-img" src="https://travopti.de/api/v1/regions/{{result.region_id}}/image">
|
||||
<img alt="Picture of {{result.name}}" class="result-img"
|
||||
src="https://travopti.de/api/v1/regions/{{result.region_id}}/image">
|
||||
<div class="result-title">
|
||||
<div class="result-name">
|
||||
<span class="result-name">{{result.name}}</span>
|
||||
<span class="result-country">| {{result.country}}</span>
|
||||
</div>
|
||||
<button mat-icon-button>
|
||||
<mat-icon>bookmark</mat-icon>
|
||||
</button>
|
||||
<app-bookmark-button [region]="result"></app-bookmark-button>
|
||||
<button mat-icon-button>
|
||||
<mat-icon>share</mat-icon>
|
||||
</button>
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
<h2>Your bookmarks</h2>
|
||||
<div *ngIf="!isLoading" class="bookmarks-contianer">
|
||||
<app-region (click)="onBookmarkClick(bookmark)" *ngFor="let bookmark of bookmarks" [region]="bookmark"></app-region>
|
||||
<span *ngIf="bookmarks.length === 0">You have no bookmarks :(</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isLoading" class="spinner">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bookmarks-contianer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
> * {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
|
||||
import {BookmarkListComponent} from './bookmark-list.component';
|
||||
|
||||
describe('BookmarkListComponent', () => {
|
||||
let component: BookmarkListComponent;
|
||||
let fixture: ComponentFixture<BookmarkListComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BookmarkListComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BookmarkListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,46 @@
|
||||
import {Component, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Region} from '../../interfaces/region.interface';
|
||||
import {BookmarkService} from '../../services/bookmark.service';
|
||||
import {DataService} from '../../services/data.service';
|
||||
import {switchMap, takeUntil, tap} from 'rxjs/operators';
|
||||
import {Router} from '@angular/router';
|
||||
import {Subject} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bookmark-list',
|
||||
templateUrl: './bookmark-list.component.html',
|
||||
styleUrls: ['./bookmark-list.component.scss']
|
||||
})
|
||||
export class BookmarkListComponent implements OnInit, OnDestroy {
|
||||
|
||||
bookmarks: Region[];
|
||||
isLoading = true;
|
||||
private destroyed$ = new Subject<void>();
|
||||
|
||||
constructor(private bs: BookmarkService, private ds: DataService, private router: Router) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.bs.getRegionIds().pipe(
|
||||
takeUntil(this.destroyed$),
|
||||
tap(() => this.isLoading = true),
|
||||
switchMap(ids => {
|
||||
const regions: Promise<Region>[] = ids.map(id => this.ds.getRegion(id));
|
||||
return Promise.all(regions);
|
||||
})
|
||||
).subscribe(regions => {
|
||||
this.bookmarks = regions;
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
onBookmarkClick(region: Region) {
|
||||
this.router.navigate(['/region', region.region_id]).catch(console.log);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed$.next();
|
||||
this.destroyed$.complete();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,13 +1,12 @@
|
||||
<div *ngIf="region">
|
||||
<img class="region-img" src="https://travopti.de/api/v1/regions/{{region.region_id}}/image">
|
||||
<img alt="Picture of {{region.name}}" class="region-img"
|
||||
src="https://travopti.de/api/v1/regions/{{region.region_id}}/image">
|
||||
<div class="region-details-header">
|
||||
<div class="region-title">
|
||||
<span class="region-country">{{region.country}}</span>
|
||||
<span class="region-name">{{region.name}}</span>
|
||||
</div>
|
||||
<button mat-icon-button>
|
||||
<mat-icon>bookmark</mat-icon>
|
||||
</button>
|
||||
<app-bookmark-button [region]="region"></app-bookmark-button>
|
||||
<button mat-icon-button>
|
||||
<mat-icon>share</mat-icon>
|
||||
</button>
|
||||
|
||||
12
frontend/src/app/services/bookmark.service.spec.ts
Normal file
12
frontend/src/app/services/bookmark.service.spec.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {BookmarkService} from './bookmark.service';
|
||||
|
||||
describe('BookmarkService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: BookmarkService = TestBed.get(BookmarkService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
63
frontend/src/app/services/bookmark.service.ts
Normal file
63
frontend/src/app/services/bookmark.service.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {base64ToObj, objToBase64} from '../utils/base64conversion';
|
||||
import {map} from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BookmarkService {
|
||||
|
||||
private readonly LOCAL_STORAGE_KEY = 'bookmarks';
|
||||
|
||||
private readonly storage: Storage;
|
||||
|
||||
private readonly regionIds$: BehaviorSubject<number[]> = new BehaviorSubject<number[]>([]);
|
||||
|
||||
constructor() {
|
||||
this.storage = localStorage;
|
||||
this.load();
|
||||
}
|
||||
|
||||
public addRegion(regionId: number) {
|
||||
const regions = this.regionIds$.getValue();
|
||||
regions.push(regionId);
|
||||
this.regionIds$.next(regions);
|
||||
this.save();
|
||||
}
|
||||
|
||||
public removeRegion(regionId: number) {
|
||||
let regions = this.regionIds$.getValue();
|
||||
regions = regions.filter(id => id !== regionId);
|
||||
this.regionIds$.next(regions);
|
||||
this.save();
|
||||
}
|
||||
|
||||
public toggleRegion(regionId: number) {
|
||||
if (this.regionIds$.getValue().includes(regionId)) {
|
||||
this.removeRegion(regionId);
|
||||
} else {
|
||||
this.addRegion(regionId);
|
||||
}
|
||||
}
|
||||
|
||||
public getRegionIds(): BehaviorSubject<number[]> {
|
||||
return this.regionIds$;
|
||||
}
|
||||
|
||||
public isMarked(regionId: number): Observable<boolean> {
|
||||
return this.regionIds$.pipe(
|
||||
map(ids => ids.includes(regionId))
|
||||
);
|
||||
}
|
||||
|
||||
private load() {
|
||||
const regionIds = base64ToObj(this.storage.getItem(this.LOCAL_STORAGE_KEY)) as number[];
|
||||
|
||||
this.regionIds$.next(regionIds || []);
|
||||
}
|
||||
|
||||
private save() {
|
||||
this.storage.setItem(this.LOCAL_STORAGE_KEY, objToBase64(this.regionIds$.getValue()));
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,8 @@ export class DataService {
|
||||
|
||||
private readonly API_URL = 'https://travopti.de/api/v1';
|
||||
|
||||
private readonly regionCache = new Map<number, Region>();
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
}
|
||||
|
||||
@ -34,8 +36,12 @@ export class DataService {
|
||||
// });
|
||||
}
|
||||
|
||||
public getAllRegions(): Promise<Region[]> {
|
||||
return this.http.get<Region[]>(this.API_URL + '/regions').toPromise();
|
||||
public async getAllRegions(): Promise<Region[]> {
|
||||
const regions = await this.http.get<Region[]>(this.API_URL + '/regions').toPromise();
|
||||
|
||||
regions.forEach(region => this.regionCache.set(region.region_id, region));
|
||||
|
||||
return regions;
|
||||
// return new Promise<Region[]>(resolve => {
|
||||
// setTimeout(() => {
|
||||
// resolve(MOCK_REGIONS);
|
||||
@ -43,8 +49,16 @@ export class DataService {
|
||||
// });
|
||||
}
|
||||
|
||||
public getRegion(id: number): Promise<Region> {
|
||||
return this.http.get<Region>(`${this.API_URL}/regions/${id}`).toPromise();
|
||||
public async getRegion(id: number): Promise<Region> {
|
||||
if (this.regionCache.has(id)) {
|
||||
console.log(`Served region ${id} from cache`);
|
||||
return this.regionCache.get(id);
|
||||
}
|
||||
|
||||
const region = await this.http.get<Region>(`${this.API_URL}/regions/${id}`).toPromise();
|
||||
this.regionCache.set(id, region);
|
||||
|
||||
return region;
|
||||
// return new Promise<Region>(resolve => {
|
||||
// setTimeout(() => {
|
||||
// resolve(MOCK_REGIONS.find(region => region.region_id === id));
|
||||
|
||||
@ -11,5 +11,9 @@ export function objToBase64(obj: object): string {
|
||||
* @param base64 Encoded object
|
||||
*/
|
||||
export function base64ToObj(base64: string): object {
|
||||
if (!base64) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return JSON.parse(atob(base64));
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"cheap_water": "Cheap water",
|
||||
"cheap_transportations": "Cheap public transport",
|
||||
"cheap_entertainment": "Cheap entertainment",
|
||||
"cheap_accommodation": "Cheap accommodation",
|
||||
"warm": "warm",
|
||||
"chilly": "cold",
|
||||
"mild:": "mild",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user