@internetarchive/ia-book-actions 0.0.7-1 → 0.0.7-2

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
@@ -76,13 +76,9 @@
76
76
  overflow: hidden;
77
77
  }
78
78
 
79
- modal-manager {
79
+ modal-manager[mode='closed'] {
80
80
  display: none;
81
81
  }
82
-
83
- modal-manager[mode='open'] {
84
- display: block;
85
- }
86
82
  </style>
87
83
  </head>
88
84
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/ia-book-actions",
3
- "version": "0.0.7-1",
3
+ "version": "0.0.7-02",
4
4
  "description": "Bookreader actions for borrowable book",
5
5
  "author": "Internet Archive",
6
6
  "license": "AGPL-3.0-only",
@@ -66,20 +66,23 @@ export class CollapsibleActionGroup extends ActionsHandler {
66
66
  /* emit custom event to fetch loan token */
67
67
  emitEnableBookAccess() {
68
68
  // send consecutiveLoanCounts for browsed books only.
69
- // for borrowed books, we just send [Counts-1]
70
- if (this.borrowType === 'browsed') {
69
+ if (this.borrowType === 'browsing') {
71
70
  this.consecutiveLoanCounts =
72
71
  localStorage.getItem('consecutive-loan-count') ?? 1;
73
72
  }
74
73
 
74
+ // event category and action for browsing book access
75
+ const eventCategory = `${this.borrowType}BookAccess`;
76
+ const eventAction = `${
77
+ this.borrowType === 'browsing' ? 'BrowseCounts-' : 'Counts-'
78
+ }${this.consecutiveLoanCounts}`;
79
+
75
80
  this.dispatchEvent(
76
81
  new CustomEvent('enableBookAccess', {
77
82
  detail: {
78
83
  event: {
79
- category: `${this.borrowType}BookAccess`, // browsedBookAccess | borrowedBookAccess
80
- action: `${
81
- this.borrowType === 'browsed' ? 'BrowseCounts-' : 'Counts-'
82
- }${this.consecutiveLoanCounts}`, // (BrowseCounts-1|Counts-X)
84
+ category: eventCategory, // brosingBookAccess | borrowingBookAccess
85
+ action: eventAction, // (BrowseCounts-N|Counts-N)
83
86
  },
84
87
  borrowType: this.borrowType,
85
88
  },
@@ -13,12 +13,10 @@ export default function ActionsHandlerService(options) {
13
13
  ...options,
14
14
  };
15
15
 
16
- let baseHost = '';
17
- if (window.location.pathname === '/demo/') {
18
- baseHost = `/demo/`;
19
- } else {
20
- baseHost = `/services/loans/loan`;
21
- }
16
+ let baseHost = '/services/loans/loan';
17
+ const location = window?.location;
18
+
19
+ if (location?.pathname === '/demo/') baseHost = `/demo/`;
22
20
 
23
21
  let formData = new FormData();
24
22
  formData.append('action', option.action);
@@ -29,18 +27,17 @@ export default function ActionsHandlerService(options) {
29
27
  body: formData,
30
28
  })
31
29
  .then(response => {
32
- let shouldReturnError =
33
- window.location.href.indexOf('?error=true') !== -1;
30
+ let shouldReturnError = location?.href?.indexOf('?error=true') !== -1;
34
31
  const tokenError = 'loan token not found. please try again later.';
35
32
  const borrowError =
36
33
  'This book is not available to borrow at this time. Please try again later.';
37
34
 
38
35
  // return error reponse if query param has ?error=true param...
39
36
  const erroneousActions = ['browse_book', 'borrow_book', 'create_token'];
40
- if (shouldReturnError && erroneousActions.includes(option.action)) {
37
+ if (shouldReturnError && erroneousActions.includes(option?.action)) {
41
38
  return {
42
39
  success: false,
43
- error: option.action === 'create_token' ? tokenError : borrowError,
40
+ error: option?.action === 'create_token' ? tokenError : borrowError,
44
41
  };
45
42
  }
46
43
 
@@ -58,7 +55,7 @@ export default function ActionsHandlerService(options) {
58
55
  })
59
56
  .then(data => {
60
57
  // `data` is the parsed version of the JSON returned from the above endpoint.
61
- if (!data.error) {
58
+ if (!data?.error) {
62
59
  option?.success(data);
63
60
  } else {
64
61
  option?.error(data);
@@ -17,7 +17,7 @@ export default class ActionsHandler extends LitElement {
17
17
  this.ajaxTimeout = 6000;
18
18
  this.loanTokenPollingDelay =
19
19
  window.location.pathname === '/demo/' ? 2000 : 120000; // 120000 ms = 2 min
20
- this.loanTokenInterval = null;
20
+ this.loanTokenInterval = undefined;
21
21
  this.bindEvents();
22
22
  }
23
23
 
@@ -105,8 +105,6 @@ export default class ActionsHandler extends LitElement {
105
105
 
106
106
  // fetch loan token for browsed/borrowed book and set an interval
107
107
  if (borrowType) {
108
- console.log('token poll started...');
109
-
110
108
  this.loanTokenInterval = setInterval(() => {
111
109
  this.handleLoanTokenPoller();
112
110
  }, this.loanTokenPollingDelay);
@@ -122,7 +120,7 @@ export default class ActionsHandler extends LitElement {
122
120
 
123
121
  handleBrowseIt() {
124
122
  const context = 'browse_book';
125
- this.ActionError(context);
123
+ this.dispatchToggleActionGroup();
126
124
 
127
125
  ActionsHandlerService({
128
126
  action: context,
@@ -131,14 +129,14 @@ export default class ActionsHandler extends LitElement {
131
129
  this.handleReadItNow();
132
130
  },
133
131
  error: data => {
134
- this.ActionError(context, data);
132
+ this.dispatchActionError(context, data);
135
133
  },
136
134
  });
137
135
  }
138
136
 
139
137
  handleReturnIt() {
140
138
  const context = 'return_loan';
141
- this.ActionError(context);
139
+ this.dispatchToggleActionGroup();
142
140
 
143
141
  ActionsHandlerService({
144
142
  action: context,
@@ -148,14 +146,14 @@ export default class ActionsHandler extends LitElement {
148
146
  URLHelper.goToUrl(`/details/${this.identifier}`, true);
149
147
  },
150
148
  error: data => {
151
- this.ActionError(context, data);
149
+ this.dispatchActionError(context, data);
152
150
  },
153
151
  });
154
152
  }
155
153
 
156
154
  handleBorrowIt() {
157
155
  const context = 'borrow_book';
158
- this.ActionError(context);
156
+ this.dispatchToggleActionGroup();
159
157
 
160
158
  ActionsHandlerService({
161
159
  action: context,
@@ -164,14 +162,14 @@ export default class ActionsHandler extends LitElement {
164
162
  this.handleReadItNow();
165
163
  },
166
164
  error: data => {
167
- this.ActionError(context, data);
165
+ this.dispatchActionError(context, data);
168
166
  },
169
167
  });
170
168
  }
171
169
 
172
170
  handleReserveIt() {
173
171
  const context = 'join_waitlist';
174
- this.ActionError(context);
172
+ this.dispatchToggleActionGroup();
175
173
 
176
174
  ActionsHandlerService({
177
175
  action: context,
@@ -180,14 +178,14 @@ export default class ActionsHandler extends LitElement {
180
178
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
181
179
  },
182
180
  error: data => {
183
- this.ActionError(context, data);
181
+ this.dispatchActionError(context, data);
184
182
  },
185
183
  });
186
184
  }
187
185
 
188
186
  handleRemoveFromWaitingList() {
189
187
  const context = 'leave_waitlist';
190
- this.ActionError(context);
188
+ this.dispatchToggleActionGroup();
191
189
 
192
190
  ActionsHandlerService({
193
191
  action: context,
@@ -196,7 +194,7 @@ export default class ActionsHandler extends LitElement {
196
194
  URLHelper.goToUrl(URLHelper.getRedirectUrl(), true);
197
195
  },
198
196
  error: data => {
199
- this.ActionError(context, data);
197
+ this.dispatchActionError(context, data);
200
198
  },
201
199
  });
202
200
  }
@@ -207,13 +205,13 @@ export default class ActionsHandler extends LitElement {
207
205
  identifier: this.identifier,
208
206
  action: context,
209
207
  error: data => {
210
- this.ActionError(context, data);
208
+ this.dispatchActionError(context, data);
211
209
  clearInterval(this.loanTokenInterval); // stop token fetch api
212
210
  },
213
211
  });
214
212
  }
215
213
 
216
- ActionError(context, data = {}) {
214
+ dispatchActionError(context, data = {}) {
217
215
  this.dispatchEvent(
218
216
  new CustomEvent('lendingActionError', {
219
217
  detail: { context, data },
@@ -221,6 +219,10 @@ export default class ActionsHandler extends LitElement {
221
219
  );
222
220
  }
223
221
 
222
+ dispatchToggleActionGroup() {
223
+ this.dispatchEvent(new CustomEvent('toggleActionGroup'));
224
+ }
225
+
224
226
  handleLoginOk() {
225
227
  /* eslint class-methods-use-this: "off" */
226
228
  const target = `/account/login?referer=${encodeURIComponent(
@@ -75,9 +75,9 @@ export default class GetLendingActions {
75
75
  const disableBorrow = lendingStatus.loanCount >= lendingStatus.maxLoans;
76
76
 
77
77
  let primaryTitleText = '';
78
- let isBrowsed =
78
+ let isBrowsing =
79
79
  lendingStatus.user_has_browsed && !lendingStatus.browsingExpired;
80
- if (isBrowsed) {
80
+ if (isBrowsing) {
81
81
  primaryTitleText = this.getBrowseCountdownTitle();
82
82
  } else {
83
83
  primaryTitleText = `Your loan of this book has ${lendingStatus.daysLeftOnLoan} days left.`;
@@ -96,7 +96,7 @@ export default class GetLendingActions {
96
96
  this.actionsConfig.adminAccessConfig(),
97
97
  this.actionsConfig.purchaseConfig(),
98
98
  ],
99
- borrowType: isBrowsed ? 'browsed' : 'borrowed',
99
+ borrowType: isBrowsing ? 'browsing' : 'borrowing',
100
100
  };
101
101
  }
102
102
 
@@ -10,8 +10,6 @@ import './components/text-group.js';
10
10
  import './components/info-icon.js';
11
11
 
12
12
  import GetLendingActions from './core/services/get-lending-actions.js';
13
- import getRealTimeLendingStatus from './core/services/get-realtime-lending-status.js';
14
-
15
13
  import { mobileContainerWidth } from './core/config/constants.js';
16
14
 
17
15
  export default class IABookActions extends LitElement {
@@ -26,7 +24,6 @@ export default class IABookActions extends LitElement {
26
24
  barType: { type: String },
27
25
  sharedObserver: { attribute: false },
28
26
  disableActionGroup: { type: Boolean },
29
- modalConfig: { type: Object },
30
27
  };
31
28
  }
32
29
 
@@ -46,8 +43,6 @@ export default class IABookActions extends LitElement {
46
43
  this.secondaryActions = [];
47
44
  this.lendingOptions = {};
48
45
  this.disableActionGroup = false;
49
- this.modalConfig = {};
50
- this.realtimeInternal = 0; // store intervalID
51
46
  }
52
47
 
53
48
  disconnectedCallback() {
@@ -60,25 +55,7 @@ export default class IABookActions extends LitElement {
60
55
  this.setupResizeObserver();
61
56
  }
62
57
 
63
- if (!Object.keys(this.modalConfig).length) {
64
- this.modalConfig = new ModalConfig();
65
- this.modalConfig.headerColor = '#d9534f';
66
- }
67
58
  this.setupLendingToolbarActions();
68
-
69
- // fetch latest lending status after an internal
70
- if (
71
- this.lendingStatus.user_has_browsed ||
72
- this.lendingStatus.user_has_borrowed
73
- ) {
74
- this.realtimeInternal = setInterval(() => {
75
- const latestLendingStatus = getRealTimeLendingStatus(
76
- 'naturalhistoryof00unse_4'
77
- );
78
- console.log(latestLendingStatus);
79
- if (latestLendingStatus) this.lendingStatus = latestLendingStatus;
80
- }, 30000); // 60000 ms = 1 min
81
- }
82
59
  }
83
60
 
84
61
  updated(changed) {
@@ -167,8 +144,8 @@ export default class IABookActions extends LitElement {
167
144
  });
168
145
 
169
146
  this.borrowType = actions.borrowType;
170
- if (this.borrowType === 'browsed') {
171
- // start timer for browsed.
147
+ if (this.borrowType === 'browsing') {
148
+ // start timer for browsing.
172
149
  // when browse is completed, we shows browse-again button
173
150
  this.startBrowseTimer();
174
151
  }
@@ -202,6 +179,7 @@ export default class IABookActions extends LitElement {
202
179
  ?hasAdminAccess=${this.hasAdminAccess}
203
180
  ?disabled=${this.disableActionGroup}
204
181
  @lendingActionError=${this.handleLendingActionError}
182
+ @toggleActionGroup=${this.handleToggleActionGroup}
205
183
  >
206
184
  </collapsible-action-group>
207
185
  ${this.textGroupTemplate} ${this.infoIconTemplate}
@@ -209,17 +187,32 @@ export default class IABookActions extends LitElement {
209
187
  }
210
188
 
211
189
  /*
212
- * handle lending errors occure on different operation like
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
213
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.context - action when error occurred like 'browseBook', 'borrowBook'
205
+ * @param {string} event.detail.data.error - error message
214
206
  */
215
- handleLendingActionError(e) {
207
+ handleLendingActionError(event) {
208
+ console.log(event?.detail?.data);
216
209
  // toggle activity loader
217
- this.disableActionGroup = !this.disableActionGroup;
210
+ this.handleToggleActionGroup();
218
211
 
219
- const context = e?.detail?.context;
220
- const errorMsg = e?.detail?.data?.error;
212
+ const context = event?.detail?.context;
213
+ const errorMsg = event?.detail?.data?.error;
221
214
 
222
- if (errorMsg) this.showErrorModal(context, errorMsg);
215
+ if (errorMsg) this.showErrorModal(errorMsg);
223
216
 
224
217
  // update action bar state if book is not available to browse or borrow.
225
218
  if (errorMsg && errorMsg.match(/not available to borrow/gm)) {
@@ -240,24 +233,22 @@ export default class IABookActions extends LitElement {
240
233
  }
241
234
 
242
235
  /* show error message if something went wrong */
243
- async showErrorModal(context, errorMsg) {
244
- // add modal-manager in DOM to show alert message
245
- this.modal = document.createElement('modal-manager');
246
- this.modal.id = 'action-bar-modal';
247
- await document.body.appendChild(this.modal);
236
+ async showErrorModal(errorMsg) {
237
+ this.disableActionGroup = false;
248
238
 
249
- // fallback if <modal-manager> is not found!
239
+ this.modal = document.querySelector('#action-bar-modal');
250
240
  if (!this.modal) {
251
- alert(errorMsg);
252
- return;
241
+ this.modal = document.createElement('modal-manager');
242
+ this.modal.id = 'action-bar-modal';
253
243
  }
254
-
255
- this.disableActionGroup = false;
256
- this.modalConfig.title = 'Lending error';
257
- this.modalConfig.message = errorMsg;
244
+ await document.body.appendChild(this.modal);
258
245
 
259
246
  this.modal.showModal({
260
- config: this.modalConfig,
247
+ config: new ModalConfig({
248
+ title: 'Lending error',
249
+ message: errorMsg,
250
+ headerColor: '#d9534f',
251
+ }),
261
252
  });
262
253
  }
263
254
 
@@ -87,7 +87,7 @@ describe('<collapsible-action-group>', () => {
87
87
  });
88
88
 
89
89
  describe('fetch loan token and localStorage for browsed/borrowsed book', () => {
90
- beforeEach(() => {
90
+ afterEach(() => {
91
91
  const localStorageMock = {
92
92
  getItem: Sinon.spy(),
93
93
  setItem: Sinon.spy(),
@@ -1,31 +0,0 @@
1
- /*
2
- * Helper function to fetch real-time lending status
3
- */
4
- export default function getRealTimeLendingStatus(identifier) {
5
- let url = `https://ia-petabox-neeraj.archive.org/services/book/lending-info.php?identifier=${identifier}`;
6
-
7
- if (window.location.pathname === '/demo/') {
8
- url = ''; // change URL to prevent error on localhost
9
- }
10
-
11
- fetch(url)
12
- .then(response => {
13
- // return { response_code: '400', data: 'error happened' };
14
- return response.json();
15
- })
16
- .then(response => {
17
- console.log(response);
18
- if (response?.response_code === 200) {
19
- const lendingInfo = response?.body?.data?.lendingInfo;
20
- let newLendingInfo = lendingInfo;
21
- const newLendingStatus = lendingInfo?.lendingStatus;
22
- newLendingInfo = { ...newLendingInfo, ...newLendingStatus };
23
- newLendingInfo.lendingStatus = '';
24
- return newLendingInfo;
25
- }
26
- return null;
27
- })
28
- .catch(response => {
29
- return response;
30
- });
31
- }