@internetarchive/ia-book-actions 0.0.6 → 0.0.7-3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,69 +1,115 @@
1
- [![Build Status](https://travis-ci.com/internetarchive/iaux-your-webcomponent.svg?branch=master)](https://travis-ci.com/internetarchive/iaux-your-webcomponent) [![codecov](https://codecov.io/gh/internetarchive/iaux-your-webcomponent/branch/master/graph/badge.svg)](https://codecov.io/gh/internetarchive/iaux-your-webcomponent)
1
+ # Component for lendable and embed items
2
2
 
3
- # Internet Archive Javascript WebComponent Template
3
+ The book action button component build on LitElement with support for custom content and light DOM elements.
4
4
 
5
- This is a base template for creating Javascript WebComponents. It is based off of the [Open WebComponents generator](https://open-wc.org/docs/development/generator/) with some IA-specific customizations and some development niceities.
6
-
7
- ## Usage
8
-
9
- 1. Click the "Use this Template" button in GitHub to create a new repository based on this one.
10
- 2. Clone your new repo and update the things below.
11
-
12
- ### Things to update in your copy
13
- 1. Remove this section
14
- 2. Search for the strings `your-webcomponent` and `YourWebComponent` and those are most of the spots that need to be updated.
15
- 3. `README.md` (this file). Update the readme in general, but also the badge URLs
16
- 4. `package.json` Update the name and description
17
- 5. Rename the `your-webcomponent.ts` and its associated `.test` file
18
- 6. Update `.travis.yml` with the proper secure key. See the [Travis docs](https://blog.travis-ci.com/2014-03-13-slack-notifications/) for more information.
19
-
20
- ## Local Demo with `web-dev-server`
5
+ ## Installation
21
6
  ```bash
22
- yarn start
7
+ yarn install --save @internetarchive/ia-book-actions
23
8
  ```
24
- To run a local development server that serves the basic demo located in `demo/index.html`
25
9
 
26
- ## Testing with Web Test Runner
27
- To run the suite of Web Test Runner tests, run
28
- ```bash
29
- yarn run test
10
+ ## Usage of Book Actions
11
+ ```html
12
+ <!-- index.html -->
13
+ <style>
14
+ html {
15
+ font-size: 10px;
16
+ /* This is to match petabox's base font size */
17
+ }
18
+ body {
19
+ margin: 0;
20
+ text-align: center;
21
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
22
+ }
23
+ #demo {
24
+ height: 50px;
25
+ }
26
+ ia-book-actions {
27
+ --white: #fff;
28
+ --black: #000;
29
+ --primaryBGColor: var(--black);
30
+ --secondaryBGColor: #222;
31
+ --tertiaryBGColor: #333;
32
+ --primaryTextColor: var(--white);
33
+ --primaryCTAFill: #194880;
34
+ --primaryCTABorder: #c5d1df;
35
+ --secondaryCTAFill: #333;
36
+ --secondaryCTABorder: #999;
37
+ --secondaryCTAFillRGB: 51, 51, 51;
38
+ --primaryErrorCTAFill: #e51c26;
39
+ --primaryErrorCTABorder: #f8c6c8;
40
+ --primaryCTAFillRGB: 25, 72, 128;
41
+ --primaryErrorCTAFillRGB: 229, 28, 38;
42
+ --primaryDisableCTAFill: #767676;
43
+ }
44
+ </style>
45
+
46
+ <ia-book-actions></ia-book-actions>
47
+
48
+ <script type="module">
49
+ import '@internetarchive/ia-book-actions.js';
50
+ import { ModalConfig } from '@internetarchive/modal-manager';
51
+ import {
52
+ defaultLendingStatus,
53
+ canBrowseAndBorrow,
54
+ } from '../src/assets/data/lending-get-status-service.js';
55
+
56
+ let iaBookActions = document.querySelector('ia-book-actions');
57
+ iaBookActions.userid = '@neeraj-archive';
58
+ iaBookActions.identifier = 'naturalhistoryof00unse_4111';
59
+ iaBookActions.bookTitle = 'This is test title for any book';
60
+ iaBookActions.lendingStatus = {}; // lendinStatus in json
61
+ iaBookActions.bwbPurchaseUrl = 'https://bwb.com/produce/123';
62
+ iaBookActions.barType = ''; // title|action
63
+ </script>
30
64
  ```
31
65
 
32
- To run the tests in watch mode (for &lt;abbr title=&#34;test driven development&#34;&gt;TDD&lt;/abbr&gt;, for example), run
66
+ ![Book Action Bar](./assets/ia-book-action-screen.png "Book Action Bar Demo")
33
67
 
34
- ```bash
35
- yarn run test:watch
68
+ ## Usage of Book Title
69
+ ```html
70
+ <!-- along with above properties, set barType to 'title' -->
71
+ <script type="module">
72
+ iaBookActions.barType = 'title'; // title|action
73
+ </script>
36
74
  ```
75
+ ![Book Title Bar](./assets/ia-book-title-screen.png "Book Title Bar Demo")
37
76
 
38
- ## Linting with ESLint, Prettier, and Types
39
- To scan the project for linting errors, run
40
- ```bash
41
- yarn run lint
42
- ```
77
+ ### Config Options
43
78
 
44
- You can lint with ESLint and Prettier individually as well
45
- ```bash
46
- yarn run lint:eslint
79
+ All of the config options:
80
+
81
+ ```javascript
82
+ let iaBookActions = document.querySelector('ia-book-actions');
83
+ const modalConfig = new ModalConfig(); // to show info/error modal
84
+
85
+ // set defaultLendingStatus for unavailable (without borrowables) book
86
+ iaBookActions.userid = '@jack';
87
+ iaBookActions.identifier = 'jack-sparrow';
88
+ iaBookActions.bookTitle = 'Contemporary Black biography. Volume 39 : profiles from the interContemporary Black biography. Volume 39';
89
+ iaBookActions.lendingStatus = {active_borrows: 0, active_browses: 0}, // complete lendingStatus object
90
+ iaBookActions.bwbPurchaseUrl = ''; // BWB purchase URl if any
91
+ iaBookActions.barType = '';
92
+ iaBookActions.modalConfig = modalConfig;
47
93
  ```
94
+
95
+ # Development
96
+
97
+ ## Prerequisite
48
98
  ```bash
49
- yarn run lint:prettier
99
+ yarn install
50
100
  ```
51
101
 
52
- To automatically fix many linting errors, run
102
+ ## Start Development Server
53
103
  ```bash
54
- yarn run format
104
+ yarn start
55
105
  ```
56
106
 
57
- You can format using ESLint and Prettier individually as well
107
+ ## Testing
58
108
  ```bash
59
- yarn run format:eslint
109
+ yarn test
60
110
  ```
111
+
112
+ ## Linting
61
113
  ```bash
62
- yarn run format:prettier
114
+ yarn lint
63
115
  ```
64
-
65
- ## Tooling configs
66
-
67
- For most of the tools, the configuration is in the `package.json` to reduce the amount of files in your project.
68
-
69
- If you customize the configuration a lot, you can consider moving them to individual files.
Binary file
Binary file
package/demo/index.html CHANGED
@@ -71,6 +71,14 @@
71
71
  float: left;
72
72
  margin: 2px;
73
73
  }
74
+
75
+ body.modal-manager-open {
76
+ overflow: hidden;
77
+ }
78
+
79
+ modal-manager[mode='closed'] {
80
+ display: none;
81
+ }
74
82
  </style>
75
83
  </head>
76
84
 
@@ -85,6 +93,7 @@
85
93
  defaultLendingStatus,
86
94
  canBrowseAndBorrow,
87
95
  } from '../src/assets/data/lending-get-status-service.js';
96
+ import { ModalConfig } from '@internetarchive/modal-manager';
88
97
 
89
98
  const userid = '@neeraj-archive';
90
99
  const identifier = 'naturalhistoryof00unse_4111';
@@ -92,13 +101,17 @@
92
101
 
93
102
  let iaBookActions = document.querySelector('ia-book-actions');
94
103
 
104
+ const modalConfig = new ModalConfig();
105
+ modalConfig.headerColor = '#d9534f';
106
+
95
107
  // set defaultLendingStatus for unavailable (without borrowables) book
96
- iaBookActions.userid = '';
108
+ iaBookActions.userid = userid;
97
109
  iaBookActions.identifier = identifier;
98
110
  iaBookActions.bookTitle = 'Contemporary Black biography. Volume 39 : profiles from the interContemporary Black biography. Volume 39';
99
- iaBookActions.lendingStatus = defaultLendingStatus;
111
+ iaBookActions.lendingStatus = canBrowseAndBorrow;
100
112
  iaBookActions.bwbPurchaseUrl = '';
101
113
  iaBookActions.barType = '';
114
+ iaBookActions.modalConfig = modalConfig;
102
115
 
103
116
  let lendingStatus1 = iaBookActions.lendingStatus;
104
117
 
@@ -112,6 +125,29 @@
112
125
  });
113
126
  });
114
127
 
128
+ document.querySelectorAll('.errorEnable input[type=checkbox]').forEach((item) => {
129
+ if (window.location.href.indexOf('?error=true') !== -1) {
130
+ item.checked = true;
131
+ }
132
+ item.addEventListener('click', (e) => {
133
+ var searchParams = new URLSearchParams(window.location.search);
134
+ var separate = '';
135
+ if (e.target.checked) {
136
+ separate = '?';
137
+ searchParams.set('error', true);
138
+ } else {
139
+ separate = '';
140
+ searchParams.delete('error');
141
+ }
142
+
143
+ if (history.pushState) {
144
+ var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + separate + searchParams;
145
+ window.history.pushState({path:newurl},'',newurl);
146
+ }
147
+ });
148
+ });
149
+
150
+
115
151
  document.querySelectorAll('.userState input[type=checkbox]').forEach((item) => {
116
152
  item.addEventListener('click', (e) => {
117
153
  if (e.target.checked) {
@@ -219,6 +255,9 @@
219
255
  <fieldset>
220
256
  <legend>Dev testing for book action bar</legend>
221
257
  <table class="initial">
258
+ <tr colspan='5'>
259
+ <td class="errorEnable"><label><input type="checkbox" name="errorEnable" value="errorEnable" />Do you want to see error response?</label></td>
260
+ </tr>
222
261
  <tr>
223
262
  <td class="userState">
224
263
  <label><input type="checkbox" name="user" value="isAdmin" />isAdmin</label>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.0.6",
3
+ "version": "0.0.7-03",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -20,7 +20,9 @@
20
20
  "dependencies": {
21
21
  "@internetarchive/ia-icons": "^1.2.2",
22
22
  "@internetarchive/icon-bookmark": "^1.1.3",
23
+ "@internetarchive/icon-close": "^1.1.3",
23
24
  "@internetarchive/icon-info": "^1.2.6",
25
+ "@internetarchive/modal-manager": "^0.1.0",
24
26
  "@internetarchive/shared-resize-observer": "^0.0.1",
25
27
  "lit-element": "^2.4.0",
26
28
  "lit-html": "^1.3.0"
@@ -6,6 +6,9 @@ export default css`
6
6
  height: 3.5rem;
7
7
  padding: 1rem 0;
8
8
  }
9
+ .actiongroup {
10
+ display: flex;
11
+ }
9
12
  .action-buttons {
10
13
  display: inline-flex;
11
14
  align-items: center;
@@ -80,7 +83,7 @@ export default css`
80
83
  padding: 0 0.6rem;
81
84
  margin-left: 0;
82
85
  }
83
- .action-loader {
86
+ .actionloader {
84
87
  vertical-align: middle;
85
88
  visibility: hidden;
86
89
  padding: 0.9rem 0.2rem;
@@ -129,9 +132,14 @@ export default css`
129
132
  display: initial;
130
133
  font-size: 1.4rem;
131
134
  }
132
-
133
135
  .unavailable {
134
136
  opacity: 0.7;
135
137
  pointer-events: none;
136
138
  }
139
+
140
+ .disabled {
141
+ opacity: 0.8;
142
+ pointer-events: none;
143
+ visibility: visible;
144
+ }
137
145
  `;
@@ -1,5 +1,6 @@
1
1
  import { html } from 'lit-element';
2
2
  import { nothing } from 'lit-html';
3
+ import { classMap } from 'lit-html/directives/class-map';
3
4
 
4
5
  import ActionsHandler from '../core/services/actions-handler/actions-handler.js';
5
6
 
@@ -25,6 +26,8 @@ export class CollapsibleActionGroup extends ActionsHandler {
25
26
  width: { type: Number },
26
27
  hasAdminAccess: { type: Boolean },
27
28
  dropdownArrow: { type: String },
29
+ disabled: { type: Boolean },
30
+ borrowType: { type: String },
28
31
  };
29
32
  }
30
33
 
@@ -42,6 +45,9 @@ export class CollapsibleActionGroup extends ActionsHandler {
42
45
  this.initialButton = false;
43
46
  this.title = '';
44
47
  this.loaderIcon = 'https://archive.org/upload/images/tree/loading.gif';
48
+ this.disabled = false;
49
+ this.borrowType = ''; // (browsed|borrowed)
50
+ this.consecutiveLoanCounts = 1; // consecutive loan count
45
51
  }
46
52
 
47
53
  updated(changed) {
@@ -50,6 +56,42 @@ export class CollapsibleActionGroup extends ActionsHandler {
50
56
  this.resetActions();
51
57
  }
52
58
  }
59
+
60
+ // this is execute to fetch loan token
61
+ if (changed.has('borrowType')) {
62
+ this.emitEnableBookAccess();
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Handles events when user is borrowing book and wants to have access to read
68
+ *
69
+ * @fires CollapsibleActionGroup#enableBookAccess
70
+ */
71
+ emitEnableBookAccess() {
72
+ // send consecutiveLoanCounts for browsed books only.
73
+ if (this.borrowType === 'browsing') {
74
+ this.consecutiveLoanCounts =
75
+ localStorage.getItem('consecutive-loan-count') ?? 1;
76
+ }
77
+
78
+ // event category and action for browsing book access
79
+ const eventCategory = `${this.borrowType}BookAccess`;
80
+ const eventAction = `${
81
+ this.borrowType === 'browsing' ? 'BrowseCounts-' : 'Counts-'
82
+ }${this.consecutiveLoanCounts}`;
83
+
84
+ this.dispatchEvent(
85
+ new CustomEvent('enableBookAccess', {
86
+ detail: {
87
+ event: {
88
+ category: eventCategory, // brosingBookAccess | borrowingBookAccess
89
+ action: eventAction, // (BrowseCounts-N|Counts-N)
90
+ },
91
+ borrowType: this.borrowType,
92
+ },
93
+ })
94
+ );
53
95
  }
54
96
 
55
97
  /**
@@ -94,13 +136,20 @@ export class CollapsibleActionGroup extends ActionsHandler {
94
136
 
95
137
  render() {
96
138
  return html`
97
- ${this.getLoaderIcon}
98
- <section class="action-buttons primary">
99
- ${this.renderPrimaryActions}
100
- </section>
101
- <section class="action-buttons secondary">
102
- ${this.renderSecondaryActions}
103
- </section>
139
+ <div
140
+ class="${classMap({
141
+ actiongroup: true,
142
+ disabled: this.disabled,
143
+ })}"
144
+ >
145
+ ${this.getLoaderIcon}
146
+ <section class="action-buttons primary">
147
+ ${this.renderPrimaryActions}
148
+ </section>
149
+ <section class="action-buttons secondary">
150
+ ${this.renderSecondaryActions}
151
+ </section>
152
+ </div>
104
153
  `;
105
154
  }
106
155
 
@@ -177,11 +226,13 @@ export class CollapsibleActionGroup extends ActionsHandler {
177
226
  }
178
227
 
179
228
  /**
180
- * Click handler to emit custom event on action click
181
- * @param { string } eventName
182
- * @param { object } gaEvent
229
+ * Handles click events when user click on action buttons
230
+ * @param { string } eventName - actions like 'browseBook', 'borrowBook' etc...
231
+ * @param { object } gaEvent - contains analytics event action and category
183
232
  * @param { string } gaEvent.category
184
233
  * @param { string } gaEvent.action
234
+ *
235
+ * @fires CollapsibleActionGroup#{eventName} - (will be browseBook, borrowBook etc...)
185
236
  */
186
237
  clickHandler(eventName, gaEvent) {
187
238
  this.dropdownState = 'close';
@@ -225,7 +276,14 @@ export class CollapsibleActionGroup extends ActionsHandler {
225
276
  * @returns { HTMLElement }
226
277
  */
227
278
  get getLoaderIcon() {
228
- return html`<img class="action-loader" alt="" src="${this.loaderIcon}" />`;
279
+ return html`<img
280
+ class="${classMap({
281
+ actionloader: true,
282
+ disabled: this.disabled,
283
+ })}"
284
+ alt=""
285
+ src="${this.loaderIcon}"
286
+ />`;
229
287
  }
230
288
 
231
289
  /**
@@ -8,24 +8,15 @@ export default function ActionsHandlerService(options) {
8
8
  const option = {
9
9
  action: null,
10
10
  identifier: '',
11
- data: {},
12
11
  success() {},
13
- error: null,
14
- loader: true,
15
- type: 'POST',
12
+ error() {},
16
13
  ...options,
17
14
  };
18
15
 
19
- if (option.loader) {
20
- showActionButtonLoader();
21
- }
16
+ let baseHost = '/services/loans/loan';
17
+ const location = window?.location;
22
18
 
23
- let baseHost = '';
24
- if (window.location.pathname === '/demo/') {
25
- baseHost = `/demo/`;
26
- } else {
27
- baseHost = `/services/loans/loan`;
28
- }
19
+ if (location?.pathname === '/demo/') baseHost = `/demo/`;
29
20
 
30
21
  let formData = new FormData();
31
22
  formData.append('action', option.action);
@@ -36,23 +27,38 @@ export default function ActionsHandlerService(options) {
36
27
  body: formData,
37
28
  })
38
29
  .then(response => {
39
- if (response.status === 200) {
40
- option.success.call();
30
+ let shouldReturnError = location?.href?.indexOf('?error=true') !== -1;
31
+ const tokenError = 'loan token not found. please try again later.';
32
+ const borrowError =
33
+ 'This book is not available to borrow at this time. Please try again later.';
34
+
35
+ // return error reponse if query param has ?error=true param...
36
+ const erroneousActions = ['browse_book', 'borrow_book', 'create_token'];
37
+ if (shouldReturnError && erroneousActions.includes(option?.action)) {
38
+ return {
39
+ success: false,
40
+ error: option?.action === 'create_token' ? tokenError : borrowError,
41
+ };
41
42
  }
42
- })
43
- .catch(error => {
44
- console.log(error);
45
- });
46
- }
47
43
 
48
- function showActionButtonLoader() {
49
- const collapsibleElement = document
50
- .querySelector('ia-book-actions')
51
- .shadowRoot.querySelector('collapsible-action-group');
52
- collapsibleElement.setAttribute('style', 'opacity:0.8; pointer-events:none');
44
+ // return success response for /demo/ server...
45
+ if (baseHost == '/demo/1' || baseHost == '/demo/') {
46
+ return {
47
+ success: true,
48
+ message: 'operation executed successfully!',
49
+ };
50
+ }
53
51
 
54
- const actionLoader = collapsibleElement.shadowRoot.querySelector(
55
- '.action-loader'
56
- );
57
- actionLoader.setAttribute('style', 'visibility: visible');
52
+ // The response is a Response instance.
53
+ // You parse the data into a useable format using `.json()`
54
+ return response.json();
55
+ })
56
+ .then(data => {
57
+ // `data` is the parsed version of the JSON returned from the above endpoint.
58
+ if (!data?.error) {
59
+ option?.success(data);
60
+ } else {
61
+ option?.error(data);
62
+ }
63
+ });
58
64
  }
@@ -7,33 +7,45 @@ import ActionsHandlerService from './actions-handler-service.js';
7
7
  *
8
8
  * ActionsHandlerService is a function being used to execute
9
9
  * APIs based of the request made by user. It consist some parameters are as follows:-
10
- * 1. action: action name like browse_book, return_book, borrow_book etc...
11
- * 2. identifier: book name.
12
- * 3. success: This is callback function which will be executed after the API request
13
- * return reponse as a success.
10
+ * 1. identifier: book name.
14
11
  */
15
12
 
16
13
  export default class ActionsHandler extends LitElement {
17
14
  constructor(identifier) {
18
15
  super();
19
16
  this.identifier = identifier;
20
- this.ajaxTimeout = 6000;
17
+ this.ajaxTimeout = 7000;
18
+ this.loanTokenPollingDelay =
19
+ window.location.pathname === '/demo/' ? 2000 : 120000; // 120000 ms = 2 min
20
+ this.loanTokenInterval = undefined;
21
21
  this.bindEvents();
22
22
  }
23
23
 
24
+ disconnectedCallback() {
25
+ this.loanTokenInterval = undefined;
26
+ }
27
+
24
28
  sendEvent(eventCategory, eventAction) {
25
29
  // eslint-disable-next-line no-console
26
30
  console?.log('Book action: ', { eventCategory, eventAction });
27
- window?.archive_analytics?.send_event(
31
+ window?.archive_analytics?.send_event_no_sampling(
28
32
  eventCategory,
29
33
  eventAction,
30
- window.location.pathname
34
+ `identifier=${this.identifier}`
31
35
  );
32
36
  }
33
37
 
34
38
  bindEvents() {
35
39
  this.addEventListener('browseBook', ({ detail }) => {
36
40
  this.handleBrowseIt();
41
+ this.setConsecutiveLoanCounts();
42
+ const { category, action } = detail.event;
43
+ this.sendEvent(category, action);
44
+ });
45
+
46
+ this.addEventListener('browseBookAgain', ({ detail }) => {
47
+ this.handleBrowseIt();
48
+ this.setConsecutiveLoanCounts('browseAgain');
37
49
  const { category, action } = detail.event;
38
50
  this.sendEvent(category, action);
39
51
  });
@@ -87,59 +99,130 @@ export default class ActionsHandler extends LitElement {
87
99
  const { category, action } = detail.event;
88
100
  this.sendEvent(category, action);
89
101
  });
102
+
103
+ this.addEventListener('enableBookAccess', ({ detail }) => {
104
+ const borrowType = detail?.borrowType;
105
+
106
+ // fetch loan token for browsed/borrowed book and set an interval
107
+ if (borrowType) {
108
+ this.loanTokenInterval = setInterval(() => {
109
+ this.handleLoanTokenPoller();
110
+ }, this.loanTokenPollingDelay);
111
+
112
+ const { category, action } = detail?.event;
113
+ this.sendEvent(category, action);
114
+ } else {
115
+ // if book is not browsed, just clear token polling interval
116
+ clearInterval(this.loanTokenInterval);
117
+ }
118
+ });
90
119
  }
91
120
 
92
121
  handleBrowseIt() {
122
+ const action = 'browse_book';
123
+ this.dispatchToggleActionGroup();
124
+
93
125
  ActionsHandlerService({
94
- action: 'browse_book',
126
+ action,
95
127
  identifier: this.identifier,
96
128
  success: () => {
97
129
  this.handleReadItNow();
98
130
  },
131
+ error: data => {
132
+ this.dispatchActionError(action, data);
133
+ },
99
134
  });
100
135
  }
101
136
 
102
137
  handleReturnIt() {
138
+ const action = 'return_loan';
139
+ this.dispatchToggleActionGroup();
140
+
103
141
  ActionsHandlerService({
104
- action: 'return_loan',
142
+ action,
105
143
  identifier: this.identifier,
106
144
  success: () => {
107
145
  this.deleteLoanCookies();
108
146
  URLHelper.goToUrl(`/details/${this.identifier}`, true);
109
147
  },
148
+ error: data => {
149
+ this.dispatchActionError(action, data);
150
+ },
110
151
  });
111
152
  }
112
153
 
113
154
  handleBorrowIt() {
155
+ const action = 'borrow_book';
156
+ this.dispatchToggleActionGroup();
157
+
114
158
  ActionsHandlerService({
115
- action: 'borrow_book',
159
+ action,
116
160
  identifier: this.identifier,
117
161
  success: () => {
118
162
  this.handleReadItNow();
119
163
  },
164
+ error: data => {
165
+ this.dispatchActionError(action, data);
166
+ },
120
167
  });
121
168
  }
122
169
 
123
170
  handleReserveIt() {
171
+ const action = 'join_waitlist';
172
+ this.dispatchToggleActionGroup();
173
+
124
174
  ActionsHandlerService({
125
- action: 'join_waitlist',
175
+ action,
126
176
  identifier: this.identifier,
127
177
  success: () => {
128
178
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
129
179
  },
180
+ error: data => {
181
+ this.dispatchActionError(action, data);
182
+ },
130
183
  });
131
184
  }
132
185
 
133
186
  handleRemoveFromWaitingList() {
187
+ const action = 'leave_waitlist';
188
+ this.dispatchToggleActionGroup();
189
+
134
190
  ActionsHandlerService({
135
- action: 'leave_waitlist',
191
+ action,
136
192
  identifier: this.identifier,
137
193
  success: () => {
138
194
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
139
195
  },
196
+ error: data => {
197
+ this.dispatchActionError(action, data);
198
+ },
199
+ });
200
+ }
201
+
202
+ handleLoanTokenPoller() {
203
+ const action = 'create_token';
204
+ ActionsHandlerService({
205
+ identifier: this.identifier,
206
+ action,
207
+ error: data => {
208
+ this.dispatchActionError(action, data);
209
+ clearInterval(this.loanTokenInterval); // stop token fetch api
210
+ },
140
211
  });
141
212
  }
142
213
 
214
+ dispatchActionError(action, data = {}) {
215
+ this.dispatchEvent(
216
+ new CustomEvent('lendingActionError', {
217
+ detail: { action, data },
218
+ })
219
+ );
220
+ }
221
+
222
+ dispatchToggleActionGroup() {
223
+ this.dispatchEvent(new CustomEvent('toggleActionGroup'));
224
+ }
225
+
143
226
  handleLoginOk() {
144
227
  /* eslint class-methods-use-this: "off" */
145
228
  const target = `/account/login?referer=${encodeURIComponent(
@@ -172,6 +255,23 @@ export default class ActionsHandler extends LitElement {
172
255
  }, this.ajaxTimeout);
173
256
  }
174
257
 
258
+ // save consecutive loan count for borrow
259
+ setConsecutiveLoanCounts(action = '') {
260
+ const { localStorage } = window;
261
+ if (localStorage) {
262
+ let newCount = 1;
263
+ const storageKey = `consecutive-loan-count`;
264
+ const existingCount = Number(localStorage.getItem(storageKey));
265
+
266
+ // want to increase browse-count by 1 if,
267
+ // you consecutive reading a book.
268
+ if (action === 'browseAgain') {
269
+ newCount = existingCount ? existingCount + 1 : 1;
270
+ }
271
+ localStorage.setItem(storageKey, newCount);
272
+ }
273
+ }
274
+
175
275
  deleteLoanCookies() {
176
276
  const date = new Date();
177
277
  date.setTime(date.getTime() - 24 * 60 * 60 * 1000); // one day ago
@@ -75,7 +75,9 @@ export default class GetLendingActions {
75
75
  const disableBorrow = lendingStatus.loanCount >= lendingStatus.maxLoans;
76
76
 
77
77
  let primaryTitleText = '';
78
- if (lendingStatus.user_has_browsed) {
78
+ let isBrowsing =
79
+ lendingStatus.user_has_browsed && !lendingStatus.browsingExpired;
80
+ if (isBrowsing) {
79
81
  primaryTitleText = this.getBrowseCountdownTitle();
80
82
  } else {
81
83
  primaryTitleText = `Your loan of this book has ${lendingStatus.daysLeftOnLoan} days left.`;
@@ -94,6 +96,7 @@ export default class GetLendingActions {
94
96
  this.actionsConfig.adminAccessConfig(),
95
97
  this.actionsConfig.purchaseConfig(),
96
98
  ],
99
+ borrowType: isBrowsing ? 'browsing' : 'borrowing',
97
100
  };
98
101
  }
99
102
 
@@ -2,6 +2,7 @@
2
2
  import { html, css, LitElement } from 'lit-element';
3
3
 
4
4
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
5
+ import { ModalConfig } from '@internetarchive/modal-manager';
5
6
 
6
7
  import './components/collapsible-action-group.js';
7
8
  import './components/book-title-bar.js';
@@ -22,6 +23,7 @@ export default class IABookActions extends LitElement {
22
23
  bwbPurchaseUrl: { type: String },
23
24
  barType: { type: String },
24
25
  sharedObserver: { attribute: false },
26
+ disableActionGroup: { type: Boolean },
25
27
  };
26
28
  }
27
29
 
@@ -40,6 +42,7 @@ export default class IABookActions extends LitElement {
40
42
  this.primaryColor = 'primary';
41
43
  this.secondaryActions = [];
42
44
  this.lendingOptions = {};
45
+ this.disableActionGroup = false;
43
46
  }
44
47
 
45
48
  disconnectedCallback() {
@@ -51,6 +54,7 @@ export default class IABookActions extends LitElement {
51
54
  this.sharedObserver = new SharedResizeObserver();
52
55
  this.setupResizeObserver();
53
56
  }
57
+
54
58
  this.setupLendingToolbarActions();
55
59
  }
56
60
 
@@ -67,8 +71,7 @@ export default class IABookActions extends LitElement {
67
71
  }
68
72
 
69
73
  browseHasExpired() {
70
- const browsingExpired = true;
71
- const currStatus = { ...this.lendingStatus, browsingExpired };
74
+ const currStatus = { ...this.lendingStatus, browsingExpired: true };
72
75
  this.lendingStatus = currStatus;
73
76
  }
74
77
 
@@ -140,10 +143,10 @@ export default class IABookActions extends LitElement {
140
143
  return action != null;
141
144
  });
142
145
 
143
- if (
144
- this.lendingStatus.user_has_browsed &&
145
- !this.lendingStatus.browseHasExpired
146
- ) {
146
+ this.borrowType = actions.borrowType;
147
+ if (this.borrowType === 'browsing') {
148
+ // start timer for browsing.
149
+ // when browse is completed, we shows browse-again button
147
150
  this.startBrowseTimer();
148
151
  }
149
152
  }
@@ -172,13 +175,97 @@ export default class IABookActions extends LitElement {
172
175
  .primaryActions=${this.primaryActions}
173
176
  .secondaryActions=${this.secondaryActions}
174
177
  .width=${this.width}
175
- .hasAdminAccess=${this.hasAdminAccess}
178
+ .borrowType=${this.borrowType}
179
+ ?hasAdminAccess=${this.hasAdminAccess}
180
+ ?disabled=${this.disableActionGroup}
181
+ @lendingActionError=${this.handleLendingActionError}
182
+ @toggleActionGroup=${this.handleToggleActionGroup}
176
183
  >
177
184
  </collapsible-action-group>
178
185
  ${this.textGroupTemplate} ${this.infoIconTemplate}
179
186
  `;
180
187
  }
181
188
 
189
+ /*
190
+ * custom event handler to toggle action group visibility
191
+ *
192
+ * @event IABookActions#toggleActionGroup
193
+ */
194
+ handleToggleActionGroup() {
195
+ this.disableActionGroup = !this.disableActionGroup;
196
+ }
197
+
198
+ /*
199
+ * handle lending errors occure during different operation like
200
+ * browse book, borrow book, borrowed book token etc...
201
+ *
202
+ * @event IABookActions#lendingActionError
203
+ * @param {Object} event - The employee who is responsible for the project
204
+ * @param {string} event.detail.action - action when error occurred like 'browseBook', 'borrowBook'
205
+ * @param {string} event.detail.data.error - error message
206
+ */
207
+ handleLendingActionError(event) {
208
+ // toggle activity loader
209
+ this.handleToggleActionGroup();
210
+
211
+ const action = event?.detail?.action;
212
+ const errorMsg = event?.detail?.data?.error;
213
+
214
+ if (errorMsg) this.showErrorModal(errorMsg, action);
215
+
216
+ // update action bar state if book is not available to browse or borrow.
217
+ if (errorMsg && errorMsg.match(/not available to borrow/gm)) {
218
+ let currStatus = this.lendingStatus;
219
+ if (action === 'browse_book') {
220
+ currStatus = {
221
+ ...this.lendingStatus,
222
+ available_to_browse: false,
223
+ };
224
+ } else if (action === 'borrow_book') {
225
+ currStatus = {
226
+ ...this.lendingStatus,
227
+ available_to_borrow: false,
228
+ };
229
+ }
230
+ this.lendingStatus = currStatus;
231
+ }
232
+ }
233
+
234
+ /* show error message if something went wrong */
235
+ async showErrorModal(errorMsg, action) {
236
+ this.disableActionGroup = false;
237
+
238
+ this.modal = document.querySelector('#action-bar-modal');
239
+ if (!this.modal) {
240
+ this.modal = document.createElement('modal-manager');
241
+ this.modal.id = 'action-bar-modal';
242
+ }
243
+ await document.body.appendChild(this.modal);
244
+
245
+ const modalConfig = new ModalConfig({
246
+ title: 'Lending error',
247
+ message: errorMsg,
248
+ headerColor: '#d9534f',
249
+ });
250
+
251
+ if (action === 'create_token') {
252
+ modalConfig.closeOnBackdropClick = false;
253
+ modalConfig.showCloseButton = false;
254
+ modalConfig.message = html` Uh oh, something went wrong trying to access
255
+ this book.<br />
256
+ Please <a href="/">refresh</a> to try again or send us an email to
257
+ <a
258
+ href="mailto:info@archive.org?subject=Help: cannot access my borrowed book: ${this
259
+ .identifier}"
260
+ >info@archive.org</a
261
+ >`;
262
+ }
263
+
264
+ this.modal.showModal({
265
+ config: modalConfig,
266
+ });
267
+ }
268
+
182
269
  get iconClass() {
183
270
  return this.width <= mobileContainerWidth ? 'mobile' : 'desktop';
184
271
  }
@@ -1,5 +1,6 @@
1
1
  import { html, fixture, expect } from '@open-wc/testing';
2
2
  import '../../src/components/collapsible-action-group.js';
3
+ import Sinon from 'sinon';
3
4
 
4
5
  const container = ({
5
6
  primaryActions = [
@@ -23,10 +24,12 @@ const container = ({
23
24
  text: 'Purchase',
24
25
  },
25
26
  ],
27
+ borrowType = '',
26
28
  } = {}) =>
27
29
  html`<collapsible-action-group
28
30
  .primaryActions=${primaryActions}
29
31
  .secondaryActions=${secondaryActions}
32
+ .borrowType=${borrowType}
30
33
  ></collapsible-action-group>`;
31
34
 
32
35
  describe('<collapsible-action-group>', () => {
@@ -39,6 +42,22 @@ describe('<collapsible-action-group>', () => {
39
42
  const primaryButton = primaryActionContainer.querySelector('.ia-button');
40
43
  expect(primaryButton.innerText).to.equal('Borrow for 1 hour');
41
44
  });
45
+
46
+ it('check if loader is active and action-group is disabled', async () => {
47
+ const el = await fixture(container());
48
+
49
+ const primaryActionContainer = el.shadowRoot.querySelector('.primary');
50
+ primaryActionContainer
51
+ .querySelector('.ia-button')
52
+ .dispatchEvent(new CustomEvent('toggle-loader'));
53
+ el.disabled = true;
54
+ await el.updateComplete;
55
+
56
+ expect(el.disabled).to.be.true;
57
+ expect(el.shadowRoot.querySelector('div').classList.contains('disabled')).to
58
+ .be.true;
59
+ });
60
+
42
61
  it('fires analytics events', async () => {
43
62
  const el = await fixture(container());
44
63
  const primaryActionContainer = el.shadowRoot.querySelector('.primary');
@@ -66,3 +85,27 @@ describe('<collapsible-action-group>', () => {
66
85
  expect(eAnalytics.action).to.equal('Browse');
67
86
  });
68
87
  });
88
+
89
+ describe('fetch loan token and localStorage for browsed/borrowsed book', () => {
90
+ beforeEach(() => {
91
+ const localStorageMock = {
92
+ getItem: Sinon.spy(),
93
+ setItem: Sinon.spy(),
94
+ };
95
+ Object.defineProperty(window, 'localStorage', {
96
+ value: localStorageMock,
97
+ });
98
+ });
99
+
100
+ it('test localStorage function when borrowType is browsing', async () => {
101
+ await fixture(container({ borrowType: 'browsing' }));
102
+ expect(localStorage.getItem).to.be.calledOnce;
103
+ expect(localStorage.getItem).to.be.calledWith('consecutive-loan-count');
104
+ });
105
+
106
+ it('test localStorage function when borrowType is borrowing', async () => {
107
+ await fixture(container({ borrowType: 'borrowing' }));
108
+ expect(localStorage.getItem).to.not.be.calledOnce;
109
+ expect(localStorage.getItem).to.not.be.calledWith('consecutive-loan-count');
110
+ });
111
+ });
@@ -106,6 +106,64 @@ describe('Primary Actions data', () => {
106
106
  });
107
107
 
108
108
  describe('Borrow status actions', () => {
109
+ it('Update available_to_browse key when book is not available to borrow', async () => {
110
+ const el = await fixture(
111
+ container({
112
+ userid: '@user1',
113
+ lendingStatus: {
114
+ is_lendable: true,
115
+ available_to_browse: true,
116
+ available_to_borrow: true,
117
+ },
118
+ })
119
+ );
120
+
121
+ const errorEvent = new CustomEvent('lendingActionError');
122
+ el.addEventListener('lendingActionError', () => {
123
+ el.handleLendingActionError({
124
+ detail: {
125
+ action: 'browse_book',
126
+ data: { error: 'not available to borrow' },
127
+ },
128
+ });
129
+ });
130
+ el.dispatchEvent(errorEvent);
131
+ await el.updateComplete;
132
+
133
+ // removed available_to_browse from lending bar
134
+ expect(el.lendingStatus.available_to_browse).to.be.false;
135
+ expect(el.primaryActions[0].text).to.equal('Borrow for 14 days');
136
+ });
137
+
138
+ it('Update available_to_borrow key when book is not available to borrow', async () => {
139
+ const el = await fixture(
140
+ container({
141
+ userid: '@user1',
142
+ lendingStatus: {
143
+ is_lendable: true,
144
+ available_to_browse: true,
145
+ available_to_borrow: true,
146
+ },
147
+ })
148
+ );
149
+
150
+ const errorEvent = new Event('lendingActionError');
151
+ el.addEventListener('lendingActionError', () => {
152
+ el.handleLendingActionError({
153
+ detail: {
154
+ action: 'borrow_book',
155
+ data: { error: 'not available to borrow' },
156
+ },
157
+ });
158
+ });
159
+ el.dispatchEvent(errorEvent);
160
+ await el.updateComplete;
161
+
162
+ // removed available_to_borrow from lending bar
163
+ expect(el.lendingStatus.available_to_borrow).to.be.false;
164
+ expect(el.primaryActions[0].text).to.equal('Borrow for 1 hour');
165
+ });
166
+
109
167
  it('Check action for borrowable book without user', async () => {
110
168
  const el = await fixture(
111
169
  container({