@kompasid/lit-web-components 0.8.26 → 0.8.28

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.
Files changed (19) hide show
  1. package/demo/index.html +16 -0
  2. package/dist/src/components/kompasid-widget-recirculations-default/KompasWidgetRecirculationsDefault.d.ts +16 -2
  3. package/dist/src/components/kompasid-widget-recirculations-default/KompasWidgetRecirculationsDefault.js +112 -31
  4. package/dist/src/components/kompasid-widget-recirculations-default/KompasWidgetRecirculationsDefault.js.map +1 -1
  5. package/dist/src/components/kompasid-widget-recirculations-default/types.d.ts +8 -0
  6. package/dist/src/components/kompasid-widget-recirculations-default/types.js.map +1 -1
  7. package/dist/src/components/kompasid-widget-recirculations-list/KompasWidgetRecirculationsList.d.ts +1 -0
  8. package/dist/src/components/kompasid-widget-recirculations-list/KompasWidgetRecirculationsList.js +8 -1
  9. package/dist/src/components/kompasid-widget-recirculations-list/KompasWidgetRecirculationsList.js.map +1 -1
  10. package/dist/src/utils/IntersectionObserver.d.ts +21 -0
  11. package/dist/src/utils/IntersectionObserver.js +109 -0
  12. package/dist/src/utils/IntersectionObserver.js.map +1 -0
  13. package/dist/tsconfig.tsbuildinfo +1 -1
  14. package/package.json +1 -1
  15. package/src/components/kompasid-widget-recirculations-default/KompasWidgetRecirculationsDefault.ts +89 -35
  16. package/src/components/kompasid-widget-recirculations-default/readme.md +32 -17
  17. package/src/components/kompasid-widget-recirculations-default/types.ts +9 -0
  18. package/src/components/kompasid-widget-recirculations-list/KompasWidgetRecirculationsList.ts +10 -1
  19. package/src/utils/IntersectionObserver.ts +97 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Kompas.id reusable web components",
4
4
  "license": "MIT",
5
5
  "author": "kompas.id",
6
- "version": "v0.8.26",
6
+ "version": "v0.8.28",
7
7
  "type": "module",
8
8
  "main": "dist/src/index.js",
9
9
  "module": "dist/src/index.js",
@@ -3,7 +3,9 @@ import { customElement, property } from 'lit/decorators.js'
3
3
  import { TWStyles } from '../../../tailwind/tailwind.js'
4
4
  import { Post } from './types.js'
5
5
  import { useTimeDifference } from '../../utils/useTimeDIfference.js'
6
+ import { createIntersectionObserverComponent } from '../../utils/IntersectionObserver.js'
6
7
 
8
+ createIntersectionObserverComponent()
7
9
  @customElement('kompasid-widget-recirculations-default')
8
10
  export class KompasWidgetRecirculationsDefault extends LitElement {
9
11
  static styles = [
@@ -32,7 +34,7 @@ export class KompasWidgetRecirculationsDefault extends LitElement {
32
34
  * Props
33
35
  */
34
36
  @property({ type: Array }) posts: Post[][] = []
35
- @property({ type: String }) accessToken = ''
37
+ @property({ type: String }) recoID = ''
36
38
  @property({ type: String }) permalinkArticle = ''
37
39
  @property({ type: String }) userGuid = '0'
38
40
  @property({ type: String }) slugs = ''
@@ -44,6 +46,18 @@ export class KompasWidgetRecirculationsDefault extends LitElement {
44
46
 
45
47
  @property({ type: Boolean }) isDark = false
46
48
 
49
+ // data dari artikel yg sedang dibuka, untuk datalayer reco_content_viewed
50
+ @property({ type: String }) tracker_content_id = '' //
51
+ @property({ type: String }) tracker_content_categories = ''
52
+ // datau user
53
+ @property({ type: String }) tracker_user_id = ''
54
+ @property({ type: String }) tracker_user_type = ''
55
+ @property({ type: String }) tracker_subscription_status = ''
56
+ @property({ type: String }) tracker_subscription_package = ''
57
+ @property({ type: String }) tracker_metered_wall_type = ''
58
+ @property({ type: Number }) tracker_metered_wall_balance = 0
59
+ @property({ type: String }) tracker_page_domain = ''
60
+
47
61
  /**
48
62
  * Fetch Data
49
63
  */
@@ -51,7 +65,6 @@ export class KompasWidgetRecirculationsDefault extends LitElement {
51
65
  super.connectedCallback()
52
66
  try {
53
67
  if (this.type === 'relatedArticle') {
54
- await this.fetchAccessToken()
55
68
  await this.relatedArticles()
56
69
  } else if (this.type === 'otherArticle') {
57
70
  await this.otherArticles()
@@ -61,34 +74,7 @@ export class KompasWidgetRecirculationsDefault extends LitElement {
61
74
  }
62
75
  }
63
76
 
64
- async fetchAccessToken() {
65
- const response = await fetch(
66
- 'https://api.kompas.id/account/api/v1/login/guest',
67
- {
68
- method: 'POST',
69
- headers: {
70
- 'Content-Type': 'application/json',
71
- },
72
- body: JSON.stringify({
73
- email: 'anonynous.user@kompas.id',
74
- }),
75
- }
76
- )
77
-
78
- const result = await response.json()
79
-
80
- if (result?.data?.accessToken) {
81
- this.accessToken = result.data.accessToken
82
- } else {
83
- throw new Error('Token akses tidak tersedia dalam respons')
84
- }
85
- }
86
-
87
77
  async relatedArticles() {
88
- if (!this.accessToken) {
89
- throw new Error('Token akses tidak tersedia')
90
- }
91
-
92
78
  const kompasApiAi = 'https://ai.kompas.id/api/v1'
93
79
 
94
80
  // Constructing parameters
@@ -104,11 +90,14 @@ export class KompasWidgetRecirculationsDefault extends LitElement {
104
90
 
105
91
  const response = await fetch(endpoint, {
106
92
  headers: {
107
- Authorization: `Bearer ${this.accessToken}`,
108
93
  'Content-Type': 'application/json',
109
94
  },
110
95
  })
111
96
 
97
+ // get x-request-id from headers
98
+ const recoId = response.headers.get('X-Request-Id') || ''
99
+ this.recoID = recoId
100
+
112
101
  const result = await response.json()
113
102
 
114
103
  if (result?.result) {
@@ -256,6 +245,55 @@ export class KompasWidgetRecirculationsDefault extends LitElement {
256
245
  `
257
246
  }
258
247
 
248
+ /**
249
+ * mengirim event ke datalayer
250
+ */
251
+ private recoContentClicked(post: Post, recoIndex: number) {
252
+ if (this.type !== 'relatedArticle') return // Hanya trigger event jika type 'relatedArticle'
253
+
254
+ window.dataLayer = window.dataLayer || []
255
+ window.dataLayer.push({
256
+ // data dari component
257
+ event: 'reco_content_clicked',
258
+ content_id: post.name,
259
+ content_categories: post.category?.map(cat => cat.name).join('|') || '',
260
+ reco_index: recoIndex,
261
+ reco_id: this.recoID || '',
262
+ reco_model: 'context',
263
+ // data dari implementor
264
+ user_id: this.tracker_user_id || 'NA',
265
+ user_type: this.tracker_user_type || '',
266
+ subscription_status: this.tracker_subscription_status || '',
267
+ subscription_package: this.tracker_subscription_package || '',
268
+ metered_wall_type: this.tracker_metered_wall_type || '',
269
+ metered_wall_balance: this.tracker_metered_wall_balance,
270
+ page_domain: this.tracker_page_domain || 'Kompas.id',
271
+ })
272
+ }
273
+
274
+ private recoContentViewed() {
275
+ window.dataLayer = window.dataLayer || []
276
+ window.dataLayer.push({
277
+ event: 'reco_content_viewed',
278
+ content_id: this.tracker_content_id || '',
279
+ content_categories: this.tracker_content_categories || '',
280
+ user_type: this.tracker_user_type || '',
281
+ subscription_status: this.tracker_subscription_status || '',
282
+ subscription_package: this.tracker_subscription_package || '',
283
+ metered_wall_type: this.tracker_metered_wall_type || '',
284
+ metered_wall_balance: this.tracker_metered_wall_balance,
285
+ page_domain: this.tracker_page_domain || 'Kompas.id',
286
+ })
287
+ }
288
+
289
+ handleObserver() {
290
+ if (this.type === 'relatedArticle') {
291
+ this.recoContentViewed()
292
+ } else {
293
+ // kalo mau implemen buat artikel lainnya
294
+ }
295
+ }
296
+
259
297
  render() {
260
298
  return html`
261
299
  <div class="w-full ${this.isDark ? 'bg-dark-5 ' : ''}">
@@ -264,18 +302,27 @@ export class KompasWidgetRecirculationsDefault extends LitElement {
264
302
  ? this.titleRelatedArticle()
265
303
  : this.titleOtherArticle()}
266
304
  <!-- end: widget title -->
267
-
305
+ <intersection-observer-component
306
+ .onTrigger="${() => this.handleObserver()}"
307
+ ></intersection-observer-component>
268
308
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8">
269
309
  <!-- start: left-post-loop -->
270
310
  <div class="grid grid-cols-1 gap-6">
271
311
  ${this.posts[0]
272
312
  ? this.posts[0].map(
273
- post => html`
313
+ (post, index) => html`
274
314
  <div class="w-full">
275
315
  <div class="flex">
276
316
  <div class="w-1/2">${this.renderImage(post)}</div>
277
317
  <div class="flex-grow pl-4 w-1/2">
278
- <a href="${post.permalink}">
318
+ <a
319
+ href="${post.permalink}?open_from=${this.type ===
320
+ 'relatedArticle'
321
+ ? 'Artikel_Terkait'
322
+ : 'Artikel_Lainnya'}"
323
+ @click="${() =>
324
+ this.recoContentClicked(post, index + 1)}"
325
+ >
279
326
  ${this.renderChips(post)}
280
327
  <h5
281
328
  class="hover:underline font-bold font-sans leading-tight text-lg md:text-xl ${this
@@ -306,12 +353,19 @@ export class KompasWidgetRecirculationsDefault extends LitElement {
306
353
  <div class="flex flex-col w-full">
307
354
  ${this.posts[1]
308
355
  ? this.posts[1].map(
309
- post => html`
356
+ (post, index) => html`
310
357
  <div class="mb-6">
311
358
  <div class="flex">
312
359
  <div class="w-1/2">${this.renderImage(post)}</div>
313
360
  <div class="flex-grow pl-4 w-1/2">
314
- <a href="${post.permalink}">
361
+ <a
362
+ href="${post.permalink}?open_from=${this.type ===
363
+ 'relatedArticle'
364
+ ? 'Artikel_Terkait'
365
+ : 'Artikel_Lainnya'}"
366
+ @click="${() =>
367
+ this.recoContentClicked(post, index + 6)}"
368
+ >
315
369
  ${this.renderChips(post)}
316
370
  <h5
317
371
  class="hover:underline font-bold font-sans leading-tight text-lg md:text-xl ${this
@@ -15,33 +15,48 @@ const widgetOtherPost = {
15
15
  titleLink: '/kategori/opini'
16
16
  }
17
17
 
18
- <div>
19
18
  <kompasid-widget-recirculations-default
20
- .permalinkArticle=${widgetRelatedPost.permalinkArticle}
21
- .slugs=${widgetRelatedPost.slugs}
19
+ permalinkArticle=${widgetRelatedPost.permalinkArticle}
20
+ slugs=${widgetRelatedPost.slugs}
21
+ tracker_content_id="slug artikel yg dibuka"
22
+ tracker_content_categories="kategori artikel yg dibuka"
23
+ tracker_user_id="user guid"
24
+ tracker_user_type="S" --> // G/R/S
25
+ tracker_subscription_status="AC" --> // IA/AC/EX/GP
26
+ tracker_subscription_package="paket-langganan" --> // contoh: `cash-app-kdd1-reguler`
27
+ tracker_metered_wall_type="" --> // "" atau MP/HP/MRW
28
+ tracker_metered_wall_balance=0 --> // sisa kuota metered 5/4/3/2/1
29
+ tracker_page_domain="" --> // default "Kompas.id"
22
30
  ></kompasid-widget-recirculations-default>
23
31
 
32
+
24
33
  <kompasid-widget-recirculations-default
25
- .titleName=${widgetOtherPost.titleName}
26
- .titleLink=${widgetOtherPost.titleLink}
34
+ titleName=${widgetOtherPost.titleName}
35
+ titleLink=${widgetOtherPost.titleLink}
27
36
  type='otherArticle'
28
37
  mainCategory='opini'
29
38
  ></kompasid-widget-recirculations-default>
30
- </div>
31
39
  ```
32
40
 
33
41
  ## Properti
34
-
35
- | Property | Attribute | Deskripsi | Tipe | Default | Konten |
36
- | ------------------ | --------------- | -------------------------------------------------------------------------------------------------- | -------- | --------------- | ------------------------------------------- |
37
- | `titleName` | `titleName` | Nama judul yang akan ditampilkan di widget. (Digunakan jika memakai props type=otherArticle) | `String` | `''` |
38
- | `titleLink` | `titleLink` | Tautan yang akan ditetapkan pada judul di widget. (Digunakan jika memakai props type=otherArticle) | `String` | `''` |
39
- | `userGuid` | `userGuid` | GUID pengguna yang sedang menggunakan aplikasi untuk fetch data artikel terkait. | `String` | `'0'` | |
40
- | `slugs` | `slugs` | Daftar slug kategori atau tag yang terkait dengan artikel untuk artikel terkait. | `String` | `''` | |
41
- | `permalinkArticle` | `permalinkArticle` | Tautan kategori artikel yang sedang ditampilkan atau dibaca untuk rekomendasi artikel terkait. | `String` | `''` | |
42
- | `type` | `type` | Tipe widget untuk membedakan antara pengambilan data artikel terkait (`relatedArticle`) atau lainnya dalam kategori (`otherArticle`). | `String` | `'relatedArticle'` | `"relatedArticle" \| "otherArticle"` |
43
- | `mainCategory` | `mainCategory` | Kategori utama artikel yang akan ditampilkan jika menggunakan tipe `otherArticle` untuk lainnya dalam kategori. | `String` | `''` | |
44
-
42
+ | Property | Attribute | Deskripsi | Tipe | Default | Konten |
43
+ | ----------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------- | -------- | ------------------- | ------------------------------------------- |
44
+ | `titleName` | `titleName` | Nama judul yang akan ditampilkan di widget. (Digunakan jika memakai props `type=otherArticle`) | `String` | `''` | |
45
+ | `titleLink` | `titleLink` | Tautan yang akan ditetapkan pada judul di widget. (Digunakan jika memakai props `type=otherArticle`) | `String` | `''` | |
46
+ | `userGuid` | `userGuid` | GUID pengguna yang sedang menggunakan aplikasi untuk fetch data artikel terkait. | `String` | `'0'` | |
47
+ | `slugs` | `slugs` | Daftar slug kategori atau tag yang terkait dengan artikel untuk artikel terkait. | `String` | `''` | |
48
+ | `permalinkArticle` | `permalinkArticle` | Tautan kategori artikel yang sedang ditampilkan atau dibaca untuk rekomendasi artikel terkait. | `String` | `''` | |
49
+ | `type` | `type` | Tipe widget untuk membedakan antara `relatedArticle` atau `otherArticle`. | `String` | `'relatedArticle'` | `"relatedArticle" \| "otherArticle"` |
50
+ | `mainCategory` | `mainCategory` | Kategori utama artikel yang akan ditampilkan jika menggunakan tipe `otherArticle`. | `String` | `''` | |
51
+ | `tracker_content_id` | `tracker_content_id` | ID dari artikel (slug) yang sedang dibuka. | `String` | `''` | |
52
+ | `tracker_content_categories` | `tracker_content_categories`| Kategori artikel yang sedang dibuka. | `String` | `''` | |
53
+ | `tracker_user_id` | `tracker_user_id` | ID pengguna yang sedang mengakses konten. | `String` | `'NA'` | |
54
+ | `tracker_user_type` | `tracker_user_type` | Tipe pengguna (`G/R/S`). | `String` | `''` | |
55
+ | `tracker_subscription_status` | `tracker_subscription_status` | Status langganan pengguna (`IA/AC/EX/GP`). | `String` | `''` | |
56
+ | `tracker_subscription_package`| `tracker_subscription_package` | Paket langganan pengguna (contoh: `cash-app-kdd1-reguler`). | `String` | `''` | |
57
+ | `tracker_metered_wall_type` | `tracker_metered_wall_type` | Tipe metered wall pada artikel (`MRW/MP/HP`). | `String` | `''` | |
58
+ | `tracker_metered_wall_balance`| `tracker_metered_wall_balance` | Sisa kuota metered wall (`5/4/3/2/1`). | `Number` | `0` | |
59
+ | `tracker_page_domain` | `tracker_page_domain` | Domain halaman yang sedang diakses. | `String` | `'Kompas.id'` | |
45
60
 
46
61
  ### Digunakan oleh
47
62
 
@@ -1,9 +1,18 @@
1
+ interface Category {
2
+ id: number
3
+ name: string
4
+ slug: string
5
+ }
6
+
1
7
  interface PostTag {
2
8
  slug: string
3
9
  }
4
10
 
5
11
  export interface Post {
12
+ id: number
6
13
  title: string
14
+ name: string
15
+ category: Category[]
7
16
  isAnalisis?: boolean
8
17
  isEksklusif?: boolean
9
18
  isFreemium?: boolean
@@ -164,6 +164,15 @@ export class KompasWidgetRecirculationsList extends LitElement {
164
164
  return chips
165
165
  }
166
166
 
167
+ getSectionName() {
168
+ const sectionMapping = {
169
+ Terbaru: 'Section_Terbaru',
170
+ Terpopuler: 'Section_Terpopuler',
171
+ } as const
172
+
173
+ return sectionMapping[this.widgetTitle as keyof typeof sectionMapping] || ''
174
+ }
175
+
167
176
  render() {
168
177
  return html`
169
178
  <div class="${this.isDark ? 'bg-dark-5 ' : ''}">
@@ -187,7 +196,7 @@ export class KompasWidgetRecirculationsList extends LitElement {
187
196
  <div class="flex flex-col w-full">
188
197
  ${this.renderChips(item)}
189
198
  <a
190
- href="${item.permalink}"
199
+ href="${item.permalink}?open_from=${this.getSectionName()}"
191
200
  class="font-bold font-lora hover:underline leading-normal text-xl ${this
192
201
  .isDark
193
202
  ? 'text-white'
@@ -0,0 +1,97 @@
1
+ import { html, LitElement } from 'lit'
2
+ import { property, query } from 'lit/decorators.js'
3
+
4
+ export class IntersectionObserverComponent extends LitElement {
5
+ @property({ type: String }) rootMargin: string = '0px 0px 0px 0px'
6
+ @property({ type: Number }) threshold: number = 1
7
+ @property({ type: Boolean }) multipleTrigger: boolean = false
8
+ @property({ type: Boolean }) forceTrigger: boolean = false
9
+ @property({ type: Number }) timer: number = 0
10
+ @property({ attribute: false }) onTrigger?: () => void
11
+
12
+ @query('#elementTarget') elementTarget!: HTMLElement
13
+
14
+ private intersectionObserver: IntersectionObserver | null = null
15
+ private timerId: number | null = null
16
+
17
+ connectedCallback(): void {
18
+ super.connectedCallback()
19
+ this.intersectionObserver = new IntersectionObserver(
20
+ this.handleIntersection.bind(this),
21
+ {
22
+ threshold: this.threshold,
23
+ rootMargin: this.rootMargin,
24
+ }
25
+ )
26
+ }
27
+
28
+ firstUpdated(): void {
29
+ if (this.elementTarget) {
30
+ this.intersectionObserver?.observe(this.elementTarget)
31
+ }
32
+ if (this.forceTrigger) {
33
+ this.emitTrigger()
34
+ }
35
+ }
36
+
37
+ disconnectedCallback(): void {
38
+ super.disconnectedCallback()
39
+ this.intersectionObserver?.disconnect()
40
+ this.clearTimer()
41
+ }
42
+
43
+ private handleIntersection(entries: IntersectionObserverEntry[]): void {
44
+ entries.forEach(entry => {
45
+ if (entry.isIntersecting) {
46
+ this.handleEntryInView()
47
+ } else {
48
+ this.clearTimer()
49
+ }
50
+ })
51
+ }
52
+
53
+ private handleEntryInView(): void {
54
+ if (this.timer > 0) {
55
+ this.clearTimer()
56
+ this.timerId = window.setTimeout(() => {
57
+ this.emitTrigger()
58
+ if (!this.multipleTrigger) {
59
+ this.intersectionObserver?.disconnect()
60
+ }
61
+ }, this.timer)
62
+ } else {
63
+ this.emitTrigger()
64
+ if (!this.multipleTrigger) {
65
+ this.intersectionObserver?.disconnect()
66
+ }
67
+ }
68
+ }
69
+
70
+ private clearTimer(): void {
71
+ if (this.timerId !== null) {
72
+ clearTimeout(this.timerId)
73
+ this.timerId = null
74
+ }
75
+ }
76
+
77
+ private emitTrigger(): void {
78
+ this.dispatchEvent(new CustomEvent('trigger'))
79
+ if (this.onTrigger) {
80
+ this.onTrigger()
81
+ }
82
+ }
83
+
84
+ render() {
85
+ return html`<div id="elementTarget"><slot></slot></div>`
86
+ }
87
+ }
88
+
89
+ export const createIntersectionObserverComponent = () => {
90
+ if (!customElements.get('intersection-observer-component')) {
91
+ customElements.define(
92
+ 'intersection-observer-component',
93
+ IntersectionObserverComponent
94
+ )
95
+ }
96
+ return IntersectionObserverComponent
97
+ }