@kompasid/lit-web-components 0.8.29 → 0.9.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.
@@ -0,0 +1,550 @@
1
+ /* eslint-disable lit-a11y/click-events-have-key-events */
2
+ import { html, css, LitElement } from 'lit'
3
+ import { customElement, property, state } from 'lit/decorators.js'
4
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js'
5
+ import { TWStyles } from '../../../tailwind/tailwind.js'
6
+ import { getFontAwesomeIcon } from '../../utils/fontawesome-setup.js'
7
+ import { decodeSpecialChars } from '../../utils/decodeSpecialChars.js'
8
+ import { timedContent } from '../../utils/timedContent.js'
9
+
10
+ interface DataExternalLink {
11
+ external?: boolean
12
+ gtmClass?: string
13
+ icon: object | null
14
+ iconify: string | null
15
+ isNew: boolean
16
+ name: string
17
+ url: string
18
+ }
19
+
20
+ interface dataType {
21
+ href: string
22
+ external?: boolean
23
+ icon: object | null
24
+ iconify: string | null
25
+ name: string
26
+ slug: string
27
+ redDot: [
28
+ {
29
+ start: string
30
+ end: string
31
+ }
32
+ ]
33
+ children: [
34
+ {
35
+ href: string
36
+ external: boolean
37
+ icon: string
38
+ iconify: string | null
39
+ name: string
40
+ slug: string
41
+ redDot: [
42
+ {
43
+ start: string
44
+ end: string
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
50
+ interface DataSideBarLink {
51
+ feature: dataType[]
52
+ category: dataType[]
53
+ }
54
+
55
+ @customElement('kompasid-menu-side-bar')
56
+ export class KompasMenuSideBar extends LitElement {
57
+ static styles = [
58
+ css`
59
+ .slide-side-enter-active,
60
+ .slide-side-leave-active {
61
+ transition: all 0.3s ease-out;
62
+ }
63
+ .slide-side-enter,
64
+ .slide-side-leave-to {
65
+ transform: translateX(-100%);
66
+ }
67
+ /* end: transisi buat sidebar */
68
+ .nuxt-link-exact-active {
69
+ @apply text-brand-1;
70
+ }
71
+ .menu-menu-sidebar {
72
+ z-index: 99999;
73
+ }
74
+ /* Force scrollbar to always show (for debugging) */
75
+ .menu-menu-sidebar::-webkit-scrollbar {
76
+ width: 4px;
77
+ }
78
+
79
+ .menu-menu-sidebar::-webkit-scrollbar-track {
80
+ background: white;
81
+ }
82
+
83
+ .menu-menu-sidebar::-webkit-scrollbar-thumb {
84
+ background-color: #00557d; /* Replace with your brand color */
85
+ border-radius: 8px;
86
+ }
87
+
88
+ .menu-menu-sidebar::-webkit-scrollbar-button,
89
+ .menu-menu-sidebar::-webkit-scrollbar-corner {
90
+ background-color: white;
91
+ }
92
+ `,
93
+ TWStyles,
94
+ ]
95
+ @property({ type: Array }) dataExternal: DataExternalLink[] = []
96
+ // @property({ type: Array }) dataSidebar: DataSideBarLink[] = []
97
+
98
+ async connectedCallback() {
99
+ super.connectedCallback()
100
+ try {
101
+ await this.fetchExternal()
102
+ } catch (error) {
103
+ this.handleFetchError(error)
104
+ }
105
+ }
106
+ handleFetchError(error: unknown) {
107
+ const errorMessage =
108
+ error instanceof Error ? error.message : 'Kesalahan tidak diketahui'
109
+ alert(`Terjadi kesalahan: ${errorMessage}`)
110
+ }
111
+
112
+ dataSidebar: DataSideBarLink = {
113
+ feature: [],
114
+ category: [],
115
+ }
116
+ async fetchExternal() {
117
+ // External
118
+ const endpointExternal = `https://cdn-www.kompas.id/assets/json/ApiMenuExternalLink.json`
119
+ const response = await fetch(endpointExternal, {
120
+ headers: {
121
+ 'Content-Type': 'application/json',
122
+ },
123
+ })
124
+ const resultExternal = await response.json()
125
+ // eslint-disable-next-line no-undef
126
+ if (!resultExternal || !Array.isArray(resultExternal)) {
127
+ console.error(
128
+ 'Error: resultExternal.result is undefined or not an array',
129
+ resultExternal
130
+ )
131
+ this.dataExternal = [] // Ensure dataExternal is an empty array instead of undefined
132
+ } else {
133
+ this.dataExternal = resultExternal.map(
134
+ (externalLink: Partial<DataExternalLink>) => ({
135
+ external: externalLink.external ?? false,
136
+ gtmClass: externalLink.gtmClass ?? '',
137
+ icon: externalLink.icon ?? null,
138
+ iconify: externalLink.iconify ?? null,
139
+ isNew: externalLink.isNew ?? false,
140
+ name: externalLink.name ?? '',
141
+ url: externalLink.url ?? '',
142
+ })
143
+ )
144
+ }
145
+ // Sidebar
146
+ const endpointSidebar = `https://cdn-www.kompas.id/assets/json/ApiMenuSideV2.json`
147
+ const responseSidebar = await fetch(endpointSidebar, {
148
+ headers: {
149
+ 'Content-Type': 'application/json',
150
+ },
151
+ })
152
+ const resultSidebar = await responseSidebar.json()
153
+ // Validate the structure of the response
154
+ if (!resultSidebar || typeof resultSidebar !== 'object') {
155
+ console.error('Invalid response format:', resultSidebar)
156
+ return
157
+ }
158
+
159
+ // Convert object to an array
160
+ const sidebarArray = Object.values(resultSidebar)
161
+ const [featureArray, categoryArray] = sidebarArray as [any[], any[]]
162
+ const features: dataType[] =
163
+ featureArray?.map((item: any) => ({
164
+ href: item?.href ?? '',
165
+ external: item?.external ?? false,
166
+ icon: item?.icon ?? null,
167
+ iconify: item?.iconify ?? null,
168
+ name: item?.name ?? '',
169
+ slug: item?.slug ?? '',
170
+ redDot: [
171
+ {
172
+ start: item?.redDot?.start ?? '',
173
+ end: item?.redDot?.end ?? '',
174
+ },
175
+ ],
176
+ children: Array.isArray(item?.children)
177
+ ? item.children.map((child: any) => ({
178
+ href: child?.href ?? '',
179
+ external: child?.external ?? false,
180
+ icon: child?.icon ?? '',
181
+ iconify: child?.iconify ?? '',
182
+ name: child?.name ?? '',
183
+ slug: child?.slug ?? '',
184
+ redDot: [
185
+ {
186
+ start: child?.redDot?.start ?? '',
187
+ end: child?.redDot?.end ?? '',
188
+ },
189
+ ],
190
+ }))
191
+ : [],
192
+ })) ?? []
193
+
194
+ // Map category data
195
+ const categories: dataType[] =
196
+ categoryArray?.map((item: any) => ({
197
+ href: item?.href ?? '',
198
+ external: item?.external ?? false,
199
+ icon: item?.icon ?? null,
200
+ iconify: item?.iconify ?? null,
201
+ name: item?.name ?? '',
202
+ slug: item?.slug ?? '',
203
+ redDot: [
204
+ {
205
+ start: item?.redDot?.start ?? '',
206
+ end: item?.redDot?.end ?? '',
207
+ },
208
+ ],
209
+ children: Array.isArray(item?.children)
210
+ ? item.children.map((child: any) => ({
211
+ href: child?.href ?? '',
212
+ external: child?.external ?? false,
213
+ icon: child?.icon ?? '',
214
+ iconify: child?.iconify ?? '',
215
+ name: child?.name ?? '',
216
+ slug: child?.slug ?? '',
217
+ redDot: [
218
+ {
219
+ start: child?.redDot?.start ?? '',
220
+ end: child?.redDot?.end ?? '',
221
+ },
222
+ ],
223
+ }))
224
+ : [],
225
+ })) ?? []
226
+ this.dataSidebar = { feature: features, category: categories }
227
+ this.requestUpdate()
228
+ }
229
+
230
+ renderChips() {
231
+ const chips = []
232
+ chips.push(
233
+ html`
234
+ <div class="flex">
235
+ <div
236
+ class="py-1 px-2 rounded text-xs"
237
+ style="position: absolute; top: -22px; right: -24px; display: inline-flex; background-color:#D71920;"
238
+ >
239
+ <span class="font-normal text-white capitalize">Baru</span>
240
+ </div>
241
+ </div>
242
+ `
243
+ )
244
+ return chips
245
+ }
246
+
247
+ hasChildren(item: any): boolean {
248
+ return Array.isArray(item?.children) && item.children.length > 0
249
+ }
250
+
251
+ rubricClicked(item: { name: string; href?: string }, event?: Event): void {
252
+ if (event) {
253
+ event.stopPropagation() // Prevent parent click event
254
+ }
255
+ if (item.href) {
256
+ window.location.href = item.href
257
+ }
258
+
259
+ // add data layer here
260
+ }
261
+
262
+ @state()
263
+ private expandedSlug: string | null = null
264
+ private toggleChildren(item: any) {
265
+ this.expandedSlug = this.expandedSlug === item.slug ? null : item.slug
266
+ }
267
+
268
+ @state()
269
+ private showNavBar: boolean = false
270
+
271
+ toggleNavSidebar = (e: Event) => {
272
+ e.stopPropagation() // prevent bubbling
273
+ this.showNavBar = !this.showNavBar
274
+ }
275
+
276
+ render() {
277
+ return html`
278
+ <!-- Button Menu -->
279
+ <div
280
+ class="w-fit flex items-center justify-center cursor-pointer relative"
281
+ @click=${this.toggleNavSidebar}
282
+ >
283
+ <div class="h-4 inline-flex text-brand-1">
284
+ ${unsafeSVG(getFontAwesomeIcon('fas', 'bars'))}
285
+ </div>
286
+ <span
287
+ class="font-sans hidden sm:inline ml-2 tracking-wide text-brand-1 font-bold"
288
+ >Menu</span
289
+ >
290
+ </div>
291
+ <!-- Side Menu -->
292
+ <nav
293
+ @click=${this.toggleNavSidebar}
294
+ class=${this.showNavBar
295
+ ? 'fixed left-0 top-0 w-screen z-[100]'
296
+ : 'hidden'}
297
+ >
298
+ <div
299
+ ref="toggle-nav-sidebar"
300
+ class="bg-white h-screen menu-menu-sidebar overflow-y-auto pb-20 pt-0 shadow-lg"
301
+ style="width: 312px;"
302
+ >
303
+ <div
304
+ class="bg-[#e1f0ff] flex flex-col items-center justify-center mb-6 w-full"
305
+ >
306
+ <div
307
+ ref="logo-kompas"
308
+ class="flex items-center justify-between px-4 py-6 w-full"
309
+ >
310
+ <a href="/" class="flex h-9 w-9">
311
+ <img
312
+ src="https://cdn-www.kompas.id/assets/img/icons/kompas-icon.svg"
313
+ alt="Kompas.id"
314
+ scale="0"
315
+ class="block w-full"
316
+ />
317
+ </a>
318
+ <span
319
+ class="bg-[#93c8fd] cursor-pointer flex h-10 items-center justify-center rounded text-base w-10 p-4"
320
+ @click=${this.toggleNavSidebar}
321
+ >
322
+ ${unsafeSVG(getFontAwesomeIcon('fa', 'times'))}
323
+ </span>
324
+ </div>
325
+ <div class="flex flex-wrap px-4 w-full">
326
+ ${this.dataExternal.map(
327
+ item => html`
328
+ <a href="${item.url}" class="block w-1/2 no-underline">
329
+ <div class="cursor-pointer flex items-center pb-6 w-full">
330
+ <div class="flex mr-2">
331
+ ${item.icon &&
332
+ Array.isArray(item.icon) &&
333
+ item.icon.length >= 2
334
+ ? unsafeSVG(
335
+ getFontAwesomeIcon(item.icon[0], item.icon[1])
336
+ )
337
+ : ''}
338
+ </div>
339
+ <span class="font-sans relative text-sm text-brand-1">
340
+ ${item.name} ${item.isNew ? this.renderChips() : ''}
341
+ </span>
342
+ </div>
343
+ </a>
344
+ `
345
+ )}
346
+ </div>
347
+ </div>
348
+ <!-- feature -->
349
+ <div class="flex">
350
+ <div class="flex justify-between flex-col">
351
+ ${this.dataSidebar.feature.map(
352
+ item => html`
353
+ <div class="w-full font-sans">
354
+ <!-- Parent item -->
355
+ <div
356
+ class="flex items-center justify-between px-4 text-sm font-medium text-gray-700 transition-all cursor-pointer"
357
+ @click=${(e: Event) => this.rubricClicked(item, e)}
358
+ >
359
+ <div
360
+ class="w-[216px] hover:bg-[#f3f4f6] rounded h-12 flex items-center"
361
+ >
362
+ <div class="flex items-center space-x-3">
363
+ <span
364
+ class="text-xl text-brand-1 w-8 h-8 flex items-center"
365
+ >
366
+ ${item.icon &&
367
+ Array.isArray(item.icon) &&
368
+ item.icon.length >= 2
369
+ ? unsafeSVG(
370
+ getFontAwesomeIcon(item.icon[0], item.icon[1])
371
+ )
372
+ : ''}
373
+ </span>
374
+ <span class="font-bold relative text-[#333] w-full"
375
+ >${decodeSpecialChars(item.name)}</span
376
+ >
377
+ ${timedContent(
378
+ item.redDot[0].start,
379
+ item.redDot[0].end
380
+ )
381
+ ? html`<span
382
+ class="bg-orange-600 h-2 relative rounded-full w-2 -top-[12px]"
383
+ ></span>`
384
+ : ''}
385
+ </div>
386
+ </div>
387
+
388
+ <!-- Toggle chevron -->
389
+ ${this.hasChildren(item)
390
+ ? html`
391
+ <span
392
+ class="text-xs bg-[#e1f0ff] flex justify-center items-center rounded my-1 p-4 w-10 h-10 cursor-pointer"
393
+ @click=${(e: Event) => {
394
+ e.stopPropagation() // Prevents click from bubbling to parent
395
+ this.toggleChildren(item)
396
+ }}
397
+ >
398
+ ${this.expandedSlug === item.slug
399
+ ? unsafeSVG(
400
+ getFontAwesomeIcon('fas', 'chevron-up')
401
+ )
402
+ : unsafeSVG(
403
+ getFontAwesomeIcon('fas', 'chevron-down')
404
+ )}
405
+ </span>
406
+ `
407
+ : null}
408
+ </div>
409
+
410
+ <!-- Children items -->
411
+ ${this.hasChildren(item) && this.expandedSlug === item.slug
412
+ ? html`
413
+ <div class="pl-14 pt-1 pb-2 space-y-1">
414
+ ${item.children.map(
415
+ child => html`
416
+ <div
417
+ class="flex items-center text-sm text-gray-600 px-4 cursor-pointer transition-all"
418
+ @click=${() => this.rubricClicked(child)}
419
+ >
420
+ <div
421
+ class="w-[216px] hover:bg-[#f3f4f6] rounded h-12 flex items-center pl-11"
422
+ >
423
+ ${decodeSpecialChars(child.name)}
424
+ ${timedContent(
425
+ child.redDot[0].start,
426
+ child.redDot[0].end
427
+ )
428
+ ? html`<span
429
+ class="bg-orange-600 h-2 relative rounded-full w-2 -top-[12px]"
430
+ ></span>`
431
+ : ''}
432
+ <div></div>
433
+ </div>
434
+ </div>
435
+ `
436
+ )}
437
+ </div>
438
+ `
439
+ : ''}
440
+ </div>
441
+ `
442
+ )}
443
+ </div>
444
+ </div>
445
+ <div class="border-b border-[#DDD] m-6 "></div>
446
+ <!-- category -->
447
+ <div class="flex">
448
+ <div class="w-full flex justify-between flex-col">
449
+ ${this.dataSidebar.category.map(
450
+ item => html`
451
+ <div class="w-full font-sans">
452
+ <!-- Parent item -->
453
+ <div
454
+ class="flex items-center justify-between text-sm font-medium text-gray-700 px-4 transition-all cursor-pointer"
455
+ @click=${(e: Event) => this.rubricClicked(item, e)}
456
+ >
457
+ <div
458
+ class="w-[216px] hover:bg-[#f3f4f6] rounded h-12 flex items-center"
459
+ >
460
+ <div class="flex items-center space-x-3">
461
+ <span
462
+ class="text-xl text-brand-1 w-8 h-8 flex items-center"
463
+ >
464
+ ${item.icon &&
465
+ Array.isArray(item.icon) &&
466
+ item.icon.length >= 2
467
+ ? unsafeSVG(
468
+ getFontAwesomeIcon(item.icon[0], item.icon[1])
469
+ )
470
+ : ''}
471
+ </span>
472
+ <span
473
+ class="font-bold ${item.name === 'Beranda'
474
+ ? 'text-[#00559a]'
475
+ : 'text-[#333] w-full'}"
476
+ >${decodeSpecialChars(item.name)}</span
477
+ >
478
+
479
+ ${timedContent(
480
+ item.redDot[0].start,
481
+ item.redDot[0].end
482
+ )
483
+ ? html`<span
484
+ class="bg-orange-600 h-2 relative rounded-full w-2 -top-[12px]"
485
+ ></span>`
486
+ : ''}
487
+ </div>
488
+ </div>
489
+
490
+ <!-- Toggle chevron -->
491
+ ${this.hasChildren(item)
492
+ ? html`
493
+ <span
494
+ class="text-xs bg-[#e1f0ff] flex justify-center items-center rounded my-1 p-4 w-10 h-10 cursor-pointer"
495
+ @click=${(e: Event) => {
496
+ e.stopPropagation() // Prevents click from bubbling to parent
497
+ this.toggleChildren(item)
498
+ }}
499
+ >
500
+ ${this.expandedSlug === item.slug
501
+ ? unsafeSVG(
502
+ getFontAwesomeIcon('fas', 'chevron-up')
503
+ )
504
+ : unsafeSVG(
505
+ getFontAwesomeIcon('fas', 'chevron-down')
506
+ )}
507
+ </span>
508
+ `
509
+ : null}
510
+ </div>
511
+
512
+ <!-- Children items -->
513
+ ${this.hasChildren(item) && this.expandedSlug === item.slug
514
+ ? html`
515
+ <div class="pt-1 pb-2 space-y-1">
516
+ ${item.children.map(
517
+ child => html`
518
+ <div
519
+ class="flex items-center text-sm text-gray-600 px-4 cursor-pointer transition-all"
520
+ @click=${() => this.rubricClicked(child)}
521
+ >
522
+ <div
523
+ class="w-[216px] hover:bg-[#f3f4f6] rounded h-12 flex items-center pl-11"
524
+ >
525
+ ${decodeSpecialChars(child.name)}
526
+ ${timedContent(
527
+ child.redDot[0].start,
528
+ child.redDot[0].end
529
+ )
530
+ ? html`<span
531
+ class="bg-orange-600 h-2 relative rounded-full w-2 -top-[12px]"
532
+ ></span>`
533
+ : ''}
534
+ </div>
535
+ </div>
536
+ `
537
+ )}
538
+ </div>
539
+ `
540
+ : ''}
541
+ </div>
542
+ `
543
+ )}
544
+ </div>
545
+ </div>
546
+ </div>
547
+ </nav>
548
+ `
549
+ }
550
+ }
@@ -0,0 +1,23 @@
1
+ export const decodeSpecialChars = (str: string) => {
2
+ if (!str) return str
3
+
4
+ const charList: { regex: RegExp; replace: string }[] = [
5
+ { regex: /&amp;amp;/g, replace: '&' },
6
+ { regex: /&amp;/g, replace: '&' },
7
+ { regex: /&ldquo;/g, replace: '“' },
8
+ { regex: /&rdquo;/g, replace: '”' },
9
+ { regex: /&lsquo;/g, replace: '‘' },
10
+ { regex: /&rsquo;/g, replace: '’' },
11
+ { regex: /&quot;/g, replace: '”' },
12
+ { regex: /&nbsp;/g, replace: ' ' },
13
+ { regex: /&hellip;/g, replace: '...' },
14
+ { regex: /&ndash;/g, replace: '–' },
15
+ ]
16
+
17
+ charList.forEach(({ regex, replace }) => {
18
+ // eslint-disable-next-line no-param-reassign
19
+ str = str.replace(regex, replace)
20
+ })
21
+
22
+ return str
23
+ }
@@ -0,0 +1,32 @@
1
+ import { parse } from 'date-fns'
2
+
3
+ const DATE_FORMAT = 'yyyy-MM-dd HH:mm:ss'
4
+
5
+ export function getTimeZonedDate(
6
+ dateStr: string | Date,
7
+ timeZone: string
8
+ ): Date {
9
+ const date =
10
+ typeof dateStr === 'string'
11
+ ? parse(dateStr, DATE_FORMAT, new Date())
12
+ : dateStr
13
+
14
+ const localeString = date.toLocaleString('en-US', { timeZone })
15
+ return new Date(localeString)
16
+ }
17
+
18
+ export function timedContent(
19
+ start: string,
20
+ end: string,
21
+ timeZone: string = 'Asia/Jakarta'
22
+ ): boolean {
23
+ const now = getTimeZonedDate(new Date(), timeZone).getTime()
24
+ const startTime = getTimeZonedDate(start, timeZone).getTime()
25
+ const endTime = getTimeZonedDate(end, timeZone).getTime()
26
+
27
+ if (startTime > endTime) {
28
+ throw new Error('Start date must be before end date')
29
+ }
30
+
31
+ return now >= startTime && now <= endTime
32
+ }