Skip to content

Commit 99bb71b

Browse files
committed
Add authToken to External tutorial
1 parent 406dc45 commit 99bb71b

File tree

10 files changed

+262
-13
lines changed

10 files changed

+262
-13
lines changed

cloudapp/src/app/app.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { TranslateComponent } from './translate/translate.component';
2222
import { ConfigurationComponent } from './configuration/configuration.component';
2323
import { MultiSelectComponent } from './multi-select/multi-select.component';
2424
import { SelectEntitiesComponent } from './multi-select/select-entities/select-entities.component';
25+
import { LightboxComponent } from './external/lightbox/lightbox.component'
2526

2627
export function getToastrModule() {
2728
return ToastrModule.forRoot({
@@ -46,7 +47,8 @@ export function getToastrModule() {
4647
TranslateComponent,
4748
ConfigurationComponent,
4849
MultiSelectComponent,
49-
SelectEntitiesComponent
50+
SelectEntitiesComponent,
51+
LightboxComponent
5052
],
5153
imports: [
5254
MaterialModule,

cloudapp/src/app/external/external.component.html

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<form>
2+
<h3>Hathitrust Bibliographic API</h3>
3+
<p>This API is open and does not require authentication. Enter an identifier and select an identifier type to search the API.</p>
24
<section class="settings-section">
35
<mat-form-field>
46
<mat-label>Identifier Type</mat-label>
@@ -22,7 +24,7 @@
2224

2325
<div class="commands-container">
2426
<button mat-stroked-button type="button" color="primary" (click)="search(identifierType.value, identifier.value)">Search</button>
25-
<mat-spinner diameter="30" class="spinner" *ngIf="running"></mat-spinner>
27+
<mat-spinner diameter="30" class="spinner" *ngIf="running.search"></mat-spinner>
2628
</div>
2729

2830
<mat-card *ngIf="record">
@@ -41,4 +43,29 @@
4143
</li>
4244
</ul>
4345
</mat-card-content>
44-
</mat-card>
46+
</mat-card>
47+
48+
<section *ngIf="authToken">
49+
<h3>Hathitrust Data API</h3>
50+
<p>This API is restricted and requires authentication.</p>
51+
<section class="settings-section">
52+
<mat-form-field>
53+
<mat-label>Identifier</mat-label>
54+
<input matInput #data_api_identifier value="mdp.39015000000128">
55+
</mat-form-field>
56+
</section>
57+
<div class="commands-container">
58+
<button mat-stroked-button type="button" color="primary" (click)="dataApi(data_api_identifier.value)">Retrieve Metadata</button>
59+
<mat-spinner diameter="30" class="spinner" *ngIf="running.data"></mat-spinner>
60+
</div>
61+
<mat-card *ngIf="images.length>0">
62+
<mat-card-header>
63+
<mat-card-title>HathiTrust Page Scans</mat-card-title>
64+
</mat-card-header>
65+
<mat-card-content>
66+
<p>Retrieved <strong>{{images.length}}</strong> pages from the API for this title.</p>
67+
<button mat-stroked-button type="button" color="primary" (click)="openModal()">View images</button>
68+
</mat-card-content>
69+
</mat-card>
70+
</section>
71+
<app-lightbox></app-lightbox>

cloudapp/src/app/external/external.component.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@
1616

1717
li {
1818
margin-bottom: 5px;
19+
}
20+
21+
mat-card {
22+
margin-bottom: 20px
1923
}
Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,41 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component, OnInit, ViewChild } from '@angular/core';
22
import { AppService } from '../app.service';
33
import { HttpClient } from '@angular/common/http';
44
import { map, finalize } from 'rxjs/operators';
55
import { ToastrService } from 'ngx-toastr';
6+
import { environment } from '../../environments/environment';
7+
import { LightboxComponent } from './lightbox/lightbox.component';
8+
import { CloudAppEventsService } from '@exlibris/exl-cloudapp-angular-lib';
69

710
@Component({
811
selector: 'app-external',
912
templateUrl: './external.component.html',
1013
styleUrls: ['./external.component.scss']
1114
})
1215
export class ExternalComponent implements OnInit {
13-
running = false;
16+
@ViewChild(LightboxComponent, {static: true}) lightbox: LightboxComponent;
17+
running = { search: false, data: false };
1418
record: any;
19+
images: Array<string> = [];
20+
authToken: string;
1521

1622
constructor(
1723
private appService: AppService,
24+
private eventsService: CloudAppEventsService,
1825
private http: HttpClient,
1926
private toastr: ToastrService
2027
) { }
2128

2229
ngOnInit() {
2330
this.appService.setTitle('Reaching out');
31+
this.eventsService.getAuthToken()
32+
.subscribe(authToken => this.authToken = authToken);
2433
}
2534

2635
search(identifierType: string, identifier: string) {
27-
this.running = true;
36+
this.running.search = true;
2837
this.record = null;
29-
this.http.get<any>(`https://catalog.hathitrust.org/api/volumes/brief/${identifierType}/${identifier}.json`)
38+
this.http.get<any>(hathitrustSearchUrl(identifierType, identifier))
3039
.pipe(
3140
map(res => {
3241
if (Object.keys(res.records).length == 0) {
@@ -36,11 +45,39 @@ export class ExternalComponent implements OnInit {
3645
{ id: Object.keys(res.records)[0], ...Object.values(res.records)[0] })
3746
}
3847
}),
39-
finalize(() => this.running = false)
48+
finalize(() => this.running.search = false)
4049
)
41-
.subscribe( {
42-
next: response=>this.record = response,
43-
error: (e) => this.toastr.error(e.message)
50+
.subscribe({
51+
next: resp => this.record = resp,
52+
error: e => this.toastr.error(e.message)
4453
});
4554
}
55+
56+
dataApi(id: string) {
57+
const headers = {'Authorization': `Bearer ${this.authToken}` };
58+
this.lightbox.headers = headers;
59+
this.running.data = true;
60+
this.http.get<any>(hathitrustMetaUrl(id), { headers } ).pipe(
61+
map( resp => {
62+
if (resp['htd:seqmap'] && resp['htd:seqmap'][0] && resp['htd:seqmap'][0]['htd:seq']) {
63+
const seqmap: Array<any> = resp['htd:seqmap'][0]['htd:seq'];
64+
return seqmap.map( s => hathitrustImageUrl(id, s.pseq) );
65+
}
66+
}),
67+
finalize(() => this.running.data = false)
68+
).subscribe({
69+
next: resp => this.images = resp,
70+
error: e => this.toastr.error(e.message)
71+
})
72+
}
73+
74+
openModal() {
75+
this.lightbox.openModal();
76+
this.lightbox.images = this.images;
77+
this.lightbox.currentSlide(1);
78+
}
4679
}
80+
81+
const hathitrustMetaUrl = (id: string) => `${environment.hathitrustProxy}/volume/meta/${id}?v=2&format=json`;
82+
const hathitrustImageUrl = (id: string, pseq: string) => `${environment.hathitrustProxy}/volume/pageimage/${id}/${pseq}?v=2&format=png&res=2`;
83+
const hathitrustSearchUrl = (identifierType: string, identifier: string) => `${environment.hathitrustUrl}/api/volumes/brief/${identifierType}/${identifier}.json`;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<div id="imgModal" class="slide-modal">
2+
<span class="image-counter">{{slideIndex}} of {{images?.length}}</span>
3+
<span class="close cursor" (click)="closeModal()">&times;</span>
4+
<div class="slide-modal-content">
5+
<div id="imgPane">
6+
<img src="{{src}}" *ngIf="!loading">
7+
</div>
8+
<mat-spinner diameter="75" class="spinner" *ngIf="loading" color="accent"></mat-spinner>
9+
<a class="prev" (click)="nextImage(-1)">&#10094;</a>
10+
<a class="next" (click)="nextImage(1)">&#10095;</a>
11+
</div>
12+
</div>
13+
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
img {
2+
width: 100%;
3+
}
4+
5+
.slide-modal {
6+
display: none;
7+
position: fixed;
8+
z-index: 1;
9+
padding-top: 10px;
10+
left: 0;
11+
top: 0;
12+
width: 100%;
13+
height: 100%;
14+
overflow: auto;
15+
background-color: #000000;
16+
}
17+
18+
.slide-modal-content {
19+
position: relative;
20+
margin: auto;
21+
padding: 0;
22+
width: 100%;
23+
max-width: 1200px;
24+
height: 95%
25+
}
26+
27+
.close {
28+
color: white;
29+
position: absolute;
30+
top: 10px;
31+
right: 25px;
32+
font-size: 35px;
33+
font-weight: bold;
34+
}
35+
36+
.close:hover,
37+
.close:focus {
38+
color: #888888;
39+
text-decoration: none;
40+
cursor: pointer;
41+
}
42+
43+
.image-counter {
44+
color:white;
45+
margin-left: 10px;
46+
}
47+
48+
.cursor {
49+
cursor: pointer;
50+
}
51+
52+
.prev,
53+
.next {
54+
cursor: pointer;
55+
position: absolute;
56+
top: 35%;
57+
width: auto;
58+
padding: 18px;
59+
margin-top: -50px;
60+
color: white !important;
61+
font-weight: bold;
62+
font-size: 20px;
63+
transition: 0.8s ease;
64+
border-radius: 0 5px 5px 0;
65+
user-select: none;
66+
-webkit-user-select: none;
67+
background-color: rgba(150, 150, 150, 0.5);
68+
}
69+
70+
.next {
71+
right: 0;
72+
border-radius: 3px 0 0 3px;
73+
}
74+
75+
.prev:hover,
76+
.next:hover {
77+
background-color: rgba(0, 0, 0, 0.8);
78+
}
79+
80+
.mat-spinner {
81+
float: none;
82+
margin: 0 auto;
83+
top: 50%;
84+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Component, OnInit, HostListener, Input } from '@angular/core';
2+
import { HttpClient } from '@angular/common/http';
3+
import { finalize } from 'rxjs/operators';
4+
5+
@Component({
6+
selector: 'app-lightbox',
7+
templateUrl: './lightbox.component.html',
8+
styleUrls: ['./lightbox.component.scss']
9+
})
10+
export class LightboxComponent implements OnInit {
11+
@Input() images: Array<string>;
12+
@Input() headers = {};
13+
slideIndex = 0;
14+
loading = true;
15+
src: string;
16+
17+
constructor(
18+
private http: HttpClient
19+
) {}
20+
21+
ngOnInit(): void {
22+
}
23+
24+
openModal() {
25+
document.getElementById('imgModal').style.display = "block";
26+
}
27+
28+
closeModal() {
29+
document.getElementById('imgModal').style.display = "none";
30+
}
31+
32+
nextImage(n: number) {
33+
this.showImage(this.slideIndex += n);
34+
}
35+
36+
currentSlide(n: number) {
37+
this.showImage(this.slideIndex = n);
38+
}
39+
40+
showImage(n) {
41+
if (n > this.images.length) this.slideIndex = 1;
42+
if (n < 1) this.slideIndex = this.images.length;
43+
this.loading = true;
44+
this.http.get(this.images[this.slideIndex-1], { responseType: 'blob', headers: this.headers })
45+
.pipe(finalize(() => this.loading = false))
46+
.subscribe( {
47+
next: res => this.imageFromBlob(res),
48+
error: e => console.error(e.message)
49+
});
50+
}
51+
52+
imageFromBlob(image: Blob) {
53+
let reader = new FileReader();
54+
reader.addEventListener("load", () => {
55+
if (typeof reader.result === 'string') {
56+
this.src = reader.result;
57+
}
58+
}, false);
59+
60+
if (image) {
61+
reader.readAsDataURL(image);
62+
}
63+
}
64+
65+
@HostListener('document:keydown.escape', ['$event'])
66+
escapeHandler(event: KeyboardEvent) {
67+
this.closeModal();
68+
}
69+
}
70+
71+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const environment = {
2+
production: true,
3+
hathitrustProxy: "https://api.exldevnetwork.net/hathitrust-proxy",
4+
hathitrustUrl: "https://catalog.hathitrust.org"
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const environment = {
2+
production: false,
3+
hathitrustProxy: "https://api.exldevnetwork.net/hathitrust-proxy",
4+
hathitrustUrl: "https://catalog.hathitrust.org"
5+
}

manifest.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
2-
"id": "cloud-app-tutorials",
2+
"id": "ExLibrisGroup/cloudapp-tutorials",
33
"title": "Cloud App Tutorials",
44
"description": "For more information on these tutorials, see https://developers.exlibrisgroup.com/cloudapps/tutorials/.",
55
"subtitle": "Accompanying code for the Cloud App tutorials in the Developer Network",
66
"author": "Josh Weisman",
77
"contentSecurity": {
88
"connectSrc": [
9-
"https://catalog.hathitrust.org/"
9+
"https://catalog.hathitrust.org/",
10+
"https://api.exldevnetwork.net/"
1011
],
1112
"sandbox": {
1213
"popups": true,

0 commit comments

Comments
 (0)