@@ -55,7 +55,8 @@
|
|
55 |
"maximumError": "4kB"
|
56 |
}
|
57 |
],
|
58 |
-
"outputHashing": "all"
|
|
|
59 |
},
|
60 |
"development": {
|
61 |
"optimization": false,
|
55 |
"maximumError": "4kB"
|
56 |
}
|
57 |
],
|
58 |
+
"outputHashing": "all",
|
59 |
+
"serviceWorker": "ngsw-config.json"
|
60 |
},
|
61 |
"development": {
|
62 |
"optimization": false,
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
3 |
+
"index": "/index.html",
|
4 |
+
"appData": {
|
5 |
+
"version": "1.1.0",
|
6 |
+
"changelog": "Updated Version"
|
7 |
+
},
|
8 |
+
"assetGroups": [
|
9 |
+
{
|
10 |
+
"name": "app",
|
11 |
+
"installMode": "prefetch",
|
12 |
+
"resources": {
|
13 |
+
"files": [
|
14 |
+
"/favicon.ico",
|
15 |
+
"/index.html",
|
16 |
+
"/manifest.webmanifest",
|
17 |
+
"/*.css",
|
18 |
+
"/*.js"
|
19 |
+
]
|
20 |
+
}
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"name": "assets",
|
24 |
+
"installMode": "lazy",
|
25 |
+
"updateMode": "prefetch",
|
26 |
+
"resources": {
|
27 |
+
"files": [
|
28 |
+
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
|
29 |
+
]
|
30 |
+
}
|
31 |
+
}
|
32 |
+
],
|
33 |
+
"dataGroups": [
|
34 |
+
{
|
35 |
+
"name": "Books",
|
36 |
+
"urls": [
|
37 |
+
"/books",
|
38 |
+
"/books/search/**",
|
39 |
+
"/books/**"
|
40 |
+
],
|
41 |
+
"cacheConfig": {
|
42 |
+
"strategy": "freshness",
|
43 |
+
"maxSize": 50,
|
44 |
+
"maxAge": "1d2h",
|
45 |
+
"timeout": "3s"
|
46 |
+
}
|
47 |
+
}
|
48 |
+
]
|
49 |
+
}
|
@@ -20,6 +20,7 @@
|
|
20 |
"@angular/platform-browser": "^18.2.0",
|
21 |
"@angular/platform-browser-dynamic": "^18.2.0",
|
22 |
"@angular/router": "^18.2.0",
|
|
|
23 |
"angular-date-value-accessor": "^3.0.0",
|
24 |
"book-monkey5-styles": "^1.0.4",
|
25 |
"rxjs": "~7.8.0",
|
@@ -42,4 +43,4 @@
|
|
42 |
"typescript": "~5.5.2",
|
43 |
"typescript-eslint": "8.1.0"
|
44 |
}
|
45 |
-
}
|
20 |
"@angular/platform-browser": "^18.2.0",
|
21 |
"@angular/platform-browser-dynamic": "^18.2.0",
|
22 |
"@angular/router": "^18.2.0",
|
23 |
+
"@angular/service-worker": "^18.2.0",
|
24 |
"angular-date-value-accessor": "^3.0.0",
|
25 |
"book-monkey5-styles": "^1.0.4",
|
26 |
"rxjs": "~7.8.0",
|
43 |
"typescript": "~5.5.2",
|
44 |
"typescript-eslint": "8.1.0"
|
45 |
}
|
46 |
+
}
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "book-monkey",
|
3 |
+
"short_name": "book-monkey",
|
4 |
+
"theme_color": "#1976d2",
|
5 |
+
"background_color": "#fafafa",
|
6 |
+
"display": "standalone",
|
7 |
+
"scope": "./",
|
8 |
+
"start_url": "./",
|
9 |
+
"icons": [
|
10 |
+
{
|
11 |
+
"src": "icons/icon-72x72.png",
|
12 |
+
"sizes": "72x72",
|
13 |
+
"type": "image/png",
|
14 |
+
"purpose": "maskable any"
|
15 |
+
},
|
16 |
+
{
|
17 |
+
"src": "icons/icon-96x96.png",
|
18 |
+
"sizes": "96x96",
|
19 |
+
"type": "image/png",
|
20 |
+
"purpose": "maskable any"
|
21 |
+
},
|
22 |
+
{
|
23 |
+
"src": "icons/icon-128x128.png",
|
24 |
+
"sizes": "128x128",
|
25 |
+
"type": "image/png",
|
26 |
+
"purpose": "maskable any"
|
27 |
+
},
|
28 |
+
{
|
29 |
+
"src": "icons/icon-144x144.png",
|
30 |
+
"sizes": "144x144",
|
31 |
+
"type": "image/png",
|
32 |
+
"purpose": "maskable any"
|
33 |
+
},
|
34 |
+
{
|
35 |
+
"src": "icons/icon-152x152.png",
|
36 |
+
"sizes": "152x152",
|
37 |
+
"type": "image/png",
|
38 |
+
"purpose": "maskable any"
|
39 |
+
},
|
40 |
+
{
|
41 |
+
"src": "icons/icon-192x192.png",
|
42 |
+
"sizes": "192x192",
|
43 |
+
"type": "image/png",
|
44 |
+
"purpose": "maskable any"
|
45 |
+
},
|
46 |
+
{
|
47 |
+
"src": "icons/icon-384x384.png",
|
48 |
+
"sizes": "384x384",
|
49 |
+
"type": "image/png",
|
50 |
+
"purpose": "maskable any"
|
51 |
+
},
|
52 |
+
{
|
53 |
+
"src": "icons/icon-512x512.png",
|
54 |
+
"sizes": "512x512",
|
55 |
+
"type": "image/png",
|
56 |
+
"purpose": "maskable any"
|
57 |
+
}
|
58 |
+
]
|
59 |
+
}
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookCreateComponent } from './book-create.component';
|
4 |
+
|
5 |
+
describe('BookCreateComponent', () => {
|
6 |
+
let component: BookCreateComponent;
|
7 |
+
let fixture: ComponentFixture<BookCreateComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookCreateComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookCreateComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookEditComponent } from './book-edit.component';
|
4 |
+
|
5 |
+
describe('BookEditComponent', () => {
|
6 |
+
let component: BookEditComponent;
|
7 |
+
let fixture: ComponentFixture<BookEditComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookEditComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookEditComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookFormComponent } from './book-form.component';
|
4 |
+
|
5 |
+
describe('BookFormComponent', () => {
|
6 |
+
let component: BookFormComponent;
|
7 |
+
let fixture: ComponentFixture<BookFormComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookFormComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookFormComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { FormErrorsComponent } from './form-errors.component';
|
4 |
+
|
5 |
+
describe('FormErrorsComponent', () => {
|
6 |
+
let component: FormErrorsComponent;
|
7 |
+
let fixture: ComponentFixture<FormErrorsComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ FormErrorsComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(FormErrorsComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { AsyncValidatorsService } from './async-validators.service';
|
4 |
+
|
5 |
+
describe('AsyncValidatorsService', () => {
|
6 |
+
let service: AsyncValidatorsService;
|
7 |
+
|
8 |
+
beforeEach(() => {
|
9 |
+
TestBed.configureTestingModule({});
|
10 |
+
service = TestBed.inject(AsyncValidatorsService);
|
11 |
+
});
|
12 |
+
|
13 |
+
it('should be created', () => {
|
14 |
+
expect(service).toBeTruthy();
|
15 |
+
});
|
16 |
+
});
|
@@ -9,6 +9,12 @@
|
|
9 |
<button class="red"
|
10 |
(click)="auth.logout()"
|
11 |
*ngIf="auth.isAuthenticated">Logout</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
</div>
|
13 |
</nav>
|
14 |
|
9 |
<button class="red"
|
10 |
(click)="auth.logout()"
|
11 |
*ngIf="auth.isAuthenticated">Logout</button>
|
12 |
+
<button
|
13 |
+
(click)="requestSubscription()"
|
14 |
+
[class.green]="permission === 'granted'"
|
15 |
+
[class.red]="permission === 'denied'"
|
16 |
+
[disabled]="!permission"
|
17 |
+
aria-label="Notifications">!</button>
|
18 |
</div>
|
19 |
</nav>
|
20 |
|
@@ -1,6 +1,13 @@
|
|
1 |
import { Component } from '@angular/core';
|
|
|
2 |
|
3 |
import { AuthService } from './shared/auth.service';
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
@Component({
|
6 |
selector: 'bm-root',
|
@@ -8,5 +15,62 @@ import { AuthService } from './shared/auth.service';
|
|
8 |
styleUrls: ['./app.component.css']
|
9 |
})
|
10 |
export class AppComponent {
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
}
|
1 |
import { Component } from '@angular/core';
|
2 |
+
import { SwUpdate } from '@angular/service-worker';
|
3 |
|
4 |
import { AuthService } from './shared/auth.service';
|
5 |
+
import { WebNotificationService } from './shared/web-notification.service';
|
6 |
+
|
7 |
+
interface AppData {
|
8 |
+
version: string;
|
9 |
+
changelog: string;
|
10 |
+
}
|
11 |
|
12 |
@Component({
|
13 |
selector: 'bm-root',
|
15 |
styleUrls: ['./app.component.css']
|
16 |
})
|
17 |
export class AppComponent {
|
18 |
+
permission?: NotificationPermission;
|
19 |
+
|
20 |
+
constructor(
|
21 |
+
public auth: AuthService,
|
22 |
+
private swUpdate: SwUpdate,
|
23 |
+
private notificationService: WebNotificationService
|
24 |
+
) {
|
25 |
+
this.swUpdate.versionUpdates.subscribe(e => {
|
26 |
+
switch (e.type) {
|
27 |
+
case 'VERSION_DETECTED': {
|
28 |
+
console.log(
|
29 |
+
'Downloading new app version:',
|
30 |
+
e.version.appData
|
31 |
+
);
|
32 |
+
break;
|
33 |
+
}
|
34 |
+
|
35 |
+
case 'VERSION_READY': {
|
36 |
+
const current = e.currentVersion.appData as AppData;
|
37 |
+
const latest = e.latestVersion.appData as AppData;
|
38 |
+
|
39 |
+
const from = current.version;
|
40 |
+
const to = latest.version;
|
41 |
+
const changes = latest.changelog;
|
42 |
+
|
43 |
+
const confirmText = `Update from ${from} to ${to}. Changes: ${changes}. Install?`;
|
44 |
+
|
45 |
+
if (window.confirm(confirmText)) {
|
46 |
+
window.location.reload();
|
47 |
+
}
|
48 |
+
break;
|
49 |
+
}
|
50 |
+
|
51 |
+
case 'VERSION_INSTALLATION_FAILED': {
|
52 |
+
console.log(
|
53 |
+
`Failed to install ${e.version.appData}:`,
|
54 |
+
e.error
|
55 |
+
);
|
56 |
+
break;
|
57 |
+
}
|
58 |
+
}
|
59 |
+
});
|
60 |
+
|
61 |
+
if (this.notificationService.isEnabled) {
|
62 |
+
this.setPermission();
|
63 |
+
}
|
64 |
+
}
|
65 |
+
|
66 |
+
private setPermission() {
|
67 |
+
if ('Notification' in window) {
|
68 |
+
this.permission = Notification.permission;
|
69 |
+
}
|
70 |
+
}
|
71 |
+
|
72 |
+
requestSubscription() {
|
73 |
+
this.notificationService.requestSubscription()
|
74 |
+
.subscribe(() => this.setPermission());
|
75 |
+
}
|
76 |
}
|
@@ -1,6 +1,7 @@
|
|
1 |
-
import { NgModule } from '@angular/core';
|
2 |
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
3 |
import { BrowserModule } from '@angular/platform-browser';
|
|
|
4 |
|
5 |
import { AppRoutingModule } from './app-routing.module';
|
6 |
import { AppComponent } from './app.component';
|
@@ -18,6 +19,12 @@ import { AuthInterceptor } from './shared/auth.interceptor';
|
|
18 |
BrowserModule,
|
19 |
AppRoutingModule,
|
20 |
HttpClientModule,
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
],
|
22 |
providers: [
|
23 |
{
|
1 |
+
import { NgModule, isDevMode } from '@angular/core';
|
2 |
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
3 |
import { BrowserModule } from '@angular/platform-browser';
|
4 |
+
import { ServiceWorkerModule } from '@angular/service-worker';
|
5 |
|
6 |
import { AppRoutingModule } from './app-routing.module';
|
7 |
import { AppComponent } from './app.component';
|
19 |
BrowserModule,
|
20 |
AppRoutingModule,
|
21 |
HttpClientModule,
|
22 |
+
ServiceWorkerModule.register('ngsw-worker.js', {
|
23 |
+
enabled: !isDevMode(),
|
24 |
+
// Register the ServiceWorker as soon as the application is stable
|
25 |
+
// or after 30 seconds (whichever comes first).
|
26 |
+
registrationStrategy: 'registerWhenStable:30000'
|
27 |
+
})
|
28 |
],
|
29 |
providers: [
|
30 |
{
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookDetailsComponent } from './book-details.component';
|
4 |
+
|
5 |
+
describe('BookDetailsComponent', () => {
|
6 |
+
let component: BookDetailsComponent;
|
7 |
+
let fixture: ComponentFixture<BookDetailsComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookDetailsComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookDetailsComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookListComponent } from './book-list.component';
|
4 |
+
|
5 |
+
describe('BookListComponent', () => {
|
6 |
+
let component: BookListComponent;
|
7 |
+
let fixture: ComponentFixture<BookListComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookListComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookListComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { LoggedinOnlyDirective } from './loggedin-only.directive';
|
2 |
+
|
3 |
+
describe('LoggedinOnlyDirective', () => {
|
4 |
+
it('should create an instance', () => {
|
5 |
+
const directive = new LoggedinOnlyDirective();
|
6 |
+
expect(directive).toBeTruthy();
|
7 |
+
});
|
8 |
+
});
|
@@ -1,4 +1,3 @@
|
|
1 |
-
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
2 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
3 |
|
4 |
import { HomeComponent } from './home.component';
|
@@ -9,8 +8,7 @@ describe('HomeComponent', () => {
|
|
9 |
|
10 |
beforeEach(async () => {
|
11 |
await TestBed.configureTestingModule({
|
12 |
-
declarations: [ HomeComponent ]
|
13 |
-
schemas: [NO_ERRORS_SCHEMA]
|
14 |
})
|
15 |
.compileComponents();
|
16 |
|
|
|
1 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
|
3 |
import { HomeComponent } from './home.component';
|
8 |
|
9 |
beforeEach(async () => {
|
10 |
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ HomeComponent ]
|
|
|
12 |
})
|
13 |
.compileComponents();
|
14 |
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { SearchComponent } from './search.component';
|
4 |
+
|
5 |
+
describe('SearchComponent', () => {
|
6 |
+
let component: SearchComponent;
|
7 |
+
let fixture: ComponentFixture<SearchComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ SearchComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(SearchComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookStoreService } from './book-store.service';
|
4 |
+
|
5 |
+
describe('BookStoreService', () => {
|
6 |
+
let service: BookStoreService;
|
7 |
+
|
8 |
+
beforeEach(() => {
|
9 |
+
TestBed.configureTestingModule({});
|
10 |
+
service = TestBed.inject(BookStoreService);
|
11 |
+
});
|
12 |
+
|
13 |
+
it('should be created', () => {
|
14 |
+
expect(service).toBeTruthy();
|
15 |
+
});
|
16 |
+
});
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Injectable } from '@angular/core';
|
2 |
+
import { HttpClient } from '@angular/common/http';
|
3 |
+
import { SwPush } from '@angular/service-worker';
|
4 |
+
import { concatMap, from, Observable } from 'rxjs';
|
5 |
+
import { Router } from '@angular/router';
|
6 |
+
|
7 |
+
@Injectable({
|
8 |
+
providedIn: 'root',
|
9 |
+
})
|
10 |
+
export class WebNotificationService {
|
11 |
+
readonly VAPID_PUBLIC_KEY = 'BGk2Rx3DEjXdRv9qP8aKrypFoNjISAZ54l-3V05xpPOV-5ZQJvVH9OB9Rz5Ug7H_qH6CEr40f4Pi3DpjzYLbfCA';
|
12 |
+
private baseUrl = 'https://api5.angular-buch.com/notifications';
|
13 |
+
|
14 |
+
constructor(private http: HttpClient, private swPush: SwPush, private router: Router) {
|
15 |
+
// when user clicks a notification
|
16 |
+
this.swPush.notificationClicks.subscribe(e => {
|
17 |
+
// if ISBN is given, navigate to detail page
|
18 |
+
const data = e.notification.data;
|
19 |
+
if (data?.book?.isbn) {
|
20 |
+
this.router.navigate(['/books', data.book.isbn])
|
21 |
+
}
|
22 |
+
});
|
23 |
+
}
|
24 |
+
|
25 |
+
get isEnabled() {
|
26 |
+
return this.swPush.isEnabled;
|
27 |
+
}
|
28 |
+
|
29 |
+
// request a subscription from the browser and register it on the server
|
30 |
+
requestSubscription(): Observable<unknown> {
|
31 |
+
const request = this.swPush.requestSubscription({
|
32 |
+
serverPublicKey: this.VAPID_PUBLIC_KEY
|
33 |
+
});
|
34 |
+
|
35 |
+
return from(request).pipe(
|
36 |
+
concatMap(sub => this.registerOnServer(sub))
|
37 |
+
);
|
38 |
+
}
|
39 |
+
|
40 |
+
private registerOnServer(params: PushSubscription) {
|
41 |
+
return this.http.post(this.baseUrl, params);
|
42 |
+
}
|
43 |
+
}
|
@@ -6,10 +6,18 @@
|
|
6 |
<base href="/">
|
7 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
8 |
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
</head>
|
10 |
<body>
|
11 |
<bm-root>
|
12 |
<div class="loader">Loading ...</div>
|
13 |
</bm-root>
|
|
|
14 |
</body>
|
15 |
</html>
|
6 |
<base href="/">
|
7 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
8 |
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
9 |
+
<link rel="manifest" href="manifest.webmanifest">
|
10 |
+
<meta name="theme-color" content="#1976d2">
|
11 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
12 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
13 |
+
<link rel="apple-touch-startup-image" href="assets/icons/apple-splash-screen.png">
|
14 |
+
<link rel="apple-touch-icon" href="assets/icons/icon-512x512.png">
|
15 |
+
<link rel="apple-touch-icon" sizes="152x152" href="assets/icons/icon-152x152.png">
|
16 |
</head>
|
17 |
<body>
|
18 |
<bm-root>
|
19 |
<div class="loader">Loading ...</div>
|
20 |
</bm-root>
|
21 |
+
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
22 |
</body>
|
23 |
</html>
|