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

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,23 @@
92
101
 
93
102
  let iaBookActions = document.querySelector('ia-book-actions');
94
103
 
104
+ const modalConfig = new ModalConfig();
105
+ modalConfig.headerColor = '#d9534f';
106
+
107
+ let lendingBarPostInit = function lendingBarPostInit() {
108
+ setTimeout(function() {
109
+ console.log('executed');
110
+ }, 100);
111
+ }
95
112
  // set defaultLendingStatus for unavailable (without borrowables) book
96
- iaBookActions.userid = '';
113
+ iaBookActions.userid = userid;
97
114
  iaBookActions.identifier = identifier;
98
115
  iaBookActions.bookTitle = 'Contemporary Black biography. Volume 39 : profiles from the interContemporary Black biography. Volume 39';
99
- iaBookActions.lendingStatus = defaultLendingStatus;
116
+ iaBookActions.lendingStatus = canBrowseAndBorrow;
100
117
  iaBookActions.bwbPurchaseUrl = '';
101
118
  iaBookActions.barType = '';
119
+ iaBookActions.modalConfig = modalConfig;
120
+ iaBookActions.lendingBarPostInit = lendingBarPostInit;
102
121
 
103
122
  let lendingStatus1 = iaBookActions.lendingStatus;
104
123
 
@@ -112,6 +131,29 @@
112
131
  });
113
132
  });
114
133
 
134
+ document.querySelectorAll('.errorEnable input[type=checkbox]').forEach((item) => {
135
+ if (window.location.href.indexOf('?error=true') !== -1) {
136
+ item.checked = true;
137
+ }
138
+ item.addEventListener('click', (e) => {
139
+ var searchParams = new URLSearchParams(window.location.search);
140
+ var separate = '';
141
+ if (e.target.checked) {
142
+ separate = '?';
143
+ searchParams.set('error', true);
144
+ } else {
145
+ separate = '';
146
+ searchParams.delete('error');
147
+ }
148
+
149
+ if (history.pushState) {
150
+ var newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + separate + searchParams;
151
+ window.history.pushState({path:newurl},'',newurl);
152
+ }
153
+ });
154
+ });
155
+
156
+
115
157
  document.querySelectorAll('.userState input[type=checkbox]').forEach((item) => {
116
158
  item.addEventListener('click', (e) => {
117
159
  if (e.target.checked) {
@@ -219,6 +261,9 @@
219
261
  <fieldset>
220
262
  <legend>Dev testing for book action bar</legend>
221
263
  <table class="initial">
264
+ <tr colspan='5'>
265
+ <td class="errorEnable"><label><input type="checkbox" name="errorEnable" value="errorEnable" />Do you want to see error response?</label></td>
266
+ </tr>
222
267
  <tr>
223
268
  <td class="userState">
224
269
  <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-10",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -20,7 +20,10 @@
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/local-cache": "^0.2.0",
26
+ "@internetarchive/modal-manager": "^0.1.0",
24
27
  "@internetarchive/shared-resize-observer": "^0.0.1",
25
28
  "lit-element": "^2.4.0",
26
29
  "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,7 @@ export class CollapsibleActionGroup extends ActionsHandler {
25
26
  width: { type: Number },
26
27
  hasAdminAccess: { type: Boolean },
27
28
  dropdownArrow: { type: String },
29
+ disabled: { type: Boolean },
28
30
  };
29
31
  }
30
32
 
@@ -42,13 +44,12 @@ export class CollapsibleActionGroup extends ActionsHandler {
42
44
  this.initialButton = false;
43
45
  this.title = '';
44
46
  this.loaderIcon = 'https://archive.org/upload/images/tree/loading.gif';
47
+ this.disabled = false;
45
48
  }
46
49
 
47
50
  updated(changed) {
48
- if (changed.has('width')) {
49
- if (this.isBelowTabletContainer) {
50
- this.resetActions();
51
- }
51
+ if (changed.has('width') && this.isBelowTabletContainer) {
52
+ this.resetActions();
52
53
  }
53
54
  }
54
55
 
@@ -94,13 +95,20 @@ export class CollapsibleActionGroup extends ActionsHandler {
94
95
 
95
96
  render() {
96
97
  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>
98
+ <div
99
+ class="${classMap({
100
+ actiongroup: true,
101
+ disabled: this.disabled,
102
+ })}"
103
+ >
104
+ ${this.getLoaderIcon}
105
+ <section class="action-buttons primary">
106
+ ${this.renderPrimaryActions}
107
+ </section>
108
+ <section class="action-buttons secondary">
109
+ ${this.renderSecondaryActions}
110
+ </section>
111
+ </div>
104
112
  `;
105
113
  }
106
114
 
@@ -177,11 +185,13 @@ export class CollapsibleActionGroup extends ActionsHandler {
177
185
  }
178
186
 
179
187
  /**
180
- * Click handler to emit custom event on action click
181
- * @param { string } eventName
182
- * @param { object } gaEvent
188
+ * Dispatches click events when patron clicks on action buttons
189
+ * @param { string } eventName - actions like 'browseBook', 'borrowBook' etc...
190
+ * @param { object } gaEvent - contains analytics event action and category
183
191
  * @param { string } gaEvent.category
184
192
  * @param { string } gaEvent.action
193
+ *
194
+ * @fires CollapsibleActionGroup#{eventName} - (will be browseBook, borrowBook etc...)
185
195
  */
186
196
  clickHandler(eventName, gaEvent) {
187
197
  this.dropdownState = 'close';
@@ -225,7 +235,14 @@ export class CollapsibleActionGroup extends ActionsHandler {
225
235
  * @returns { HTMLElement }
226
236
  */
227
237
  get getLoaderIcon() {
228
- return html`<img class="action-loader" alt="" src="${this.loaderIcon}" />`;
238
+ return html`<img
239
+ class="${classMap({
240
+ actionloader: true,
241
+ disabled: this.disabled,
242
+ })}"
243
+ alt=""
244
+ src="${this.loaderIcon}"
245
+ />`;
229
246
  }
230
247
 
231
248
  /**
@@ -8,24 +8,20 @@ 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
+ // return intentional error on dev environment
20
+ const shouldReturnError =
21
+ location?.href?.indexOf('?error=true') !== -1 &&
22
+ location?.hostname !== 'archive.org';
23
+
24
+ if (location?.pathname === '/demo/') baseHost = `/demo/`;
29
25
 
30
26
  let formData = new FormData();
31
27
  formData.append('action', option.action);
@@ -36,23 +32,37 @@ export default function ActionsHandlerService(options) {
36
32
  body: formData,
37
33
  })
38
34
  .then(response => {
39
- if (response.status === 200) {
40
- option.success.call();
35
+ const tokenError = 'loan token not found. please try again later.';
36
+ const borrowError =
37
+ 'This book is not available to borrow at this time. Please try again later.';
38
+
39
+ // return error reponse when not production and has ?error=true param...
40
+ const erroneousActions = ['browse_book', 'borrow_book', 'create_token'];
41
+ if (shouldReturnError && erroneousActions.includes(option?.action)) {
42
+ return {
43
+ success: false,
44
+ error: option?.action === 'create_token' ? tokenError : borrowError,
45
+ };
41
46
  }
42
- })
43
- .catch(error => {
44
- console.log(error);
45
- });
46
- }
47
47
 
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');
48
+ // return success response for /demo/ server...
49
+ if (baseHost == '/demo/1' || baseHost == '/demo/') {
50
+ return {
51
+ success: true,
52
+ message: 'operation executed successfully!',
53
+ };
54
+ }
53
55
 
54
- const actionLoader = collapsibleElement.shadowRoot.querySelector(
55
- '.action-loader'
56
- );
57
- actionLoader.setAttribute('style', 'visibility: visible');
56
+ // The response is a Response instance.
57
+ // You parse the data into a useable format using `.json()`
58
+ return response.json();
59
+ })
60
+ .then(data => {
61
+ // `data` is the parsed version of the JSON returned from the above endpoint.
62
+ if (!data?.error) {
63
+ option?.success(data);
64
+ } else {
65
+ option?.error(data);
66
+ }
67
+ });
58
68
  }
@@ -1,4 +1,6 @@
1
1
  import { LitElement } from 'lit-element';
2
+ import { LocalCache } from '@internetarchive/local-cache';
3
+
2
4
  import { URLHelper } from '../../config/url-helper.js';
3
5
  import ActionsHandlerService from './actions-handler-service.js';
4
6
 
@@ -7,33 +9,39 @@ import ActionsHandlerService from './actions-handler-service.js';
7
9
  *
8
10
  * ActionsHandlerService is a function being used to execute
9
11
  * 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.
12
+ * 1. identifier: book name.
14
13
  */
15
14
 
16
15
  export default class ActionsHandler extends LitElement {
17
16
  constructor(identifier) {
18
17
  super();
19
18
  this.identifier = identifier;
20
- this.ajaxTimeout = 6000;
19
+ this.ajaxTimeout = 7000;
20
+ this.localCache = new LocalCache();
21
21
  this.bindEvents();
22
22
  }
23
23
 
24
24
  sendEvent(eventCategory, eventAction) {
25
25
  // eslint-disable-next-line no-console
26
26
  console?.log('Book action: ', { eventCategory, eventAction });
27
- window?.archive_analytics?.send_event(
27
+ window?.archive_analytics?.send_event_no_sampling(
28
28
  eventCategory,
29
29
  eventAction,
30
- window.location.pathname
30
+ `identifier=${this.identifier}`
31
31
  );
32
32
  }
33
33
 
34
34
  bindEvents() {
35
35
  this.addEventListener('browseBook', ({ detail }) => {
36
36
  this.handleBrowseIt();
37
+ this.setConsecutiveLoanCounts();
38
+ const { category, action } = detail.event;
39
+ this.sendEvent(category, action);
40
+ });
41
+
42
+ this.addEventListener('browseBookAgain', ({ detail }) => {
43
+ this.handleBrowseIt();
44
+ this.setConsecutiveLoanCounts('browseAgain');
37
45
  const { category, action } = detail.event;
38
46
  this.sendEvent(category, action);
39
47
  });
@@ -90,56 +98,114 @@ export default class ActionsHandler extends LitElement {
90
98
  }
91
99
 
92
100
  handleBrowseIt() {
101
+ const action = 'browse_book';
102
+ this.dispatchToggleActionGroup();
103
+
93
104
  ActionsHandlerService({
94
- action: 'browse_book',
105
+ action,
95
106
  identifier: this.identifier,
96
107
  success: () => {
97
108
  this.handleReadItNow();
98
109
  },
110
+ error: data => {
111
+ this.dispatchActionError(action, data);
112
+ },
99
113
  });
100
114
  }
101
115
 
102
116
  handleReturnIt() {
117
+ const action = 'return_loan';
118
+ this.dispatchToggleActionGroup();
119
+
103
120
  ActionsHandlerService({
104
- action: 'return_loan',
121
+ action,
105
122
  identifier: this.identifier,
106
123
  success: () => {
107
124
  this.deleteLoanCookies();
108
125
  URLHelper.goToUrl(`/details/${this.identifier}`, true);
109
126
  },
127
+ error: data => {
128
+ this.dispatchActionError(action, data);
129
+ },
110
130
  });
111
131
  }
112
132
 
113
133
  handleBorrowIt() {
134
+ const action = 'borrow_book';
135
+ this.dispatchToggleActionGroup();
136
+
114
137
  ActionsHandlerService({
115
- action: 'borrow_book',
138
+ action,
116
139
  identifier: this.identifier,
117
140
  success: () => {
118
141
  this.handleReadItNow();
119
142
  },
143
+ error: data => {
144
+ this.dispatchActionError(action, data);
145
+ },
120
146
  });
121
147
  }
122
148
 
123
149
  handleReserveIt() {
150
+ const action = 'join_waitlist';
151
+ this.dispatchToggleActionGroup();
152
+
124
153
  ActionsHandlerService({
125
- action: 'join_waitlist',
154
+ action,
126
155
  identifier: this.identifier,
127
156
  success: () => {
128
157
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
129
158
  },
159
+ error: data => {
160
+ this.dispatchActionError(action, data);
161
+ },
130
162
  });
131
163
  }
132
164
 
133
165
  handleRemoveFromWaitingList() {
166
+ const action = 'leave_waitlist';
167
+ this.dispatchToggleActionGroup();
168
+
134
169
  ActionsHandlerService({
135
- action: 'leave_waitlist',
170
+ action,
136
171
  identifier: this.identifier,
137
172
  success: () => {
138
173
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
139
174
  },
175
+ error: data => {
176
+ this.dispatchActionError(action, data);
177
+ },
140
178
  });
141
179
  }
142
180
 
181
+
182
+ /**
183
+ * Dispatches event when an error occured on action
184
+ * Notes:- toggle <collapsible-action-group> visibility (enable/disable).
185
+ *
186
+ * @param { String } action - name of action like browse_book, borrow_book
187
+ * @param { Object } data - erroneous response from api call
188
+ *
189
+ * @fires ActionsHandler#lendingActionError
190
+ */
191
+ dispatchActionError(action, data = {}) {
192
+ this.dispatchEvent(
193
+ new CustomEvent('lendingActionError', {
194
+ detail: { action, data },
195
+ })
196
+ );
197
+ }
198
+
199
+ /**
200
+ * Dispatches event when patron is clicked on action buttons.
201
+ * Notes:- toggle <collapsible-action-group> disable/enable.
202
+ *
203
+ * @fires ActionsHandler#toggleActionGroup
204
+ */
205
+ dispatchToggleActionGroup() {
206
+ this.dispatchEvent(new CustomEvent('toggleActionGroup'));
207
+ }
208
+
143
209
  handleLoginOk() {
144
210
  /* eslint class-methods-use-this: "off" */
145
211
  const target = `/account/login?referer=${encodeURIComponent(
@@ -172,6 +238,29 @@ export default class ActionsHandler extends LitElement {
172
238
  }, this.ajaxTimeout);
173
239
  }
174
240
 
241
+ // save consecutive loan count for borrow
242
+ async setConsecutiveLoanCounts(action = '') {
243
+ try {
244
+ let newCount = 1;
245
+ const storageKey = `consecutive-loan-count`;
246
+ const existingCount = await this.localCache.get(storageKey);
247
+
248
+ // increase browse-count by 1 when you consecutive reading a book.
249
+ if (action === 'browseAgain' && existingCount !== undefined) {
250
+ newCount = existingCount ? existingCount + 1 : 1;
251
+ }
252
+
253
+ // set new value
254
+ await this.localCache.set({
255
+ key: storageKey,
256
+ value: newCount,
257
+ ttl: 7200, // 2 hours
258
+ });
259
+ } catch (error) {
260
+ console.log(error);
261
+ }
262
+ }
263
+
175
264
  deleteLoanCookies() {
176
265
  const date = new Date();
177
266
  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 ? 'browsed' : 'borrowed',
97
100
  };
98
101
  }
99
102
 
@@ -0,0 +1,103 @@
1
+ import { LitElement } from 'lit-element';
2
+ import { LocalCache } from '@internetarchive/local-cache';
3
+
4
+ import ActionsHandlerService from './actions-handler/actions-handler-service.js';
5
+
6
+ /**
7
+ * This class is used to create loan token for borrowed books
8
+ *
9
+ * ActionsHandlerService is a function being used to execute
10
+ */
11
+ export class LoanTokenPoller extends LitElement {
12
+ constructor() {
13
+ super();
14
+ this.identifier = '';
15
+ this.loanTokenPollingDelay = window.location.pathname === '/demo/' ? 2000 : 120000; // 120000 ms = 2 min
16
+ this.loanTokenInterval = undefined;
17
+ this.localCache = new LocalCache();
18
+ }
19
+
20
+ disconnectedCallback() {
21
+ this.loanTokenInterval = undefined;
22
+ }
23
+
24
+ async enableBookAccess(identifier, borrowType) {
25
+ this.identifier = identifier
26
+ let consecutiveLoanCounts = 1;
27
+
28
+ if (borrowType) {
29
+ // send consecutiveLoanCounts for browsed books only.
30
+ if (borrowType === 'browsed') {
31
+ try {
32
+ const existingCount = await this.localCache.get(
33
+ 'consecutive-loan-count'
34
+ );
35
+ consecutiveLoanCounts = existingCount ?? 1;
36
+ } catch (error) {
37
+ console.log(error);
38
+ }
39
+ }
40
+
41
+ // Do an initial token, then set an interval
42
+ this.handleLoanTokenPoller();
43
+
44
+ this.loanTokenInterval = setInterval(() => {
45
+ this.handleLoanTokenPoller();
46
+ }, this.loanTokenPollingDelay);
47
+
48
+ // event category and action for browsing book access
49
+ const category = `${borrowType}BookAccess`;
50
+ const action = `${
51
+ borrowType === 'browsed' ? 'BrowseCounts-' : 'Counts-'
52
+ }${consecutiveLoanCounts}`;
53
+ console.log(consecutiveLoanCounts)
54
+ this.sendEvent(category, action);
55
+ } else {
56
+ // if book is not browsed, just clear token polling interval
57
+ clearInterval(this.loanTokenInterval);
58
+ }
59
+ }
60
+
61
+ sendEvent(eventCategory, eventAction) {
62
+ // eslint-disable-next-line no-console
63
+ console?.log('Book action: ', { eventCategory, eventAction });
64
+ window?.archive_analytics?.send_event_no_sampling(
65
+ eventCategory,
66
+ eventAction,
67
+ `identifier=${this.identifier}`
68
+ );
69
+ }
70
+
71
+ async handleLoanTokenPoller() {
72
+ const action = 'create_token';
73
+ // await new Promise(r => setTimeout(r, 3000));
74
+ ActionsHandlerService({
75
+ identifier: this.identifier,
76
+ action,
77
+ error: data => {
78
+ console.log('error', data);
79
+ this.handleLendingActionError({detail: { action, data }})
80
+ // this.dispatchActionError(action, data);
81
+ clearInterval(this.loanTokenInterval); // stop token fetch api
82
+ },
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Dispatches event when an error occured on action
88
+ * Notes:- toggle <collapsible-action-group> visibility (enable/disable).
89
+ *
90
+ * @param { String } action - name of action like browse_book, borrow_book
91
+ * @param { Object } data - erroneous response from api call
92
+ *
93
+ * @fires ActionsHandler#lendingActionError
94
+ */
95
+ dispatchActionError(action, data = {}) {
96
+ console.log('disptaced');
97
+ this.dispatchEvent(
98
+ new CustomEvent('lendingActionError', {
99
+ detail: { action, data },
100
+ })
101
+ );
102
+ }
103
+ }
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable camelcase */
2
- import { html, css, LitElement } from 'lit-element';
2
+ import { html, css } 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';
@@ -10,8 +11,9 @@ import './components/info-icon.js';
10
11
 
11
12
  import GetLendingActions from './core/services/get-lending-actions.js';
12
13
  import { mobileContainerWidth } from './core/config/constants.js';
14
+ import { LoanTokenPoller } from './core/services/loan-token-poller.js';
13
15
 
14
- export default class IABookActions extends LitElement {
16
+ export default class IABookActions extends LoanTokenPoller {
15
17
  static get properties() {
16
18
  return {
17
19
  userid: { type: String },
@@ -20,8 +22,11 @@ export default class IABookActions extends LitElement {
20
22
  lendingStatus: { type: Object },
21
23
  width: { type: Number },
22
24
  bwbPurchaseUrl: { type: String },
25
+ lendingBarPostInit: { type: Function },
23
26
  barType: { type: String },
24
27
  sharedObserver: { attribute: false },
28
+ disableActionGroup: { type: Boolean },
29
+ modal: { Object },
25
30
  };
26
31
  }
27
32
 
@@ -33,13 +38,20 @@ export default class IABookActions extends LitElement {
33
38
  this.lendingStatus = {};
34
39
  this.width = 0;
35
40
  this.bwbPurchaseUrl = '';
41
+ this.lendingBarPostInit = () => {};
36
42
  this.barType = 'action'; // 'title'|'action'
37
43
  this.sharedObserver = undefined;
44
+ this.disableActionGroup = false;
45
+ this.modal = undefined;
46
+
47
+ // private props
38
48
  this.primaryActions = [];
39
49
  this.primaryTitle = '';
40
50
  this.primaryColor = 'primary';
41
51
  this.secondaryActions = [];
42
52
  this.lendingOptions = {};
53
+ this.borrowType = ''; // (browsed|borrowed)
54
+ this.consecutiveLoanCounts = 1; // consecutive loan count
43
55
  }
44
56
 
45
57
  disconnectedCallback() {
@@ -51,10 +63,10 @@ export default class IABookActions extends LitElement {
51
63
  this.sharedObserver = new SharedResizeObserver();
52
64
  this.setupResizeObserver();
53
65
  }
54
- this.setupLendingToolbarActions();
55
66
  }
56
67
 
57
68
  updated(changed) {
69
+ console.log(changed)
58
70
  if (changed.has('lendingStatus') || changed.has('bwbPurchaseUrl')) {
59
71
  this.setupLendingToolbarActions();
60
72
  this.update();
@@ -67,8 +79,7 @@ export default class IABookActions extends LitElement {
67
79
  }
68
80
 
69
81
  browseHasExpired() {
70
- const browsingExpired = true;
71
- const currStatus = { ...this.lendingStatus, browsingExpired };
82
+ const currStatus = { ...this.lendingStatus, browsingExpired: true };
72
83
  this.lendingStatus = currStatus;
73
84
  }
74
85
 
@@ -140,20 +151,34 @@ export default class IABookActions extends LitElement {
140
151
  return action != null;
141
152
  });
142
153
 
143
- if (
144
- this.lendingStatus.user_has_browsed &&
145
- !this.lendingStatus.browseHasExpired
146
- ) {
154
+ this.borrowType = actions.borrowType;
155
+ if (this.borrowType === 'browsed') {
156
+ // start timer for browsed.
157
+ // when browse is completed, we shows browse-again button
147
158
  this.startBrowseTimer();
148
159
  }
160
+
161
+ console.log('this.borrowType', this.borrowType);
162
+ // enable access of borrowed/browsed books
163
+ if (this.borrowType) {
164
+ await this.enableBookAccess(this.identifier, this.borrowType);
165
+ }
166
+
167
+ /**
168
+ * this callback function use to:-
169
+ * - disptach lenfingFlow::PostInit event
170
+ * - initialize bookreader using br.init()
171
+ */
172
+ console.log(this.lendingBarPostInit);
173
+ this.lendingBarPostInit();
149
174
  }
150
175
 
151
176
  render() {
152
- return html`
153
- <section class="lending-wrapper">
154
- ${this.barType === 'title' ? this.bookTitleBar : this.bookActionBar}
155
- </section>
156
- `;
177
+ if (this.barType === 'title') {
178
+ return html`<section class="lending-wrapper">${this.bookTitleBar}</section>`;
179
+ }
180
+
181
+ return html`<section class="lending-wrapper">${this.bookActionBar}</section>`;
157
182
  }
158
183
 
159
184
  get bookTitleBar() {
@@ -172,13 +197,102 @@ export default class IABookActions extends LitElement {
172
197
  .primaryActions=${this.primaryActions}
173
198
  .secondaryActions=${this.secondaryActions}
174
199
  .width=${this.width}
175
- .hasAdminAccess=${this.hasAdminAccess}
200
+ .borrowType=${this.borrowType}
201
+ ?hasAdminAccess=${this.hasAdminAccess}
202
+ ?disabled=${this.disableActionGroup}
203
+ @lendingActionError=${this.handleLendingActionError}
204
+ @toggleActionGroup=${this.handleToggleActionGroup}
176
205
  >
177
206
  </collapsible-action-group>
178
207
  ${this.textGroupTemplate} ${this.infoIconTemplate}
179
208
  `;
180
209
  }
181
210
 
211
+ /*
212
+ * custom event handler to toggle action group visibility
213
+ *
214
+ * @event IABookActions#toggleActionGroup
215
+ */
216
+ handleToggleActionGroup() {
217
+ this.disableActionGroup = !this.disableActionGroup;
218
+ }
219
+
220
+ /*
221
+ * handle lending errors occure during different operation like
222
+ * browse book, borrow book, borrowed book token etc...
223
+ *
224
+ * @event IABookActions#lendingActionError
225
+ * @param {Object} event - The employee who is responsible for the project
226
+ * @param {string} event.detail.action - action when error occurred like 'browseBook', 'borrowBook'
227
+ * @param {string} event.detail.data.error - error message
228
+ */
229
+ handleLendingActionError(event) {
230
+ // toggle activity loader
231
+ this.handleToggleActionGroup();
232
+
233
+ const action = event?.detail?.action;
234
+ const errorMsg = event?.detail?.data?.error;
235
+
236
+ if (errorMsg) this.showErrorModal(errorMsg, action);
237
+
238
+ // update action bar state if book is not available to browse or borrow.
239
+ if (errorMsg && errorMsg.match(/not available to borrow/gm)) {
240
+ let currStatus = this.lendingStatus;
241
+ if (action === 'browse_book') {
242
+ currStatus = {
243
+ ...this.lendingStatus,
244
+ available_to_browse: false,
245
+ };
246
+ } else if (action === 'borrow_book') {
247
+ currStatus = {
248
+ ...this.lendingStatus,
249
+ available_to_borrow: false,
250
+ };
251
+ }
252
+ this.lendingStatus = currStatus;
253
+ }
254
+ }
255
+
256
+ /* show error message if something went wrong */
257
+ async showErrorModal(errorMsg, action) {
258
+ this.disableActionGroup = false;
259
+
260
+ // check if this.modal passed as prop
261
+ if (!this.modal) {
262
+ this.modal = document.querySelector('modal-manager');
263
+
264
+ // check the DOM if <modal-manager> already there
265
+ if (!this.modal) this.modal = document.createElement('modal-manager');
266
+ }
267
+
268
+ this.modal.id = 'action-bar-modal';
269
+ await document.body.appendChild(this.modal);
270
+
271
+ const modalConfig = new ModalConfig({
272
+ title: 'Lending error',
273
+ message: errorMsg,
274
+ headerColor: '#d9534f',
275
+ });
276
+
277
+ if (action === 'create_token') {
278
+ modalConfig.closeOnBackdropClick = false;
279
+ modalConfig.showCloseButton = false;
280
+ modalConfig.message = html` Uh oh, something went wrong trying to access
281
+ this book.<br />
282
+ Please <a href="${() => window.location.reload(true)}">refresh</a> to
283
+ try again or send us an email to
284
+ <a
285
+ href="mailto:info@archive.org?subject=Help: cannot access my borrowed book: ${this
286
+ .identifier}"
287
+ >info@archive.org</a
288
+ >`;
289
+ }
290
+
291
+ this.modal.showModal({
292
+ config: modalConfig,
293
+ });
294
+ }
295
+
182
296
  get iconClass() {
183
297
  return this.width <= mobileContainerWidth ? 'mobile' : 'desktop';
184
298
  }
@@ -23,10 +23,12 @@ const container = ({
23
23
  text: 'Purchase',
24
24
  },
25
25
  ],
26
+ borrowType = '',
26
27
  } = {}) =>
27
28
  html`<collapsible-action-group
28
29
  .primaryActions=${primaryActions}
29
30
  .secondaryActions=${secondaryActions}
31
+ .borrowType=${borrowType}
30
32
  ></collapsible-action-group>`;
31
33
 
32
34
  describe('<collapsible-action-group>', () => {
@@ -39,6 +41,22 @@ describe('<collapsible-action-group>', () => {
39
41
  const primaryButton = primaryActionContainer.querySelector('.ia-button');
40
42
  expect(primaryButton.innerText).to.equal('Borrow for 1 hour');
41
43
  });
44
+
45
+ it('check if loader is active and action-group is disabled', async () => {
46
+ const el = await fixture(container());
47
+
48
+ const primaryActionContainer = el.shadowRoot.querySelector('.primary');
49
+ primaryActionContainer
50
+ .querySelector('.ia-button')
51
+ .dispatchEvent(new CustomEvent('toggle-loader'));
52
+ el.disabled = true;
53
+ await el.updateComplete;
54
+
55
+ expect(el.disabled).to.be.true;
56
+ expect(el.shadowRoot.querySelector('div').classList.contains('disabled')).to
57
+ .be.true;
58
+ });
59
+
42
60
  it('fires analytics events', async () => {
43
61
  const el = await fixture(container());
44
62
  const primaryActionContainer = el.shadowRoot.querySelector('.primary');
@@ -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({