@eox/pages-theme-eox 1.9.0 → 2.0.0-alpha.10
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/.gitlab-ci.yml +4 -1
- package/README.md +88 -11
- package/cypress/support/mocks/authors.data.js +14 -0
- package/cypress/support/mocks/helpers.js +65 -4
- package/cypress/support/mocks/vitepress.js +8 -1
- package/cypress.config.js +14 -0
- package/package.json +3 -1
- package/src/Layout.vue +69 -1
- package/src/authors.data.js +14 -0
- package/src/components/BlogList.vue +655 -0
- package/src/components/BlogPost.vue +472 -0
- package/src/components/CTASection.vue +18 -4
- package/src/components/FeatureCard/Index.vue +462 -31
- package/src/components/FeatureCard/Poster.vue +25 -3
- package/src/components/FeatureSection.cy.js +16 -0
- package/src/components/FeatureSection.vue +3 -8
- package/src/components/FeaturesGallery.vue +34 -2
- package/src/components/Footer.cy.js +52 -2
- package/src/components/Footer.vue +17 -5
- package/src/components/HeroSection.cy.js +20 -0
- package/src/components/HeroSection.vue +140 -25
- package/src/components/ImageCarousel.vue +11 -2
- package/src/components/ImageElement.vue +16 -4
- package/src/components/ImageGallery.vue +29 -4
- package/src/components/LogoSection.vue +26 -3
- package/src/components/MobileNavDropdown.vue +13 -3
- package/src/components/NavBar.cy.js +15 -0
- package/src/components/NavBar.vue +149 -21
- package/src/components/NavDropdown.vue +12 -2
- package/src/components/OpenPositions.vue +292 -0
- package/src/components/SectionHeader.cy.js +54 -0
- package/src/components/SectionHeader.vue +115 -0
- package/src/components/TagList.vue +311 -0
- package/src/components/TeamPage.vue +347 -0
- package/src/components/TestimonialsSection.cy.js +76 -0
- package/src/components/TestimonialsSection.vue +368 -0
- package/src/components/TextSection.cy.js +115 -0
- package/src/components/TextSection.vue +396 -0
- package/src/helpers.js +182 -37
- package/src/helpers.test.js +181 -0
- package/src/index.js +16 -0
- package/src/pages/author/[author].md +5 -0
- package/src/pages/author/[author].paths.js +60 -0
- package/src/pages/blog.md +5 -0
- package/src/pages/tag/[tag].md +5 -0
- package/src/pages/tag/[tag].paths.js +49 -0
- package/src/pages/tag.md +5 -0
- package/src/pages/team.md +5 -0
- package/src/posts.data.js +134 -0
- package/src/style.css +389 -8
- package/src/tagUtils.js +148 -0
- package/src/team.data.js +1 -1
- package/src/vitepressConfig.mjs +101 -1
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section
|
|
3
|
+
class="text-section full-width"
|
|
4
|
+
:class="{
|
|
5
|
+
'is-reverse': reverse,
|
|
6
|
+
'no-image': !image && !$slots.media,
|
|
7
|
+
'is-compact': compact,
|
|
8
|
+
}"
|
|
9
|
+
>
|
|
10
|
+
<div class="holder">
|
|
11
|
+
<div
|
|
12
|
+
class="text-section-grid"
|
|
13
|
+
:class="{
|
|
14
|
+
'grid-reverse': reverse,
|
|
15
|
+
'grid-single': !image && !$slots.media,
|
|
16
|
+
}"
|
|
17
|
+
>
|
|
18
|
+
<!-- Text Column -->
|
|
19
|
+
<div class="text-column">
|
|
20
|
+
<span v-if="tagline" class="tagline">{{ tagline }}</span>
|
|
21
|
+
<h2 v-if="title" class="section-title">{{ title }}</h2>
|
|
22
|
+
<div class="section-content">
|
|
23
|
+
<slot></slot>
|
|
24
|
+
</div>
|
|
25
|
+
<!-- Prop-based button -->
|
|
26
|
+
<div v-if="buttonText && buttonLink" class="section-actions">
|
|
27
|
+
<a
|
|
28
|
+
:href="resolvedButtonLink"
|
|
29
|
+
:target="resolvedButtonTarget"
|
|
30
|
+
class="button"
|
|
31
|
+
@click="
|
|
32
|
+
trackEvent([
|
|
33
|
+
'TextSection',
|
|
34
|
+
'Click',
|
|
35
|
+
title || buttonText,
|
|
36
|
+
buttonText,
|
|
37
|
+
])
|
|
38
|
+
"
|
|
39
|
+
>
|
|
40
|
+
<span>{{ buttonText }}</span>
|
|
41
|
+
<i v-if="buttonIcon" :class="buttonIcon" class="btn-icon"></i>
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
44
|
+
<slot name="actions"></slot>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Media Column -->
|
|
48
|
+
<div v-if="image || $slots.media" class="media-column">
|
|
49
|
+
<slot name="media">
|
|
50
|
+
<img
|
|
51
|
+
v-if="image"
|
|
52
|
+
:src="resolvedImage"
|
|
53
|
+
:alt="imageAlt || title || ''"
|
|
54
|
+
class="section-image"
|
|
55
|
+
loading="lazy"
|
|
56
|
+
/>
|
|
57
|
+
</slot>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</section>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script setup>
|
|
65
|
+
import { withBase } from "vitepress";
|
|
66
|
+
import { computed } from "vue";
|
|
67
|
+
import { trackEvent } from "../helpers.js";
|
|
68
|
+
|
|
69
|
+
const props = defineProps({
|
|
70
|
+
title: {
|
|
71
|
+
type: String,
|
|
72
|
+
default: "",
|
|
73
|
+
},
|
|
74
|
+
tagline: {
|
|
75
|
+
type: String,
|
|
76
|
+
default: "",
|
|
77
|
+
},
|
|
78
|
+
image: {
|
|
79
|
+
type: String,
|
|
80
|
+
default: "",
|
|
81
|
+
},
|
|
82
|
+
imageAlt: {
|
|
83
|
+
type: String,
|
|
84
|
+
default: "",
|
|
85
|
+
},
|
|
86
|
+
reverse: {
|
|
87
|
+
type: Boolean,
|
|
88
|
+
default: false,
|
|
89
|
+
},
|
|
90
|
+
buttonText: {
|
|
91
|
+
type: String,
|
|
92
|
+
default: "",
|
|
93
|
+
},
|
|
94
|
+
buttonLink: {
|
|
95
|
+
type: String,
|
|
96
|
+
default: "",
|
|
97
|
+
},
|
|
98
|
+
buttonTarget: {
|
|
99
|
+
type: String,
|
|
100
|
+
default: "",
|
|
101
|
+
},
|
|
102
|
+
buttonIcon: {
|
|
103
|
+
type: String,
|
|
104
|
+
default: "",
|
|
105
|
+
},
|
|
106
|
+
compact: {
|
|
107
|
+
type: Boolean,
|
|
108
|
+
default: false,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function resolveUrl(url) {
|
|
113
|
+
if (!url) return "";
|
|
114
|
+
if (
|
|
115
|
+
url.startsWith("http://") ||
|
|
116
|
+
url.startsWith("https://") ||
|
|
117
|
+
url.startsWith("//") ||
|
|
118
|
+
url.startsWith("#")
|
|
119
|
+
) {
|
|
120
|
+
return url;
|
|
121
|
+
}
|
|
122
|
+
return withBase(url);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const resolvedImage = computed(() => resolveUrl(props.image));
|
|
126
|
+
const resolvedButtonLink = computed(() => resolveUrl(props.buttonLink));
|
|
127
|
+
const resolvedButtonTarget = computed(() => {
|
|
128
|
+
if (props.buttonTarget) return props.buttonTarget;
|
|
129
|
+
if (
|
|
130
|
+
props.buttonLink &&
|
|
131
|
+
(props.buttonLink.startsWith("http://") ||
|
|
132
|
+
props.buttonLink.startsWith("https://") ||
|
|
133
|
+
props.buttonLink.startsWith("//"))
|
|
134
|
+
) {
|
|
135
|
+
return "_blank";
|
|
136
|
+
}
|
|
137
|
+
return "_self";
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<style scoped>
|
|
142
|
+
.full-width {
|
|
143
|
+
width: 100svw;
|
|
144
|
+
position: relative;
|
|
145
|
+
left: 50%;
|
|
146
|
+
right: 50%;
|
|
147
|
+
margin-left: -50svw;
|
|
148
|
+
margin-right: -50svw;
|
|
149
|
+
max-inline-size: unset !important;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.text-section {
|
|
153
|
+
padding-block-start: 56px;
|
|
154
|
+
padding-block-end: 80px;
|
|
155
|
+
display: flex;
|
|
156
|
+
justify-content: center;
|
|
157
|
+
background: transparent;
|
|
158
|
+
color: var(--on-surface, #020617);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.text-section.is-compact {
|
|
162
|
+
padding-block-start: 28px;
|
|
163
|
+
padding-block-end: 40px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* When text-sections are placed consecutively, tighten spacing and only apply extra end padding to the last one */
|
|
167
|
+
:deep(.text-section:has(+ .text-section)),
|
|
168
|
+
.text-section:has(+ .text-section) {
|
|
169
|
+
padding-block-end: 56px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
:deep(.text-section.is-compact:has(+ .text-section)),
|
|
173
|
+
.text-section.is-compact:has(+ .text-section) {
|
|
174
|
+
padding-block-end: 28px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
:deep(.text-section + .text-section),
|
|
178
|
+
.text-section + .text-section {
|
|
179
|
+
padding-block-start: 16px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.text-section .holder {
|
|
183
|
+
width: 100%;
|
|
184
|
+
max-width: 1200px;
|
|
185
|
+
margin-left: auto;
|
|
186
|
+
margin-right: auto;
|
|
187
|
+
padding-inline: 24px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.text-section-grid {
|
|
191
|
+
display: grid;
|
|
192
|
+
grid-template-columns: 1fr;
|
|
193
|
+
gap: 2rem;
|
|
194
|
+
align-items: center;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* On mobile (< 1024px), the media/image column is always at the start before the text */
|
|
198
|
+
.text-section-grid .media-column {
|
|
199
|
+
order: 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.text-section-grid .text-column {
|
|
203
|
+
order: 2;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@media (min-width: 1024px) {
|
|
207
|
+
.text-section-grid {
|
|
208
|
+
grid-template-columns: 1fr 1fr;
|
|
209
|
+
gap: 4rem;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Default on desktop: Text on left (1), Media on right (2) */
|
|
213
|
+
.text-section-grid .text-column {
|
|
214
|
+
order: 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.text-section-grid .media-column {
|
|
218
|
+
order: 2;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Reverse on desktop: Media on left (1), Text on right (2) */
|
|
222
|
+
.text-section-grid.grid-reverse .text-column {
|
|
223
|
+
order: 2;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.text-section-grid.grid-reverse .media-column {
|
|
227
|
+
order: 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.text-section-grid.grid-single {
|
|
231
|
+
grid-template-columns: 1fr;
|
|
232
|
+
max-width: 680px;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.text-column {
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-direction: column;
|
|
239
|
+
width: 100%;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.tagline {
|
|
243
|
+
display: inline-block;
|
|
244
|
+
font-size: 0.875rem;
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
text-transform: uppercase;
|
|
247
|
+
letter-spacing: 0.05em;
|
|
248
|
+
color: var(--primary, #004170);
|
|
249
|
+
margin-bottom: 0.5rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.section-title {
|
|
253
|
+
font-family: inherit;
|
|
254
|
+
font-weight: 700;
|
|
255
|
+
font-size: 2rem;
|
|
256
|
+
line-height: 1.25;
|
|
257
|
+
color: #020617;
|
|
258
|
+
margin-top: 0;
|
|
259
|
+
margin-bottom: 1.25rem;
|
|
260
|
+
border-top: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* Markdown Content Styling */
|
|
264
|
+
.section-content :deep(h2) {
|
|
265
|
+
font-family: inherit;
|
|
266
|
+
font-weight: 700;
|
|
267
|
+
font-size: 2rem;
|
|
268
|
+
line-height: 1.25;
|
|
269
|
+
color: #020617;
|
|
270
|
+
margin-top: 0;
|
|
271
|
+
margin-bottom: 1.25rem;
|
|
272
|
+
border-top: none;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.section-content :deep(h3) {
|
|
276
|
+
font-family: inherit;
|
|
277
|
+
font-weight: 700;
|
|
278
|
+
font-size: 1.5rem;
|
|
279
|
+
line-height: 1.35;
|
|
280
|
+
color: #020617;
|
|
281
|
+
margin-top: 1.5rem;
|
|
282
|
+
margin-bottom: 0.75rem;
|
|
283
|
+
border-top: none;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.section-content :deep(h4),
|
|
287
|
+
.section-content :deep(h5) {
|
|
288
|
+
font-family: inherit;
|
|
289
|
+
font-weight: 600;
|
|
290
|
+
font-size: 1.125rem;
|
|
291
|
+
line-height: 1.4;
|
|
292
|
+
color: #020617;
|
|
293
|
+
margin-top: 1.25rem;
|
|
294
|
+
margin-bottom: 0.5rem;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.section-content :deep(p) {
|
|
298
|
+
font-size: 1rem;
|
|
299
|
+
line-height: 1.65;
|
|
300
|
+
color: #334155;
|
|
301
|
+
margin-top: 0;
|
|
302
|
+
margin-bottom: 1rem;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.section-content :deep(ul) {
|
|
306
|
+
margin-top: 0.75rem;
|
|
307
|
+
margin-bottom: 1.25rem;
|
|
308
|
+
padding-left: 1.25rem;
|
|
309
|
+
list-style-type: disc;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.section-content :deep(li) {
|
|
313
|
+
font-size: 0.95rem;
|
|
314
|
+
line-height: 1.6;
|
|
315
|
+
color: #334155;
|
|
316
|
+
margin-bottom: 0.5rem;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.section-content :deep(li strong) {
|
|
320
|
+
color: #020617;
|
|
321
|
+
font-weight: 700;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.section-content :deep(a:not(.button)) {
|
|
325
|
+
color: #0284c7;
|
|
326
|
+
text-decoration: none;
|
|
327
|
+
font-weight: 500;
|
|
328
|
+
transition: color 0.15s ease-in-out;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.section-content :deep(a:not(.button):hover) {
|
|
332
|
+
text-decoration: underline;
|
|
333
|
+
color: #0369a1;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* Pill Button Styling - for both prop and markdown links with .button */
|
|
337
|
+
.section-actions {
|
|
338
|
+
margin-top: 0.75rem;
|
|
339
|
+
margin-bottom: 0.5rem;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.button,
|
|
343
|
+
.section-content :deep(a.button),
|
|
344
|
+
.section-content :deep(.button) {
|
|
345
|
+
display: inline-flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
justify-content: center;
|
|
348
|
+
gap: 0.5rem;
|
|
349
|
+
padding: 0.5rem 1.5rem;
|
|
350
|
+
border-radius: 9999px;
|
|
351
|
+
border: 1px solid #020617;
|
|
352
|
+
background-color: #ffffff;
|
|
353
|
+
color: #020617 !important;
|
|
354
|
+
font-weight: 600;
|
|
355
|
+
font-size: 0.9375rem;
|
|
356
|
+
line-height: 1.5;
|
|
357
|
+
text-decoration: none !important;
|
|
358
|
+
transition: all 0.2s ease-in-out;
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
width: fit-content;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.button:hover,
|
|
364
|
+
.section-content :deep(a.button:hover),
|
|
365
|
+
.section-content :deep(.button:hover) {
|
|
366
|
+
background-color: #f1f5f9;
|
|
367
|
+
border-color: #020617;
|
|
368
|
+
color: #020617 !important;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.btn-icon {
|
|
372
|
+
font-size: 1.1em;
|
|
373
|
+
display: inline-flex;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Media Column & Image Styling */
|
|
377
|
+
.media-column {
|
|
378
|
+
display: flex;
|
|
379
|
+
justify-content: center;
|
|
380
|
+
align-items: center;
|
|
381
|
+
width: 100%;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.section-image {
|
|
385
|
+
width: 100%;
|
|
386
|
+
max-width: 100%;
|
|
387
|
+
height: auto;
|
|
388
|
+
border-radius: 16px;
|
|
389
|
+
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
390
|
+
box-shadow:
|
|
391
|
+
0 4px 16px -2px rgba(15, 23, 42, 0.06),
|
|
392
|
+
0 2px 6px -2px rgba(15, 23, 42, 0.04);
|
|
393
|
+
background-color: #ffffff;
|
|
394
|
+
display: block;
|
|
395
|
+
}
|
|
396
|
+
</style>
|
package/src/helpers.js
CHANGED
|
@@ -1,28 +1,157 @@
|
|
|
1
1
|
import { watch } from "vue";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Normalizes a URL / path for tracking by:
|
|
5
|
+
* - Removing trailing slash (e.g. `/explore/` -> `/explore`, except for root `/`)
|
|
6
|
+
* - Stripping transient / high-cardinality UI params (e.g., coordinates `x`, `y`, `z`)
|
|
7
|
+
* - Sorting remaining query parameters alphabetically
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* @param {string} inputUrl - The URL or relative path to normalize
|
|
10
|
+
* @param {Object} [options] - Normalization options
|
|
11
|
+
* @param {Array<string>} [options.ignoredParams] - Query parameter keys to strip
|
|
12
|
+
* @param {boolean} [options.stripTrailingSlash=true] - Whether to strip trailing slashes
|
|
13
|
+
* @returns {string} Normalized URL / path
|
|
14
|
+
*/
|
|
15
|
+
export const normalizeTrackingUrl = (inputUrl, options = {}) => {
|
|
16
|
+
if (!inputUrl || typeof inputUrl !== "string") return inputUrl;
|
|
17
|
+
const { ignoredParams = ["x", "y", "z"], stripTrailingSlash = true } =
|
|
18
|
+
options;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const base =
|
|
22
|
+
typeof window !== "undefined" && window.location?.href
|
|
23
|
+
? window.location.href
|
|
24
|
+
: "https://eox.at";
|
|
25
|
+
const url = new URL(inputUrl, base);
|
|
26
|
+
let pathname = url.pathname;
|
|
27
|
+
|
|
28
|
+
if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) {
|
|
29
|
+
pathname = pathname.replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const searchParams = new URLSearchParams(url.search);
|
|
33
|
+
for (const key of ignoredParams) {
|
|
34
|
+
searchParams.delete(key);
|
|
35
|
+
}
|
|
36
|
+
searchParams.sort();
|
|
37
|
+
|
|
38
|
+
const search = searchParams.toString();
|
|
39
|
+
const isRelativeInput = !/^https?:\/\//i.test(inputUrl);
|
|
40
|
+
|
|
41
|
+
if (isRelativeInput) {
|
|
42
|
+
return pathname + (search ? `?${search}` : "");
|
|
43
|
+
}
|
|
44
|
+
url.pathname = pathname;
|
|
45
|
+
url.search = search ? `?${search}` : "";
|
|
46
|
+
return url.toString();
|
|
47
|
+
} catch {
|
|
48
|
+
if (stripTrailingSlash && inputUrl.length > 1 && inputUrl.endsWith("/")) {
|
|
49
|
+
return inputUrl.replace(/\/+$/, "");
|
|
50
|
+
}
|
|
51
|
+
return inputUrl;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Track Matomo event with array or positional arguments of four primary components:
|
|
57
|
+
* Category, Action, Name (optional), Value (optional)
|
|
58
|
+
*
|
|
59
|
+
* Automatically normalizes any path parameters (e.g., Action or Name).
|
|
10
60
|
*
|
|
11
61
|
* See https://matomo.org/faq/reports/the-anatomy-of-an-event/
|
|
12
62
|
*
|
|
13
|
-
* @param {Array<string
|
|
63
|
+
* @param {Array<string|number>|string} eventDetails - Array of event details or Category string
|
|
64
|
+
* @param {...*} rest - Remaining event details if passed positionally
|
|
65
|
+
*/
|
|
66
|
+
export const trackEvent = (eventDetails, ...rest) => {
|
|
67
|
+
if (typeof window === "undefined") return;
|
|
68
|
+
const _paq = (window._paq = window._paq || []);
|
|
69
|
+
|
|
70
|
+
const details = Array.isArray(eventDetails)
|
|
71
|
+
? [...eventDetails]
|
|
72
|
+
: [eventDetails, ...rest];
|
|
73
|
+
|
|
74
|
+
if (typeof details[1] === "string" && details[1].startsWith("/")) {
|
|
75
|
+
details[1] = normalizeTrackingUrl(details[1]);
|
|
76
|
+
}
|
|
77
|
+
if (typeof details[2] === "string" && details[2].startsWith("/")) {
|
|
78
|
+
details[2] = normalizeTrackingUrl(details[2]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_paq.push(["trackEvent", ...details]);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Track page view with normalized URL and standardized document title.
|
|
86
|
+
*
|
|
87
|
+
* @param {Object|string} [options] - Router instance, options object, or path string
|
|
88
|
+
* @param {string} [options.path] - Custom path to track
|
|
89
|
+
* @param {string} [options.title] - Custom document title to track
|
|
90
|
+
* @param {Object} [options.route] - VitePress router route object
|
|
14
91
|
*/
|
|
15
|
-
export const
|
|
16
|
-
if (
|
|
17
|
-
|
|
92
|
+
export const trackPageView = (options = {}) => {
|
|
93
|
+
if (typeof window === "undefined") return;
|
|
94
|
+
const _paq = (window._paq = window._paq || []);
|
|
95
|
+
|
|
96
|
+
let rawPath = "";
|
|
97
|
+
let rawTitle = "";
|
|
98
|
+
|
|
99
|
+
if (typeof options === "string") {
|
|
100
|
+
rawPath = options;
|
|
101
|
+
} else if (options && typeof options === "object") {
|
|
102
|
+
if (options.route) {
|
|
103
|
+
rawPath = options.route.path;
|
|
104
|
+
rawTitle = options.route.data?.title;
|
|
105
|
+
} else {
|
|
106
|
+
rawPath =
|
|
107
|
+
options.path ||
|
|
108
|
+
options.customUrl ||
|
|
109
|
+
(typeof window !== "undefined"
|
|
110
|
+
? window.location.pathname + window.location.search
|
|
111
|
+
: "");
|
|
112
|
+
rawTitle =
|
|
113
|
+
options.title ||
|
|
114
|
+
options.customTitle ||
|
|
115
|
+
(typeof document !== "undefined" ? document.title : "");
|
|
116
|
+
}
|
|
18
117
|
}
|
|
118
|
+
|
|
119
|
+
const normalizedUrl = normalizeTrackingUrl(rawPath);
|
|
120
|
+
if (normalizedUrl) {
|
|
121
|
+
_paq.push(["setCustomUrl", normalizedUrl]);
|
|
122
|
+
}
|
|
123
|
+
if (rawTitle) {
|
|
124
|
+
_paq.push(["setDocumentTitle", rawTitle.trim()]);
|
|
125
|
+
}
|
|
126
|
+
_paq.push(["trackPageView"]);
|
|
127
|
+
_paq.push(["enableLinkTracking"]);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Track site search keyword query in Matomo.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} keyword - Searched term
|
|
134
|
+
* @param {string|boolean} [category=false] - Search category (e.g. 'stories', 'indicators')
|
|
135
|
+
* @param {number} [resultsCount=0] - Number of search results found
|
|
136
|
+
*/
|
|
137
|
+
export const trackSiteSearch = (
|
|
138
|
+
keyword,
|
|
139
|
+
category = false,
|
|
140
|
+
resultsCount = 0,
|
|
141
|
+
) => {
|
|
142
|
+
if (typeof window === "undefined") return;
|
|
143
|
+
const _paq = (window._paq = window._paq || []);
|
|
144
|
+
_paq.push(["trackSiteSearch", keyword, category, resultsCount]);
|
|
19
145
|
};
|
|
20
146
|
|
|
21
147
|
/**
|
|
22
148
|
* Accept cookies
|
|
23
149
|
*/
|
|
24
150
|
export const acceptCookies = (router) => {
|
|
25
|
-
|
|
151
|
+
if (typeof window !== "undefined") {
|
|
152
|
+
const _paq = (window._paq = window._paq || []);
|
|
153
|
+
_paq.push(["rememberCookieConsentGiven"]);
|
|
154
|
+
}
|
|
26
155
|
showBanner(false);
|
|
27
156
|
enableTracking(true, router);
|
|
28
157
|
};
|
|
@@ -31,8 +160,11 @@ export const acceptCookies = (router) => {
|
|
|
31
160
|
* Decline cookies
|
|
32
161
|
*/
|
|
33
162
|
export const declineCookies = (router) => {
|
|
34
|
-
|
|
35
|
-
|
|
163
|
+
if (typeof window !== "undefined") {
|
|
164
|
+
const _paq = (window._paq = window._paq || []);
|
|
165
|
+
_paq.push(["forgetCookieConsentGiven"]);
|
|
166
|
+
_paq.push(["optUserOut"]);
|
|
167
|
+
}
|
|
36
168
|
showBanner(false);
|
|
37
169
|
enableTracking(false, router);
|
|
38
170
|
};
|
|
@@ -60,17 +192,24 @@ export const enableTracking = (enabled, router) => {
|
|
|
60
192
|
};
|
|
61
193
|
|
|
62
194
|
/**
|
|
63
|
-
* Track page scrolling depth
|
|
195
|
+
* Track page scrolling depth with normalized path and numeric depth values.
|
|
64
196
|
*
|
|
65
197
|
* @param enabled Boolean
|
|
198
|
+
* @param router VitePress router
|
|
66
199
|
*/
|
|
67
200
|
export const trackPageScrolling = (enabled, router) => {
|
|
68
201
|
function getScrollPercent() {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
202
|
+
try {
|
|
203
|
+
const h = document.documentElement,
|
|
204
|
+
b = document.body,
|
|
205
|
+
st = "scrollTop",
|
|
206
|
+
sh = "scrollHeight";
|
|
207
|
+
const scrollable = (h[sh] || b[sh]) - h.clientHeight;
|
|
208
|
+
if (scrollable <= 0) return 0;
|
|
209
|
+
return ((h[st] || b[st]) / scrollable) * 100;
|
|
210
|
+
} catch {
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
74
213
|
}
|
|
75
214
|
const scrollTargets = {
|
|
76
215
|
25: [],
|
|
@@ -79,17 +218,18 @@ export const trackPageScrolling = (enabled, router) => {
|
|
|
79
218
|
100: [],
|
|
80
219
|
};
|
|
81
220
|
const scrollListener = () => {
|
|
221
|
+
const currentPath = normalizeTrackingUrl(router.route.path);
|
|
82
222
|
Object.keys(scrollTargets).forEach((target) => {
|
|
83
223
|
if (
|
|
84
|
-
getScrollPercent() >= parseInt(target) &&
|
|
85
|
-
!scrollTargets[target].includes(
|
|
224
|
+
getScrollPercent() >= parseInt(target, 10) &&
|
|
225
|
+
!scrollTargets[target].includes(currentPath)
|
|
86
226
|
) {
|
|
87
|
-
scrollTargets[target].push(
|
|
227
|
+
scrollTargets[target].push(currentPath);
|
|
88
228
|
trackEvent([
|
|
89
229
|
"Interaction",
|
|
90
230
|
"Scroll Depth",
|
|
91
|
-
|
|
92
|
-
|
|
231
|
+
currentPath,
|
|
232
|
+
parseInt(target, 10),
|
|
93
233
|
]);
|
|
94
234
|
}
|
|
95
235
|
});
|
|
@@ -102,19 +242,16 @@ export const trackPageScrolling = (enabled, router) => {
|
|
|
102
242
|
};
|
|
103
243
|
|
|
104
244
|
/**
|
|
105
|
-
* Track user navigation from one page to another
|
|
245
|
+
* Track user navigation from one page to another with normalized URLs.
|
|
106
246
|
*
|
|
107
|
-
* @param
|
|
247
|
+
* @param router VitePress router
|
|
108
248
|
*/
|
|
109
249
|
export const trackRouterNavigation = (router) => {
|
|
110
250
|
const trackNavigation = () => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
_paq.push(["setDocumentTitle", router.route.data.title]);
|
|
116
|
-
_paq.push(["trackPageView"]);
|
|
117
|
-
_paq.push(["enableLinkTracking"]);
|
|
251
|
+
trackPageView({
|
|
252
|
+
path: router.route.path,
|
|
253
|
+
title: router.route.data?.title,
|
|
254
|
+
});
|
|
118
255
|
};
|
|
119
256
|
watch(() => router.route.data.relativePath, trackNavigation, {
|
|
120
257
|
immediate: true,
|
|
@@ -146,6 +283,8 @@ export const getFlatList = (items) => {
|
|
|
146
283
|
* @returns {boolean}
|
|
147
284
|
*/
|
|
148
285
|
export const isActive = (item, path) => {
|
|
286
|
+
if (!item || !path) return false;
|
|
287
|
+
|
|
149
288
|
if (item.activeMatch) {
|
|
150
289
|
try {
|
|
151
290
|
return new RegExp(item.activeMatch).test(path);
|
|
@@ -155,20 +294,26 @@ export const isActive = (item, path) => {
|
|
|
155
294
|
}
|
|
156
295
|
}
|
|
157
296
|
|
|
158
|
-
if (item.items) {
|
|
159
|
-
return
|
|
297
|
+
if (item.items && item.items.some((sub) => isActive(sub, path))) {
|
|
298
|
+
return true;
|
|
160
299
|
}
|
|
161
300
|
|
|
162
301
|
if (item.link) {
|
|
163
|
-
const
|
|
164
|
-
|
|
302
|
+
const normalize = (p) =>
|
|
303
|
+
decodeURI(p)
|
|
304
|
+
.replace(/[?#].*$/, "")
|
|
305
|
+
.replace(/\.html$/, "")
|
|
306
|
+
.replace(/\/$/, "");
|
|
307
|
+
|
|
308
|
+
const linkPath = normalize(item.link);
|
|
309
|
+
const currentPath = normalize(path);
|
|
165
310
|
|
|
166
311
|
if (linkPath === currentPath) return true;
|
|
167
312
|
|
|
168
313
|
// Special case for home page to avoid matching everything
|
|
169
314
|
if (linkPath === "") return currentPath === "";
|
|
170
315
|
|
|
171
|
-
return currentPath.startsWith(linkPath);
|
|
316
|
+
return currentPath === linkPath || currentPath.startsWith(linkPath + "/");
|
|
172
317
|
}
|
|
173
318
|
|
|
174
319
|
return false;
|