tonder-web-sdk 1.4.0 → 1.8.0
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 +1 -0
- package/package.json +1 -1
- package/src/classes/inlineCheckout.js +209 -26
- package/src/data/api.js +63 -0
- package/src/helpers/skyflow.js +12 -2
- package/src/helpers/template-skeleton.js +59 -0
- package/src/helpers/template.js +337 -7
- package/src/helpers/utils.js +71 -0
- package/src/index-dev.js +2 -1
- package/v1/bundle.min.js +3 -3
package/README.md
CHANGED
|
@@ -160,6 +160,7 @@ const response = await inlineCheckout.payment(checkoutData);
|
|
|
160
160
|
## Configuration
|
|
161
161
|
| Property | Type | Description |
|
|
162
162
|
|:---------------:|:-------------:|:---------------------------------------------------:|
|
|
163
|
+
| mode | string | 'stage' 'production' 'sandbox', default 'stage' |
|
|
163
164
|
| apiKey | string | You can take this from you Tonder Dashboard |
|
|
164
165
|
| backgroundColor | string | Hex color #000000 |
|
|
165
166
|
| returnUrl | string | |
|
package/package.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { cardTemplate } from '../helpers/template.js'
|
|
1
|
+
import { cardItemsTemplate, cardTemplate } from '../helpers/template.js'
|
|
2
|
+
import { cardTemplateSkeleton } from '../helpers/template-skeleton.js'
|
|
2
3
|
import {
|
|
3
4
|
getBusiness,
|
|
4
5
|
customerRegister,
|
|
5
6
|
createOrder,
|
|
6
7
|
createPayment,
|
|
7
8
|
startCheckoutRouter,
|
|
8
|
-
getOpenpayDeviceSessionID
|
|
9
|
+
getOpenpayDeviceSessionID,
|
|
10
|
+
getCustomerCards,
|
|
11
|
+
registerCard,
|
|
12
|
+
deleteCustomerCard
|
|
9
13
|
} from '../data/api';
|
|
10
14
|
import {
|
|
11
15
|
showError,
|
|
12
16
|
getBrowserInfo,
|
|
17
|
+
mapCards,
|
|
18
|
+
showMessage,
|
|
13
19
|
} from '../helpers/utils';
|
|
14
20
|
import { initSkyflow } from '../helpers/skyflow'
|
|
15
21
|
import { ThreeDSHandler } from './3dsHandler.js';
|
|
@@ -17,22 +23,36 @@ import { ThreeDSHandler } from './3dsHandler.js';
|
|
|
17
23
|
|
|
18
24
|
export class InlineCheckout {
|
|
19
25
|
static injected = false;
|
|
26
|
+
static cardsInjected = false
|
|
20
27
|
customer = {}
|
|
21
28
|
items = []
|
|
22
|
-
baseUrl =
|
|
29
|
+
baseUrl = null
|
|
23
30
|
collectContainer = null
|
|
24
31
|
merchantData = {}
|
|
25
32
|
cartTotal = null
|
|
26
33
|
metadata = {}
|
|
27
34
|
card = {}
|
|
35
|
+
collectorIds = {
|
|
36
|
+
cardsListContainer: "cardsListContainer",
|
|
37
|
+
holderName: "collectCardholderName",
|
|
38
|
+
cardNumber: "collectCardNumber",
|
|
39
|
+
expirationMonth: "collectExpirationMonth",
|
|
40
|
+
expirationYear: "collectExpirationYear",
|
|
41
|
+
cvv: "collectCvv",
|
|
42
|
+
tonderPayButton: "tonderPayButton",
|
|
43
|
+
msgError: "msgError",
|
|
44
|
+
msgNotification: "msgNotification"
|
|
45
|
+
|
|
46
|
+
}
|
|
28
47
|
|
|
29
48
|
constructor({
|
|
49
|
+
mode = "stage",
|
|
30
50
|
apiKey,
|
|
31
51
|
returnUrl,
|
|
32
52
|
successUrl,
|
|
33
53
|
renderPaymentButton = false,
|
|
34
54
|
callBack = () => { },
|
|
35
|
-
styles
|
|
55
|
+
styles
|
|
36
56
|
}) {
|
|
37
57
|
this.apiKeyTonder = apiKey;
|
|
38
58
|
this.returnUrl = returnUrl;
|
|
@@ -40,6 +60,8 @@ export class InlineCheckout {
|
|
|
40
60
|
this.renderPaymentButton = renderPaymentButton;
|
|
41
61
|
this.callBack = callBack;
|
|
42
62
|
this.customStyles = styles
|
|
63
|
+
this.mode = mode
|
|
64
|
+
this.baseUrl = this.#getBaseUrl()
|
|
43
65
|
|
|
44
66
|
this.abortController = new AbortController()
|
|
45
67
|
this.process3ds = new ThreeDSHandler(
|
|
@@ -47,6 +69,17 @@ export class InlineCheckout {
|
|
|
47
69
|
)
|
|
48
70
|
}
|
|
49
71
|
|
|
72
|
+
#getBaseUrl() {
|
|
73
|
+
const modeUrls = {
|
|
74
|
+
'production': 'https://app.tonder.io',
|
|
75
|
+
'sandbox': 'https://sandbox.tonder.io',
|
|
76
|
+
'stage': 'https://stage.tonder.io',
|
|
77
|
+
'development': 'http://localhost:8000',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return modeUrls[this.mode] || modeUrls['stage']
|
|
81
|
+
}
|
|
82
|
+
|
|
50
83
|
#mountPayButton() {
|
|
51
84
|
if (!this.renderPaymentButton) return;
|
|
52
85
|
|
|
@@ -157,6 +190,9 @@ export class InlineCheckout {
|
|
|
157
190
|
this.cartItems = items
|
|
158
191
|
}
|
|
159
192
|
|
|
193
|
+
setCustomerEmail (email) {
|
|
194
|
+
this.email = email
|
|
195
|
+
}
|
|
160
196
|
setCartTotal(total) {
|
|
161
197
|
console.log('total: ', total)
|
|
162
198
|
this.cartTotal = total
|
|
@@ -176,14 +212,46 @@ export class InlineCheckout {
|
|
|
176
212
|
injectCheckout() {
|
|
177
213
|
if (InlineCheckout.injected) return
|
|
178
214
|
this.process3ds.verifyTransactionStatus()
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
215
|
+
const containerTonderCheckout = document.querySelector("#tonder-checkout");
|
|
216
|
+
if (containerTonderCheckout) {
|
|
217
|
+
this.#mount(containerTonderCheckout)
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const observer = new MutationObserver((mutations, obs) => {
|
|
221
|
+
const containerTonderCheckout = document.querySelector("#tonder-checkout");
|
|
222
|
+
if (containerTonderCheckout) {
|
|
223
|
+
this.#mount(containerTonderCheckout)
|
|
224
|
+
obs.disconnect();
|
|
185
225
|
}
|
|
186
|
-
}
|
|
226
|
+
});
|
|
227
|
+
observer.observe(document.body, {
|
|
228
|
+
childList: true,
|
|
229
|
+
subtree: true,
|
|
230
|
+
attributeFilter: ['id']
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
#addGlobalLoader() {
|
|
236
|
+
let checkoutContainer = document.querySelector("#global-loader");
|
|
237
|
+
if (checkoutContainer) {
|
|
238
|
+
checkoutContainer.innerHTML = cardTemplateSkeleton;
|
|
239
|
+
checkoutContainer.style.display = 'block';
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#removeGlobalLoader() {
|
|
244
|
+
const loader = document.querySelector('#global-loader');
|
|
245
|
+
if (loader) {
|
|
246
|
+
loader.style.display = 'none';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#mount(containerTonderCheckout){
|
|
251
|
+
containerTonderCheckout.innerHTML = cardTemplate;
|
|
252
|
+
this.#addGlobalLoader();
|
|
253
|
+
this.#mountTonder();
|
|
254
|
+
InlineCheckout.injected = true;
|
|
187
255
|
}
|
|
188
256
|
|
|
189
257
|
async #fetchMerchantData() {
|
|
@@ -199,26 +267,48 @@ export class InlineCheckout {
|
|
|
199
267
|
return await customerRegister(this.baseUrl, this.apiKeyTonder, customer, signal);
|
|
200
268
|
}
|
|
201
269
|
|
|
202
|
-
async #mountTonder() {
|
|
270
|
+
async #mountTonder(getCards = true) {
|
|
203
271
|
this.#mountPayButton()
|
|
272
|
+
try{
|
|
273
|
+
const {
|
|
274
|
+
vault_id,
|
|
275
|
+
vault_url,
|
|
276
|
+
} = await this.#fetchMerchantData();
|
|
277
|
+
if(this.email && getCards){
|
|
278
|
+
const customerResponse = await this.getCustomer({email: this.email});
|
|
279
|
+
if("auth_token" in customerResponse) {
|
|
280
|
+
const { auth_token } = customerResponse
|
|
281
|
+
const cards = await getCustomerCards(this.baseUrl, auth_token);
|
|
282
|
+
|
|
283
|
+
if("cards" in cards) {
|
|
284
|
+
const cardsMapped = cards.cards.map(mapCards)
|
|
285
|
+
this.#loadCardsList(cardsMapped, auth_token)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
204
289
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
290
|
+
this.collectContainer = await initSkyflow(
|
|
291
|
+
vault_id,
|
|
292
|
+
vault_url,
|
|
293
|
+
this.baseUrl,
|
|
294
|
+
this.apiKeyTonder,
|
|
295
|
+
this.abortController.signal,
|
|
296
|
+
this.customStyles,
|
|
297
|
+
);
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
this.#removeGlobalLoader()
|
|
300
|
+
}, 800)
|
|
301
|
+
}catch(e){
|
|
302
|
+
if (e && e.name !== 'AbortError') {
|
|
303
|
+
this.#removeGlobalLoader()
|
|
304
|
+
showError("No se pudieron cargar los datos del comercio.")
|
|
305
|
+
}
|
|
306
|
+
}
|
|
218
307
|
}
|
|
219
308
|
|
|
220
309
|
removeCheckout() {
|
|
221
310
|
InlineCheckout.injected = false
|
|
311
|
+
InlineCheckout.cardsInjected = false
|
|
222
312
|
// Cancel all requests
|
|
223
313
|
this.abortController.abort();
|
|
224
314
|
this.abortController = new AbortController();
|
|
@@ -230,7 +320,7 @@ export class InlineCheckout {
|
|
|
230
320
|
async #getCardTokens() {
|
|
231
321
|
if (this.card?.skyflow_id) return this.card
|
|
232
322
|
try {
|
|
233
|
-
const collectResponse = await this.collectContainer.collect();
|
|
323
|
+
const collectResponse = await this.collectContainer.container.collect();
|
|
234
324
|
const cardTokens = await collectResponse["records"][0]["fields"];
|
|
235
325
|
return cardTokens;
|
|
236
326
|
} catch (error) {
|
|
@@ -264,7 +354,23 @@ export class InlineCheckout {
|
|
|
264
354
|
this.customer,
|
|
265
355
|
this.abortController.signal
|
|
266
356
|
)
|
|
357
|
+
if(auth_token && this.email){
|
|
358
|
+
const saveCard = document.getElementById("save-checkout-card");
|
|
359
|
+
if(saveCard && "checked" in saveCard && saveCard.checked){
|
|
360
|
+
await registerCard(this.baseUrl, auth_token, { skyflow_id: cardTokens.skyflow_id });
|
|
361
|
+
|
|
362
|
+
this.cardsInjected = false;
|
|
363
|
+
|
|
364
|
+
const cards = await getCustomerCards(this.baseUrl, auth_token);
|
|
365
|
+
if("cards" in cards) {
|
|
366
|
+
const cardsMapped = cards.cards.map((card) => mapCards(card))
|
|
367
|
+
this.#loadCardsList(cardsMapped, auth_token)
|
|
368
|
+
}
|
|
267
369
|
|
|
370
|
+
showMessage("Tarjeta registrada con éxito", this.collectorIds.msgNotification);
|
|
371
|
+
|
|
372
|
+
}
|
|
373
|
+
}
|
|
268
374
|
var orderItems = {
|
|
269
375
|
business: this.apiKeyTonder,
|
|
270
376
|
client: auth_token,
|
|
@@ -346,4 +452,81 @@ export class InlineCheckout {
|
|
|
346
452
|
throw error;
|
|
347
453
|
}
|
|
348
454
|
};
|
|
455
|
+
|
|
456
|
+
#loadCardsList (cards, token) {
|
|
457
|
+
if(this.cardsInjected) return;
|
|
458
|
+
const injectInterval = setInterval(() => {
|
|
459
|
+
const queryElement = document.querySelector(`#${this.collectorIds.cardsListContainer}`);
|
|
460
|
+
if (queryElement && InlineCheckout.injected) {
|
|
461
|
+
queryElement.innerHTML = cardItemsTemplate(cards)
|
|
462
|
+
clearInterval(injectInterval)
|
|
463
|
+
this.#mountRadioButtons(token)
|
|
464
|
+
this.cardsInjected = true
|
|
465
|
+
}
|
|
466
|
+
}, 500);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#mountRadioButtons (token) {
|
|
470
|
+
const radioButtons = document.getElementsByName(`card_selected`);
|
|
471
|
+
for (const radio of radioButtons) {
|
|
472
|
+
radio.style.display = "block";
|
|
473
|
+
radio.onclick = async (event) => {
|
|
474
|
+
await this.#handleRadioButtonClick(radio);
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const cardsButtons = document.getElementsByClassName("card-delete-button");
|
|
478
|
+
for (const cardButton of cardsButtons) {
|
|
479
|
+
cardButton.addEventListener("click", (event) => {
|
|
480
|
+
event.preventDefault();
|
|
481
|
+
this.#handleDeleteCardButtonClick(token, cardButton)
|
|
482
|
+
}, false);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async #handleRadioButtonClick (radio) {
|
|
487
|
+
if(radio.id === this.radioChecked || ( radio.id === "new" && this.radioChecked === undefined)) return;
|
|
488
|
+
const containerForm = document.querySelector(".container-form");
|
|
489
|
+
if(containerForm) {
|
|
490
|
+
containerForm.style.display = radio.id === "new" ? "block" : "none";
|
|
491
|
+
}
|
|
492
|
+
if(radio.id === "new") {
|
|
493
|
+
if(this.radioChecked !== radio.id) {
|
|
494
|
+
this.#addGlobalLoader()
|
|
495
|
+
this.#mountTonder(false);
|
|
496
|
+
InlineCheckout.injected = true;
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
this.#unmountForm();
|
|
500
|
+
}
|
|
501
|
+
this.radioChecked = radio.id;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async #handleDeleteCardButtonClick (customerToken, button) {
|
|
505
|
+
const id = button.attributes.getNamedItem("id")
|
|
506
|
+
const skyflow_id = id?.value?.split("_")?.[2]
|
|
507
|
+
if(skyflow_id) {
|
|
508
|
+
const cardClicked = document.querySelector(`#card_container-${skyflow_id}`);
|
|
509
|
+
if(cardClicked) {
|
|
510
|
+
cardClicked.style.display = "none"
|
|
511
|
+
}
|
|
512
|
+
await deleteCustomerCard(this.baseUrl, customerToken, skyflow_id)
|
|
513
|
+
this.cardsInjected = false
|
|
514
|
+
const cards = await getCustomerCards(this.baseUrl, customerToken)
|
|
515
|
+
if("cards" in cards) {
|
|
516
|
+
const cardsMapped = cards.cards.map(mapCards)
|
|
517
|
+
this.#loadCardsList(cardsMapped, customerToken)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
#unmountForm () {
|
|
523
|
+
InlineCheckout.injected = false
|
|
524
|
+
if(this.collectContainer) {
|
|
525
|
+
if("unmount" in this.collectContainer.elements.cardHolderNameElement) this.collectContainer.elements.cardHolderNameElement.unmount()
|
|
526
|
+
if("unmount" in this.collectContainer.elements.cardNumberElement) this.collectContainer.elements.cardNumberElement.unmount()
|
|
527
|
+
if("unmount" in this.collectContainer.elements.expiryYearElement) this.collectContainer.elements.expiryYearElement.unmount()
|
|
528
|
+
if("unmount" in this.collectContainer.elements.expiryMonthElement) this.collectContainer.elements.expiryMonthElement.unmount()
|
|
529
|
+
if("unmount" in this.collectContainer.elements.cvvElement) this.collectContainer.elements.cvvElement.unmount()
|
|
530
|
+
}
|
|
531
|
+
}
|
|
349
532
|
}
|
package/src/data/api.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { buildErrorResponse, buildErrorResponseFromCatch } from "../helpers/utils";
|
|
2
|
+
|
|
1
3
|
export async function getOpenpayDeviceSessionID(merchant_id, public_key, signal) {
|
|
2
4
|
let openpay = await window.OpenPay;
|
|
3
5
|
openpay.setId(merchant_id);
|
|
@@ -107,3 +109,64 @@ export async function startCheckoutRouter(baseUrlTonder, apiKeyTonder, routerIte
|
|
|
107
109
|
throw error
|
|
108
110
|
}
|
|
109
111
|
}
|
|
112
|
+
|
|
113
|
+
export async function registerCard(baseUrlTonder, customerToken, data) {
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(`${baseUrlTonder}/api/v1/cards/`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'Authorization': `Token ${customerToken}`,
|
|
119
|
+
'Content-Type': 'application/json'
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify(data)
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (response.ok) return await response.json();
|
|
125
|
+
if (response.status === 409){
|
|
126
|
+
const res_json = await response.json()
|
|
127
|
+
if(res_json.error = 'Card number already exists.'){
|
|
128
|
+
return {
|
|
129
|
+
code: 200,
|
|
130
|
+
body: res_json,
|
|
131
|
+
name: '',
|
|
132
|
+
message: res_json.error,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw await buildErrorResponse(response);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw buildErrorResponseFromCatch(error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function deleteCustomerCard(baseUrlTonder, customerToken, skyflowId = "") {
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetch(`${baseUrlTonder}/api/v1/cards/${skyflowId}`, {
|
|
144
|
+
method: 'DELETE',
|
|
145
|
+
headers: {
|
|
146
|
+
'Authorization': `Token ${customerToken}`,
|
|
147
|
+
'Content-Type': 'application/json'
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (response.ok) return true;
|
|
152
|
+
throw await buildErrorResponse(response);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
throw buildErrorResponseFromCatch(error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function getCustomerCards(baseUrlTonder, customerToken, query = "") {
|
|
158
|
+
try {
|
|
159
|
+
const response = await fetch(`${baseUrlTonder}/api/v1/cards/${query}`, {
|
|
160
|
+
method: 'GET',
|
|
161
|
+
headers: {
|
|
162
|
+
'Authorization': `Token ${customerToken}`,
|
|
163
|
+
'Content-Type': 'application/json'
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (response.ok) return await response.json();
|
|
168
|
+
throw await buildErrorResponse(response);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw buildErrorResponseFromCatch(error);
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/helpers/skyflow.js
CHANGED
|
@@ -6,7 +6,8 @@ export async function initSkyflow(
|
|
|
6
6
|
baseUrl,
|
|
7
7
|
apiKey,
|
|
8
8
|
signal,
|
|
9
|
-
customStyles = {}
|
|
9
|
+
customStyles = {},
|
|
10
|
+
collectorIds,
|
|
10
11
|
) {
|
|
11
12
|
const skyflow = await Skyflow.init({
|
|
12
13
|
vaultID: vaultId,
|
|
@@ -121,7 +122,16 @@ export async function initSkyflow(
|
|
|
121
122
|
cardHolderNameElement,
|
|
122
123
|
)
|
|
123
124
|
|
|
124
|
-
return
|
|
125
|
+
return {
|
|
126
|
+
container: collectContainer,
|
|
127
|
+
elements: {
|
|
128
|
+
cardHolderNameElement,
|
|
129
|
+
cardNumberElement,
|
|
130
|
+
cvvElement,
|
|
131
|
+
expiryMonthElement,
|
|
132
|
+
expiryYearElement
|
|
133
|
+
}
|
|
134
|
+
}
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
async function mountElements(
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const cardTemplateSkeleton = `
|
|
2
|
+
<div class="container-tonder-skeleton">
|
|
3
|
+
<div class="skeleton-loader"></div>
|
|
4
|
+
<div class="skeleton-loader"></div>
|
|
5
|
+
<div class="collect-row-skeleton">
|
|
6
|
+
<div class="skeleton-loader skeleton-loader-item"></div>
|
|
7
|
+
<div class="skeleton-loader skeleton-loader-item"></div>
|
|
8
|
+
<div class="skeleton-loader skeleton-loader-item"></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
.container-tonder-skeleton {
|
|
14
|
+
background-color: #F9F9F9;
|
|
15
|
+
margin: 0 auto !important;
|
|
16
|
+
padding: 30px 10px 30px 10px;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
transition: max-height 0.5s ease-out;
|
|
19
|
+
max-width: 600px;
|
|
20
|
+
height: 100%;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: 45px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.collect-row-skeleton {
|
|
27
|
+
display: flex !important;
|
|
28
|
+
justify-content: space-between !important;
|
|
29
|
+
margin-left: 10px !important;
|
|
30
|
+
margin-right: 10px !important;
|
|
31
|
+
gap: 10px;
|
|
32
|
+
}
|
|
33
|
+
.skeleton-loader {
|
|
34
|
+
height: 45px !important;
|
|
35
|
+
border-radius: 8px;
|
|
36
|
+
margin-top: 2px;
|
|
37
|
+
margin-bottom: 4px;
|
|
38
|
+
margin-left: 10px !important;
|
|
39
|
+
margin-right: 10px !important;
|
|
40
|
+
background-color: #e0e0e0;
|
|
41
|
+
animation: pulse 1.5s infinite ease-in-out;
|
|
42
|
+
}
|
|
43
|
+
.skeleton-loader-item{
|
|
44
|
+
width: 35%;
|
|
45
|
+
margin: 0 !important;
|
|
46
|
+
}
|
|
47
|
+
@keyframes pulse {
|
|
48
|
+
0% {
|
|
49
|
+
background-color: #e0e0e0;
|
|
50
|
+
}
|
|
51
|
+
50% {
|
|
52
|
+
background-color: #f0f0f0;
|
|
53
|
+
}
|
|
54
|
+
100% {
|
|
55
|
+
background-color: #e0e0e0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
</style>
|
|
59
|
+
`
|