@mapvx/web-components-ssr 1.0.0-rc.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 ADDED
@@ -0,0 +1,1065 @@
1
+ # MapVX Website Component
2
+
3
+ A modern, framework-agnostic web component built with Angular 20 that provides an interactive map and location discovery interface.
4
+
5
+ ## 🚀 Features
6
+
7
+ - ✅ Compatible with any framework (React, Vue, Vanilla JS, etc.)
8
+ - ✅ Self-contained and isolated (Shadow DOM)
9
+ - ✅ Optimized bundle without hashing for production
10
+ - ✅ Angular 20 with modern architecture (standalone components)
11
+ - 🔑 **Requires API Key**: The component needs a valid API key to function
12
+
13
+ ## 📦 Installation
14
+
15
+ ### Via NPM
16
+
17
+ ```bash
18
+ npm install @mapvx/web-components-ssr
19
+ ```
20
+
21
+ After installation, you can reference the files in your application:
22
+
23
+ **JavaScript/TypeScript:**
24
+
25
+ ```javascript
26
+ // Import the main component file
27
+ import '@mapvx/web-components-ssr/dist/browser/main.js'
28
+ // Import the styles
29
+ import '@mapvx/web-components-ssr/dist/browser/styles.css'
30
+ ```
31
+
32
+ **HTML:**
33
+
34
+ ```html
35
+ <!-- Include the component files -->
36
+ <script src="node_modules/@mapvx/web-components-ssr/dist/browser/main.js"></script>
37
+ <link rel="stylesheet" href="node_modules/@mapvx/web-components-ssr/dist/browser/styles.css" />
38
+ ```
39
+
40
+ **Webpack/Vite/Bundler:**
41
+
42
+ ```javascript
43
+ // In your main entry file
44
+ import '@mapvx/web-components-ssr/dist/browser/main.js'
45
+ import '@mapvx/web-components-ssr/dist/browser/styles.css'
46
+ ```
47
+
48
+ **Framework-specific examples:**
49
+
50
+ **React:**
51
+
52
+ ```jsx
53
+ // In your main App.jsx or index.jsx
54
+ import '@mapvx/web-components-ssr/dist/browser/main.js'
55
+
56
+ function App() {
57
+ return <mapvx-website api-key="your-api-key-here" />
58
+ }
59
+ ```
60
+
61
+ ```css
62
+ /* In your main CSS file (e.g., index.css, App.css) */
63
+ @import '@mapvx/web-components-ssr/dist/browser/styles.css';
64
+ ```
65
+
66
+ **Vue:**
67
+
68
+ ```javascript
69
+ // In your main.js
70
+ import '@mapvx/web-components-ssr/dist/browser/main.js'
71
+ ```
72
+
73
+ ```css
74
+ /* In your main CSS file (e.g., style.css, main.css) */
75
+ @import '@mapvx/web-components-ssr/dist/browser/styles.css';
76
+ ```
77
+
78
+ **Angular:**
79
+
80
+ #### Method 1: HTML Script Tags
81
+
82
+ Configure assets in `angular.json` and use script tags in `index.html`:
83
+
84
+ ```json
85
+ {
86
+ "assets": [
87
+ {
88
+ "glob": "**/*",
89
+ "input": "public"
90
+ },
91
+ {
92
+ "glob": "**/*",
93
+ "input": "src/assets",
94
+ "output": "assets"
95
+ },
96
+ {
97
+ "glob": "**/*",
98
+ "input": "node_modules/@mapvx/web-components-ssr/dist/browser",
99
+ "output": "website-component"
100
+ }
101
+ ]
102
+ }
103
+ ```
104
+
105
+ Then reference the files in your `index.html`:
106
+
107
+ ```html
108
+ <!-- Web Component Scripts -->
109
+ <script src="website-component/main.js" defer></script>
110
+
111
+ <!-- Web Component Styles -->
112
+ <link rel="stylesheet" href="website-component/styles.css" />
113
+ ```
114
+
115
+ **Why this configuration is needed:**
116
+
117
+ - Angular doesn't serve files from `node_modules` by default in development
118
+ - Without proper asset configuration, you'll get `SyntaxError: Unexpected token '<'` errors
119
+ - This ensures files are served as static assets with proper caching headers
120
+
121
+ #### Method 2: TypeScript Imports
122
+
123
+ Alternatively, you can import the files in your application:
124
+
125
+ ```typescript
126
+ // In your main.ts - Import the JavaScript
127
+ import '@mapvx/web-components-ssr/dist/browser/main.js'
128
+ ```
129
+
130
+ ```scss
131
+ /* In your styles.scss - Import the styles (Modern Sass) */
132
+ @use '@mapvx/web-components-ssr/dist/browser/styles.css';
133
+
134
+ /* Alternative for older Sass versions or CSS files */
135
+ @import '@mapvx/web-components-ssr/dist/browser/styles.css';
136
+ ```
137
+
138
+ **Important**: Choose ONE method only:
139
+
140
+ - ✅ **Method 1**: HTML script tags + assets configuration
141
+ - ✅ **Method 2**: TypeScript imports + SCSS imports
142
+ - ❌ **Don't use both**: This can cause duplicate loading and conflicts
143
+
144
+ ### Helper Package (SSR Support)
145
+
146
+ For Server-Side Rendering (SSR) applications that need to preload initial data on the server:
147
+
148
+ ```bash
149
+ npm install @mapvx/web-components-ssr-helpers
150
+ ```
151
+
152
+ ### Via CDN (unpkg)
153
+
154
+ ```html
155
+ <!-- Include the component files -->
156
+ <script src="https://unpkg.com/@mapvx/web-components-ssr@latest/dist/browser/main.js"></script>
157
+ <link
158
+ rel="stylesheet"
159
+ href="https://unpkg.com/@mapvx/web-components-ssr@latest/dist/browser/styles.css"
160
+ />
161
+ ```
162
+
163
+ ## 🎯 Usage
164
+
165
+ ### Basic Usage
166
+
167
+ ```html
168
+ <!-- Basic implementation with required API key -->
169
+ <mapvx-website api-key="your-api-key-here"></mapvx-website>
170
+ ```
171
+
172
+ ### Advanced Usage with Configuration
173
+
174
+ ```html
175
+ <!-- With all available options -->
176
+ <mapvx-website
177
+ api-key="your-api-key-here"
178
+ institution-id="optional-institution-id"
179
+ initial-data='{"custom": "data"}'
180
+ default-to-map="true"
181
+ show-category-filters="false"
182
+ show-city-filter="true"
183
+ inherit-font-family="false"
184
+ >
185
+ </mapvx-website>
186
+ ```
187
+
188
+ ### Framework Integration
189
+
190
+ #### React
191
+
192
+ ```jsx
193
+ function App() {
194
+ return (
195
+ <mapvx-website
196
+ api-key="your-api-key-here"
197
+ default-to-map="true"
198
+ show-category-filters="false"
199
+ />
200
+ )
201
+ }
202
+ ```
203
+
204
+ #### Vue
205
+
206
+ ```vue
207
+ <template>
208
+ <mapvx-website
209
+ api-key="your-api-key-here"
210
+ :default-to-map="true"
211
+ :show-category-filters="false"
212
+ />
213
+ </template>
214
+ ```
215
+
216
+ #### Angular
217
+
218
+ ```typescript
219
+ // In your component
220
+ @Component({
221
+ template: `
222
+ <mapvx-website
223
+ [attr.api-key]="apiKey"
224
+ [attr.default-to-map]="defaultToMap"
225
+ [attr.show-category-filters]="showFilters"
226
+ />
227
+ `,
228
+ })
229
+ export class MyComponent {
230
+ apiKey = 'your-api-key-here'
231
+ defaultToMap = true
232
+ showFilters = false
233
+ }
234
+ ```
235
+
236
+ ## 📋 Input Properties
237
+
238
+ The web component accepts the following input properties to customize its behavior:
239
+
240
+ | Property | Type | Required | Default | Description |
241
+ | ----------------------- | --------- | ---------- | ------------------------------- | ------------------------------------------------------------------------------------------------- |
242
+ | `api-key` | `string` | ✅ **Yes** | - | Valid API key to initialize the SDK |
243
+ | `institution-id` | `string` | ✅ **Yes** | - | Institution ID for specific data filtering and search |
244
+ | `initial-data` | `string` | ❌ No | - | JSON string with initial data (use `prepareInitialData` from `@mapvx/web-components-ssr-helpers`) |
245
+ | `default-to-map` | `boolean` | ❌ No | `false` | If `true`, shows map view by default |
246
+ | `show-category-filters` | `boolean` | ❌ No | `true` | If `true`, shows category filters |
247
+ | `show-city-filter` | `boolean` | ❌ No | `true` | If `true`, shows city filter |
248
+ | `inherit-font-family` | `boolean` | ❌ No | `false` | If `true`, inherits font family from parent |
249
+ | `hide-deals` | `boolean` | ❌ No | `false` | If `true`, hides deals and promotional content |
250
+ | `show-deal-labels` | `boolean` | ❌ No | `false` | If `true`, shows deal labels over their images on the home screen |
251
+ | `card-width` | `number` | ❌ No | `237` (desktop), `170` (mobile) | Custom card width in pixels for grid view. Affects card size and number of cards per row |
252
+ | `rows-per-page` | `number` | ❌ No | `2` | Maximum number of rows per page (1-10). Shows fewer rows if not enough cards to fill all rows |
253
+
254
+ ### Input Usage Examples
255
+
256
+ #### Required Properties Only
257
+
258
+ ```html
259
+ <mapvx-website api-key="your-api-key-here" institution-id="institution-123"></mapvx-website>
260
+ ```
261
+
262
+ #### With Optional Configuration
263
+
264
+ ```html
265
+ <mapvx-website
266
+ api-key="your-api-key-here"
267
+ institution-id="institution-123"
268
+ default-to-map="true"
269
+ show-category-filters="false"
270
+ show-city-filter="true"
271
+ hide-deals="true"
272
+ show-deal-labels="true"
273
+ card-width="300"
274
+ >
275
+ </mapvx-website>
276
+ ```
277
+
278
+ #### Custom Card Width
279
+
280
+ ```html
281
+ <!-- Larger cards (300px) -->
282
+ <mapvx-website api-key="your-api-key-here" institution-id="institution-123" card-width="300">
283
+ </mapvx-website>
284
+
285
+ <!-- Smaller cards (200px) -->
286
+ <mapvx-website api-key="your-api-key-here" institution-id="institution-123" card-width="200">
287
+ </mapvx-website>
288
+ ```
289
+
290
+ #### Custom Rows Per Page
291
+
292
+ ```html
293
+ <!-- Show 3 rows per page -->
294
+ <mapvx-website api-key="your-api-key-here" institution-id="institution-123" rows-per-page="3">
295
+ </mapvx-website>
296
+
297
+ <!-- Show 1 row per page (minimal) -->
298
+ <mapvx-website api-key="your-api-key-here" institution-id="institution-123" rows-per-page="1">
299
+ </mapvx-website>
300
+
301
+ <!-- Combine with custom card width -->
302
+ <mapvx-website
303
+ api-key="your-api-key-here"
304
+ institution-id="institution-123"
305
+ card-width="250"
306
+ rows-per-page="4"
307
+ >
308
+ </mapvx-website>
309
+ ```
310
+
311
+ ## 📤 Output Events
312
+
313
+ The web component emits custom events that you can listen to for user interactions:
314
+
315
+ | Event Name | Type | Description |
316
+ | -------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
317
+ | `cardSelected` | `string` | ⚠️ **Deprecated**: Use `userAction` with type `show-place` instead. Emitted when a location card is selected. Contains the card ID. |
318
+ | `userAction` | `UserAction` | Emitted when a user performs an action (filter selection, search, place navigation, etc.). Contains action type and associated data. |
319
+
320
+ ### Output Usage Examples
321
+
322
+ #### Vanilla JavaScript
323
+
324
+ ```javascript
325
+ // Wait for the web component to be registered
326
+ function setupUserActionListener() {
327
+ const element = document.querySelector('mapvx-website')
328
+ if (element) {
329
+ element.addEventListener('userAction', (event) => {
330
+ if (event instanceof CustomEvent && event.detail.type === 'show-place') {
331
+ const placeId = event.detail.data.placeId
332
+ console.log('Place shown:', placeId)
333
+ // Handle place display
334
+ }
335
+ })
336
+ } else {
337
+ // Retry if element is not yet available
338
+ setTimeout(setupUserActionListener, 100)
339
+ }
340
+ }
341
+
342
+ // Check if web component is already registered
343
+ if (customElements.get('mapvx-website')) {
344
+ setupUserActionListener()
345
+ } else {
346
+ // Wait for registration
347
+ const checkInterval = setInterval(() => {
348
+ if (customElements.get('mapvx-website')) {
349
+ setupUserActionListener()
350
+ clearInterval(checkInterval)
351
+ }
352
+ }, 100)
353
+ }
354
+ ```
355
+
356
+ #### Update Browser URL with `history.pushState`
357
+
358
+ You can react to user actions to change the browser URL without a full page reload, which is especially useful in SSR apps looking to keep client-side navigation in sync with the selected place.
359
+
360
+ ```html
361
+ <script>
362
+ // Wait for the web component to be registered
363
+ function setupUrlUpdater() {
364
+ const element = document.querySelector('mapvx-website')
365
+ if (!element) {
366
+ setTimeout(setupUrlUpdater, 100)
367
+ return
368
+ }
369
+
370
+ const handleUserAction = (event) => {
371
+ if (event instanceof CustomEvent) {
372
+ const action = event.detail
373
+ switch (action.type) {
374
+ case 'return-to-home':
375
+ case 'select-filter': {
376
+ const { filter } = action.data
377
+ history.pushState(
378
+ { page: 'home', filter },
379
+ '',
380
+ `${location.pathname}?tab=${encodeURIComponent(filter)}`,
381
+ )
382
+ break
383
+ }
384
+ case 'show-place': {
385
+ const { placeId, alias } = action.data
386
+ if (alias) {
387
+ history.pushState(
388
+ { page: 'profile', id: alias },
389
+ '',
390
+ `${location.pathname}?tenant=${alias}`,
391
+ )
392
+ } else {
393
+ history.pushState(
394
+ { page: 'profile', id: placeId },
395
+ '',
396
+ `${location.pathname}?tenant=${placeId}`,
397
+ )
398
+ }
399
+ break
400
+ }
401
+ case 'search': {
402
+ const { searchTerm } = action.data
403
+ history.pushState(
404
+ { page: 'search', term: searchTerm },
405
+ '',
406
+ `${location.pathname}?search=${encodeURIComponent(searchTerm)}`,
407
+ )
408
+ break
409
+ }
410
+ case 'select-destination': {
411
+ const { destinationId } = action.data
412
+ history.pushState(
413
+ { page: 'route', id: destinationId },
414
+ '',
415
+ `${location.pathname}?destination=${destinationId}`,
416
+ )
417
+ break
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ element.addEventListener('userAction', handleUserAction)
424
+ }
425
+
426
+ // Check if web component is already registered
427
+ if (customElements.get('mapvx-website')) {
428
+ setupUrlUpdater()
429
+ } else {
430
+ // Wait for registration
431
+ const waitForRegistration = setInterval(() => {
432
+ if (customElements.get('mapvx-website')) {
433
+ clearInterval(waitForRegistration)
434
+ setupUrlUpdater()
435
+ }
436
+ }, 100)
437
+ }
438
+ </script>
439
+ ```
440
+
441
+ #### React
442
+
443
+ ```jsx
444
+ import { useEffect, useRef } from 'react'
445
+
446
+ function App() {
447
+ const webComponentRef = useRef(null)
448
+
449
+ useEffect(() => {
450
+ const element = webComponentRef.current
451
+ if (!element) return
452
+
453
+ const handleUserAction = (event) => {
454
+ if (event instanceof CustomEvent && event.detail.type === 'show-place') {
455
+ const placeId = event.detail.data.placeId
456
+ console.log('Place shown:', placeId)
457
+ // Handle place display
458
+ }
459
+ }
460
+
461
+ element.addEventListener('userAction', handleUserAction)
462
+
463
+ return () => {
464
+ element.removeEventListener('userAction', handleUserAction)
465
+ }
466
+ }, [])
467
+
468
+ return <mapvx-website ref={webComponentRef} api-key="your-api-key-here" />
469
+ }
470
+ ```
471
+
472
+ #### Vue
473
+
474
+ ```vue
475
+ <template>
476
+ <mapvx-website ref="webComponent" api-key="your-api-key-here" />
477
+ </template>
478
+
479
+ <script setup>
480
+ import { ref, onMounted, onUnmounted } from 'vue'
481
+
482
+ const webComponent = ref(null)
483
+
484
+ const handleUserAction = (event) => {
485
+ if (event instanceof CustomEvent && event.detail.type === 'show-place') {
486
+ const placeId = event.detail.data.placeId
487
+ console.log('Place shown:', placeId)
488
+ // Handle place display
489
+ }
490
+ }
491
+
492
+ onMounted(() => {
493
+ if (webComponent.value) {
494
+ webComponent.value.addEventListener('userAction', handleUserAction)
495
+ }
496
+ })
497
+
498
+ onUnmounted(() => {
499
+ if (webComponent.value) {
500
+ webComponent.value.removeEventListener('userAction', handleUserAction)
501
+ }
502
+ })
503
+ </script>
504
+ ```
505
+
506
+ #### Angular
507
+
508
+ ```typescript
509
+ import { Component, ElementRef, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'
510
+
511
+ @Component({
512
+ selector: 'app-mapvx',
513
+ template: ` <mapvx-website #webComponent [attr.api-key]="apiKey" /> `,
514
+ })
515
+ export class MapvxComponent implements AfterViewInit, OnDestroy {
516
+ @ViewChild('webComponent', { static: false }) webComponentRef!: ElementRef<HTMLElement>
517
+
518
+ apiKey = 'your-api-key-here'
519
+ private userActionListener?: (event: Event) => void
520
+
521
+ ngAfterViewInit() {
522
+ this.setupUserActionListener()
523
+ }
524
+
525
+ private setupUserActionListener() {
526
+ const element = this.webComponentRef?.nativeElement
527
+ if (!element) {
528
+ // Retry if element is not yet available
529
+ setTimeout(() => this.setupUserActionListener(), 100)
530
+ return
531
+ }
532
+
533
+ this.userActionListener = (event: Event) => {
534
+ if (event instanceof CustomEvent) {
535
+ const action = event.detail as { type: string; data: any }
536
+ if (action.type === 'show-place') {
537
+ const placeId = action.data.placeId
538
+ console.log('Place shown:', placeId)
539
+ // Handle place display
540
+ }
541
+ }
542
+ }
543
+
544
+ element.addEventListener('userAction', this.userActionListener)
545
+ }
546
+
547
+ ngOnDestroy() {
548
+ const element = this.webComponentRef?.nativeElement
549
+ if (element && this.userActionListener) {
550
+ element.removeEventListener('userAction', this.userActionListener)
551
+ }
552
+ }
553
+ }
554
+ ```
555
+
556
+ ### User Action Event
557
+
558
+ The `userAction` event provides detailed information about user interactions within the component. The event detail contains an object with `type` and `data` properties.
559
+
560
+ #### UserAction Types
561
+
562
+ | Type | Description | Data Structure |
563
+ | -------------------- | ------------------------------------------ | --------------------------------------- | ------------ |
564
+ | `select-filter` | Emitted when a filter is selected | `{ filter: string }` |
565
+ | `show-place` | Emitted when a place is displayed | `{ placeId: string, alias: string | undefined }` |
566
+ | `select-destination` | Emitted when a destination is selected | `{ destinationId: string, alias: string | undefined }` |
567
+ | `search` | Emitted when a search is performed | `{ searchTerm: string }` |
568
+ | `return-to-home` | Emitted when user returns to the home view | `{ filter: string }` |
569
+
570
+ #### UserAction Usage Examples
571
+
572
+ #### Vanilla JavaScript
573
+
574
+ ```javascript
575
+ // Wait for the web component to be registered
576
+ function setupUserActionListener() {
577
+ const element = document.querySelector('mapvx-website')
578
+ if (element) {
579
+ element.addEventListener('userAction', (event) => {
580
+ if (event instanceof CustomEvent) {
581
+ const action = event.detail
582
+ console.log('User action:', action.type, action.data)
583
+
584
+ // Handle different action types
585
+ switch (action.type) {
586
+ case 'select-filter':
587
+ console.log('Filter selected:', action.data.filter)
588
+ break
589
+ case 'show-place':
590
+ console.log('Place shown:', action.data.placeId)
591
+ break
592
+ case 'select-destination':
593
+ console.log('Destination selected:', action.data.destinationId)
594
+ break
595
+ case 'search':
596
+ console.log('Search performed:', action.data.searchTerm)
597
+ break
598
+ case 'return-to-home':
599
+ console.log('Returned to home with filter:', action.data.filter)
600
+ break
601
+ }
602
+ }
603
+ })
604
+ } else {
605
+ // Retry if element is not yet available
606
+ setTimeout(setupUserActionListener, 100)
607
+ }
608
+ }
609
+
610
+ // Check if web component is already registered
611
+ if (customElements.get('mapvx-website')) {
612
+ setupUserActionListener()
613
+ } else {
614
+ // Wait for registration
615
+ const checkInterval = setInterval(() => {
616
+ if (customElements.get('mapvx-website')) {
617
+ setupUserActionListener()
618
+ clearInterval(checkInterval)
619
+ }
620
+ }, 100)
621
+ }
622
+ ```
623
+
624
+ #### React
625
+
626
+ ```jsx
627
+ import { useEffect, useRef } from 'react'
628
+
629
+ function App() {
630
+ const webComponentRef = useRef(null)
631
+
632
+ useEffect(() => {
633
+ const element = webComponentRef.current
634
+ if (!element) return
635
+
636
+ const handleUserAction = (event) => {
637
+ if (event instanceof CustomEvent) {
638
+ const action = event.detail
639
+ console.log('User action:', action.type, action.data)
640
+
641
+ // Handle different action types
642
+ switch (action.type) {
643
+ case 'select-filter':
644
+ // Handle filter selection
645
+ break
646
+ case 'show-place':
647
+ // Handle place display
648
+ break
649
+ case 'search':
650
+ // Handle search
651
+ break
652
+ // ... other cases
653
+ }
654
+ }
655
+ }
656
+
657
+ element.addEventListener('userAction', handleUserAction)
658
+
659
+ return () => {
660
+ element.removeEventListener('userAction', handleUserAction)
661
+ }
662
+ }, [])
663
+
664
+ return <mapvx-website ref={webComponentRef} api-key="your-api-key-here" />
665
+ }
666
+ ```
667
+
668
+ #### Vue
669
+
670
+ ```vue
671
+ <template>
672
+ <mapvx-website ref="webComponent" api-key="your-api-key-here" />
673
+ </template>
674
+
675
+ <script setup>
676
+ import { ref, onMounted, onUnmounted } from 'vue'
677
+
678
+ const webComponent = ref(null)
679
+
680
+ const handleUserAction = (event) => {
681
+ if (event instanceof CustomEvent) {
682
+ const action = event.detail
683
+ console.log('User action:', action.type, action.data)
684
+
685
+ // Handle different action types
686
+ switch (action.type) {
687
+ case 'select-filter':
688
+ // Handle filter selection
689
+ break
690
+ case 'show-place':
691
+ // Handle place display
692
+ break
693
+ case 'search':
694
+ // Handle search
695
+ break
696
+ // ... other cases
697
+ }
698
+ }
699
+ }
700
+
701
+ onMounted(() => {
702
+ if (webComponent.value) {
703
+ webComponent.value.addEventListener('userAction', handleUserAction)
704
+ }
705
+ })
706
+
707
+ onUnmounted(() => {
708
+ if (webComponent.value) {
709
+ webComponent.value.removeEventListener('userAction', handleUserAction)
710
+ }
711
+ })
712
+ </script>
713
+ ```
714
+
715
+ #### Angular
716
+
717
+ ```typescript
718
+ import { Component, ElementRef, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'
719
+
720
+ @Component({
721
+ selector: 'app-mapvx',
722
+ template: ` <mapvx-website #webComponent [attr.api-key]="apiKey" /> `,
723
+ })
724
+ export class MapvxComponent implements AfterViewInit, OnDestroy {
725
+ @ViewChild('webComponent', { static: false }) webComponentRef!: ElementRef<HTMLElement>
726
+
727
+ apiKey = 'your-api-key-here'
728
+ private userActionListener?: (event: Event) => void
729
+
730
+ ngAfterViewInit() {
731
+ this.setupUserActionListener()
732
+ }
733
+
734
+ private setupUserActionListener() {
735
+ const element = this.webComponentRef?.nativeElement
736
+ if (!element) {
737
+ // Retry if element is not yet available
738
+ setTimeout(() => this.setupUserActionListener(), 100)
739
+ return
740
+ }
741
+
742
+ this.userActionListener = (event: Event) => {
743
+ if (event instanceof CustomEvent) {
744
+ const action = event.detail as { type: string; data: any }
745
+ console.log('User action:', action.type, action.data)
746
+
747
+ // Handle different action types
748
+ switch (action.type) {
749
+ case 'select-filter':
750
+ // Handle filter selection
751
+ break
752
+ case 'show-place':
753
+ // Handle place display
754
+ break
755
+ case 'search':
756
+ // Handle search
757
+ break
758
+ // ... other cases
759
+ }
760
+ }
761
+ }
762
+
763
+ element.addEventListener('userAction', this.userActionListener)
764
+ }
765
+
766
+ ngOnDestroy() {
767
+ const element = this.webComponentRef?.nativeElement
768
+ if (element && this.userActionListener) {
769
+ element.removeEventListener('userAction', this.userActionListener)
770
+ }
771
+ }
772
+ }
773
+ ```
774
+
775
+ ## 🔧 Server-Side Rendering (SSR) Support
776
+
777
+ For applications using Server-Side Rendering (SSR) where you need to preload initial data on the server before sending it to the browser:
778
+
779
+ ### Installation
780
+
781
+ ```bash
782
+ npm install @mapvx/web-components-ssr-helpers
783
+ ```
784
+
785
+ ### Server-Side Usage
786
+
787
+ #### Next.js (App Router)
788
+
789
+ **1. Install dependencies:**
790
+
791
+ ```bash
792
+ npm install @mapvx/web-components-ssr @mapvx/web-components-ssr-helpers
793
+ ```
794
+
795
+ **2. Copy the bundle script to your `package.json`:**
796
+
797
+ ```json
798
+ {
799
+ "scripts": {
800
+ "copy-bundle": "node copy-bundle.mjs"
801
+ }
802
+ }
803
+ ```
804
+
805
+ **3. Create `copy-bundle.mjs` in your project root:**
806
+
807
+ ```javascript
808
+ #!/usr/bin/env node
809
+
810
+ import { existsSync, mkdirSync, copyFileSync } from 'fs'
811
+ import { join } from 'path'
812
+
813
+ const sourcePath = 'node_modules/@mapvx/web-components-ssr/dist/browser/main.js'
814
+ const targetDir = 'public/mapvx-website'
815
+ const targetPath = join(targetDir, 'bundle.js')
816
+
817
+ if (!existsSync(sourcePath)) {
818
+ console.error(`❌ Source file not found: ${sourcePath}`)
819
+ process.exit(1)
820
+ }
821
+
822
+ if (!existsSync(targetDir)) {
823
+ mkdirSync(targetDir, { recursive: true })
824
+ }
825
+
826
+ copyFileSync(sourcePath, targetPath)
827
+ console.log(`✅ Bundle copied successfully!`)
828
+ ```
829
+
830
+ **4. Create your page component:**
831
+
832
+ ```typescript
833
+ // app/page.tsx
834
+ import { prepareInitialData } from '@mapvx/web-components-ssr-helpers';
835
+ import '@mapvx/web-components-ssr/dist/browser/styles.css';
836
+ import Script from 'next/script';
837
+
838
+ export default async function HomePage() {
839
+ const initialData = await prepareInitialData(process.env.NEXT_PUBLIC_MAPVX_API_KEY || '');
840
+
841
+ return (
842
+ <div>
843
+ <Script src='/mapvx-website/bundle.js' />
844
+ <mapvx-website
845
+ api-key={process.env.NEXT_PUBLIC_MAPVX_API_KEY}
846
+ institution-id={process.env.NEXT_PUBLIC_MAPVX_INSTITUTION_ID}
847
+ initial-data={initialData}
848
+ default-to-map="true"
849
+ />
850
+ </div>
851
+ );
852
+ }
853
+ ```
854
+
855
+ **5. Set environment variables in `.env.local`:**
856
+
857
+ ```bash
858
+ NEXT_PUBLIC_MAPVX_API_KEY=your-api-key-here
859
+ NEXT_PUBLIC_MAPVX_INSTITUTION_ID=your-institution-id
860
+ ```
861
+
862
+ **6. Run the copy script before building:**
863
+
864
+ ```bash
865
+ npm run copy-bundle
866
+ npm run build
867
+ ```
868
+
869
+ #### Nuxt.js
870
+
871
+ ```vue
872
+ <!-- pages/index.vue -->
873
+ <template>
874
+ <div>
875
+ <h1>My App</h1>
876
+ <mapvx-website :api-key="apiKey" :initial-data="initialData" default-to-map="true" />
877
+ </div>
878
+ </template>
879
+
880
+ <script setup>
881
+ import { prepareInitialData } from '@mapvx/web-components-ssr-helpers'
882
+
883
+ // Server-side data fetching
884
+ const { data: initialData } = await useAsyncData('mapvx-data', async () => {
885
+ return await prepareInitialData(process.env.MAPVX_API_KEY)
886
+ })
887
+
888
+ const apiKey = process.env.MAPVX_API_KEY
889
+ </script>
890
+ ```
891
+
892
+ #### SvelteKit
893
+
894
+ ```typescript
895
+ // src/routes/+page.server.ts
896
+ import { prepareInitialData } from '@mapvx/web-components-ssr-helpers'
897
+ import type { PageServerLoad } from './$types'
898
+
899
+ export const load: PageServerLoad = async () => {
900
+ // Execute on the server
901
+ const initialData = await prepareInitialData(process.env.MAPVX_API_KEY!)
902
+
903
+ return {
904
+ initialData,
905
+ }
906
+ }
907
+ ```
908
+
909
+ ```svelte
910
+ <!-- src/routes/+page.svelte -->
911
+ <script lang="ts">
912
+ import type { PageData } from './$types';
913
+
914
+ export let data: PageData;
915
+ </script>
916
+
917
+ <div>
918
+ <h1>My App</h1>
919
+ <mapvx-website
920
+ api-key={process.env.MAPVX_API_KEY}
921
+ initial-data={data.initialData}
922
+ default-to-map="true"
923
+ />
924
+ </div>
925
+ ```
926
+
927
+ ### What `prepareInitialData` does
928
+
929
+ The `prepareInitialData` function is specifically designed for SSR applications:
930
+
931
+ - **Server-side execution**: Must be called on the server, not in the browser
932
+ - **Data fetching**: Fetches available places and institutions from the API
933
+ - **Performance optimization**: Preloads data on the server to avoid client-side API calls
934
+ - **SEO benefits**: Ensures data is available during server-side rendering
935
+ - **Returns**: JSON string with combined places and institutions data
936
+
937
+ ### Important Notes
938
+
939
+ ⚠️ **SSR Only**: This function should only be used in server-side rendering contexts. For client-side applications, the web component will fetch data automatically when needed.
940
+
941
+ ## 🎨 Customization
942
+
943
+ ### Custom Colors
944
+
945
+ The web component supports custom color theming through CSS custom properties. You can override the default colors by defining CSS variables in your application's root:
946
+
947
+ ```css
948
+ :root {
949
+ --mapvx-secondary-orange: #b41500;
950
+ --mapvx-secondary-blue: #053a96;
951
+ }
952
+ ```
953
+
954
+ #### Example Implementation
955
+
956
+ **HTML with inline styles:**
957
+
958
+ ```html
959
+ <style>
960
+ :root {
961
+ --mapvx-secondary-orange: #ff6b35;
962
+ --mapvx-secondary-blue: #1e3a8a;
963
+ }
964
+ </style>
965
+
966
+ <mapvx-website api-key="your-api-key-here"></mapvx-website>
967
+ ```
968
+
969
+ **CSS file:**
970
+
971
+ ```css
972
+ /* styles.css */
973
+ :root {
974
+ --mapvx-secondary-orange: #e74c3c;
975
+ --mapvx-secondary-blue: #3498db;
976
+ }
977
+ ```
978
+
979
+ **Framework-specific examples:**
980
+
981
+ **React:**
982
+
983
+ ```jsx
984
+ // In your CSS file or styled-components
985
+ :root {
986
+ --mapvx-secondary-orange: #ff6b35;
987
+ --mapvx-secondary-blue: #1e3a8a;
988
+ }
989
+
990
+ function App() {
991
+ return <mapvx-website api-key="your-api-key-here" />;
992
+ }
993
+ ```
994
+
995
+ **Vue:**
996
+
997
+ ```vue
998
+ <template>
999
+ <mapvx-website api-key="your-api-key-here" />
1000
+ </template>
1001
+
1002
+ <style>
1003
+ :root {
1004
+ --mapvx-secondary-orange: #ff6b35;
1005
+ --mapvx-secondary-blue: #1e3a8a;
1006
+ }
1007
+ </style>
1008
+ ```
1009
+
1010
+ **Angular:**
1011
+
1012
+ ```scss
1013
+ // styles.scss
1014
+ :root {
1015
+ --mapvx-secondary-orange: #ff6b35;
1016
+ --mapvx-secondary-blue: #1e3a8a;
1017
+ }
1018
+ ```
1019
+
1020
+ ### Available Color Variables
1021
+
1022
+ | Variable | Default Value | Description |
1023
+ | -------------------------- | ------------- | ------------------------------------------ |
1024
+ | `--mapvx-secondary-orange` | `#EE5845` | Secondary orange color used in UI elements |
1025
+ | `--mapvx-secondary-blue` | `#2C57A0` | Secondary blue color used in UI elements |
1026
+
1027
+ ### Notes
1028
+
1029
+ - Colors are applied globally when defined in `:root`
1030
+ - The web component will automatically pick up these custom colors
1031
+ - Use valid CSS color values (hex, rgb, hsl, etc.)
1032
+
1033
+ ## 🔧 Troubleshooting
1034
+
1035
+ ### Common Issues
1036
+
1037
+ #### `SyntaxError: Unexpected token '<'` in Angular
1038
+
1039
+ **Problem**: Angular returns HTML instead of JavaScript files from `node_modules`.
1040
+
1041
+ **Solution**: Configure assets in `angular.json` as shown in the Angular Configuration section above.
1042
+
1043
+ #### Slow Loading (>10 seconds)
1044
+
1045
+ **Problem**: Large bundle size or incorrect asset serving.
1046
+
1047
+ **Solutions**:
1048
+
1049
+ - Use the recommended Angular assets configuration
1050
+ - Ensure files are served as static assets, not through routing
1051
+ - Check network tab in DevTools for 404 errors
1052
+
1053
+ #### Web Component Not Loading
1054
+
1055
+ **Problem**: Scripts not loading or component not registering.
1056
+
1057
+ **Solutions**:
1058
+
1059
+ - Verify file paths are correct
1060
+ - Check browser console for errors
1061
+ - Ensure `defer` attribute is used for script tags
1062
+
1063
+ ## 🏠 Homepage
1064
+
1065
+ Visit [MapVX](https://mapvx.com) for more information.