@eox/pages-theme-eox 1.7.0 → 1.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,622 @@
1
+ <template>
2
+ <div class="image-gallery-container">
3
+ <!-- 1. Grid Layout (Legacy GalleryGrid) -->
4
+ <div v-if="layoutMode === 'grid'" class="grid-container" :class="colsClass">
5
+ <div v-for="(img, idx) in normalizedImages" :key="idx" class="grid-item">
6
+ <figure class="grid-figure">
7
+ <!-- Wrap in Link if present -->
8
+ <a v-if="img.link" :href="img.link" class="grid-link">
9
+ <img
10
+ :src="img.src"
11
+ :alt="img.alt || 'Gallery image'"
12
+ :style="{ maxWidth: img.width || '100%' }"
13
+ class="grid-img"
14
+ />
15
+ <figcaption
16
+ v-if="img.caption"
17
+ class="grid-caption"
18
+ v-html="img.caption"
19
+ ></figcaption>
20
+ </a>
21
+ <!-- No Link -->
22
+ <template v-else>
23
+ <img
24
+ :src="img.src"
25
+ :alt="img.alt || 'Gallery image'"
26
+ :style="{ maxWidth: img.width || '100%' }"
27
+ class="grid-img"
28
+ :class="{ clickable: lightbox }"
29
+ @click="lightbox ? openLightbox(idx) : null"
30
+ />
31
+ <figcaption
32
+ v-if="img.caption"
33
+ class="grid-caption"
34
+ v-html="img.caption"
35
+ ></figcaption>
36
+ </template>
37
+ </figure>
38
+ </div>
39
+ </div>
40
+
41
+ <!-- 2. Gallery Layout with Featured/Thumbnails (eoxcloudless style) -->
42
+ <div v-else class="gallery-layout-wrapper">
43
+ <!-- Main Featured Image -->
44
+ <div
45
+ v-if="normalizedImages.length > 0"
46
+ class="featured-wrapper clickable"
47
+ @click="openLightbox(0)"
48
+ >
49
+ <img
50
+ :src="normalizedImages[0].src"
51
+ :alt="normalizedImages[0].alt || 'Featured gallery image'"
52
+ class="featured-img"
53
+ />
54
+ </div>
55
+
56
+ <!-- Thumbnail Grid -->
57
+ <div
58
+ v-if="normalizedImages.length > 1"
59
+ class="grid-container cols-thumbnail mt-4"
60
+ >
61
+ <div
62
+ v-for="(img, idx) in normalizedImages.slice(1)"
63
+ :key="idx"
64
+ class="thumbnail-item clickable"
65
+ @click="openLightbox(idx + 1)"
66
+ >
67
+ <img
68
+ :src="img.src"
69
+ :alt="img.alt || 'Gallery thumbnail'"
70
+ class="thumbnail-img"
71
+ />
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Fullscreen Lightbox Modal -->
77
+ <Transition name="fade">
78
+ <div
79
+ v-if="lightboxIndex !== null"
80
+ class="lightbox-overlay"
81
+ @click.self="closeLightbox"
82
+ >
83
+ <!-- Header (Close + Counter) -->
84
+ <div class="lightbox-header">
85
+ <span class="lightbox-counter">
86
+ {{ lightboxIndex + 1 }} / {{ normalizedImages.length }}
87
+ </span>
88
+ <button
89
+ @click="closeLightbox"
90
+ class="lightbox-btn lightbox-close"
91
+ aria-label="Close Lightbox"
92
+ >
93
+ <svg
94
+ class="lightbox-icon"
95
+ viewBox="0 0 24 24"
96
+ fill="none"
97
+ stroke="currentColor"
98
+ stroke-width="2"
99
+ >
100
+ <path
101
+ stroke-linecap="round"
102
+ stroke-linejoin="round"
103
+ d="M6 18L18 6M6 6l12 12"
104
+ />
105
+ </svg>
106
+ </button>
107
+ </div>
108
+
109
+ <!-- Previous Button -->
110
+ <button
111
+ v-if="normalizedImages.length > 1"
112
+ @click="prevImage"
113
+ class="lightbox-btn lightbox-nav lightbox-prev"
114
+ aria-label="Previous Image"
115
+ >
116
+ <svg
117
+ class="lightbox-nav-icon"
118
+ viewBox="0 0 24 24"
119
+ fill="none"
120
+ stroke="currentColor"
121
+ stroke-width="2"
122
+ >
123
+ <path
124
+ stroke-linecap="round"
125
+ stroke-linejoin="round"
126
+ d="M15 19l-7-7 7-7"
127
+ />
128
+ </svg>
129
+ </button>
130
+
131
+ <!-- Next Button -->
132
+ <button
133
+ v-if="normalizedImages.length > 1"
134
+ @click="nextImage"
135
+ class="lightbox-btn lightbox-nav lightbox-next"
136
+ aria-label="Next Image"
137
+ >
138
+ <svg
139
+ class="lightbox-nav-icon"
140
+ viewBox="0 0 24 24"
141
+ fill="none"
142
+ stroke="currentColor"
143
+ stroke-width="2"
144
+ >
145
+ <path
146
+ stroke-linecap="round"
147
+ stroke-linejoin="round"
148
+ d="M9 5l7 7-7 7"
149
+ />
150
+ </svg>
151
+ </button>
152
+
153
+ <!-- Lightbox Content -->
154
+ <div class="lightbox-content" @click.self="closeLightbox">
155
+ <img
156
+ :src="normalizedImages[lightboxIndex].src"
157
+ class="lightbox-image"
158
+ :alt="normalizedImages[lightboxIndex].alt || 'Expanded view'"
159
+ />
160
+ <p
161
+ v-if="normalizedImages[lightboxIndex].caption"
162
+ class="lightbox-caption"
163
+ v-html="normalizedImages[lightboxIndex].caption"
164
+ ></p>
165
+ </div>
166
+ </div>
167
+ </Transition>
168
+ </div>
169
+ </template>
170
+
171
+ <script setup>
172
+ import { computed, ref, onMounted, onUnmounted } from "vue";
173
+
174
+ const props = defineProps({
175
+ images: {
176
+ type: [String, Array],
177
+ required: true,
178
+ },
179
+ columns: {
180
+ type: Number,
181
+ default: 2,
182
+ },
183
+ layout: {
184
+ type: String,
185
+ default: "gallery", // 'gallery' (lightbox) or 'grid' (GalleryGrid)
186
+ },
187
+ lightbox: {
188
+ type: Boolean,
189
+ default: true,
190
+ },
191
+ });
192
+
193
+ const lightboxIndex = ref(null);
194
+
195
+ const layoutMode = computed(() => {
196
+ // If explicitly 'grid', or if we detect link in image objects, default to grid
197
+ if (props.layout === "grid") return "grid";
198
+ if (
199
+ Array.isArray(props.images) &&
200
+ props.images.some(
201
+ (img) => typeof img === "object" && (img.link || img.caption),
202
+ )
203
+ ) {
204
+ return "grid";
205
+ }
206
+ return "gallery";
207
+ });
208
+
209
+ const normalizedImages = computed(() => {
210
+ if (!props.images) return [];
211
+ if (Array.isArray(props.images)) {
212
+ return props.images.map((img) => {
213
+ if (typeof img === "string") {
214
+ return { src: img, alt: "", caption: "", link: "", width: "" };
215
+ }
216
+ return {
217
+ src: img.src || "",
218
+ alt: img.alt || "",
219
+ caption: img.caption || "",
220
+ link: img.link || "",
221
+ width: img.width || "",
222
+ };
223
+ });
224
+ }
225
+ if (typeof props.images === "string") {
226
+ return props.images
227
+ .split(",")
228
+ .map((img) => img.trim())
229
+ .filter(Boolean)
230
+ .map((src) => ({ src, alt: "", caption: "", link: "", width: "" }));
231
+ }
232
+ return [];
233
+ });
234
+
235
+ const colsClass = computed(() => {
236
+ const cols = props.columns;
237
+ if (cols === 1) return "cols-1";
238
+ if (cols === 3) return "cols-3";
239
+ if (cols === 4) return "cols-4";
240
+ if (cols === 5) return "cols-5";
241
+ return "cols-2"; // default
242
+ });
243
+
244
+ const openLightbox = (index) => {
245
+ if (!props.lightbox) return;
246
+ lightboxIndex.value = index;
247
+ if (typeof document !== "undefined") {
248
+ document.body.style.overflow = "hidden";
249
+ }
250
+ };
251
+
252
+ const closeLightbox = () => {
253
+ lightboxIndex.value = null;
254
+ if (typeof document !== "undefined") {
255
+ document.body.style.overflow = "";
256
+ }
257
+ };
258
+
259
+ const prevImage = () => {
260
+ if (normalizedImages.value.length <= 1) return;
261
+ lightboxIndex.value =
262
+ (lightboxIndex.value - 1 + normalizedImages.value.length) %
263
+ normalizedImages.value.length;
264
+ };
265
+
266
+ const nextImage = () => {
267
+ if (normalizedImages.value.length <= 1) return;
268
+ lightboxIndex.value =
269
+ (lightboxIndex.value + 1) % normalizedImages.value.length;
270
+ };
271
+
272
+ const handleKeydown = (e) => {
273
+ if (lightboxIndex.value === null) return;
274
+ if (e.key === "ArrowRight" || e.key === "Right") {
275
+ nextImage();
276
+ } else if (e.key === "ArrowLeft" || e.key === "Left") {
277
+ prevImage();
278
+ } else if (e.key === "Escape" || e.key === "Esc") {
279
+ closeLightbox();
280
+ }
281
+ };
282
+
283
+ onMounted(() => {
284
+ window.addEventListener("keydown", handleKeydown);
285
+ });
286
+
287
+ onUnmounted(() => {
288
+ window.removeEventListener("keydown", handleKeydown);
289
+ if (typeof document !== "undefined") {
290
+ document.body.style.overflow = "";
291
+ }
292
+ });
293
+ </script>
294
+
295
+ <style scoped>
296
+ .image-gallery-container {
297
+ width: 100%;
298
+ margin: 1.5rem 0;
299
+ }
300
+
301
+ .grid-container {
302
+ display: grid;
303
+ gap: 1.5rem;
304
+ width: 100%;
305
+ }
306
+
307
+ .cols-1 {
308
+ grid-template-columns: repeat(1, minmax(0, 1fr));
309
+ }
310
+
311
+ .cols-2 {
312
+ grid-template-columns: repeat(1, minmax(0, 1fr));
313
+ }
314
+
315
+ .cols-3 {
316
+ grid-template-columns: repeat(1, minmax(0, 1fr));
317
+ }
318
+
319
+ .cols-4 {
320
+ grid-template-columns: repeat(2, minmax(0, 1fr));
321
+ }
322
+
323
+ .cols-5 {
324
+ grid-template-columns: repeat(2, minmax(0, 1fr));
325
+ }
326
+
327
+ @media (min-width: 640px) {
328
+ .cols-2 {
329
+ grid-template-columns: repeat(2, minmax(0, 1fr));
330
+ }
331
+ .cols-3 {
332
+ grid-template-columns: repeat(2, minmax(0, 1fr));
333
+ }
334
+ .cols-4 {
335
+ grid-template-columns: repeat(3, minmax(0, 1fr));
336
+ }
337
+ .cols-5 {
338
+ grid-template-columns: repeat(3, minmax(0, 1fr));
339
+ }
340
+ }
341
+
342
+ @media (min-width: 768px) {
343
+ .cols-3 {
344
+ grid-template-columns: repeat(3, minmax(0, 1fr));
345
+ }
346
+ .cols-4 {
347
+ grid-template-columns: repeat(4, minmax(0, 1fr));
348
+ }
349
+ .cols-5 {
350
+ grid-template-columns: repeat(5, minmax(0, 1fr));
351
+ }
352
+ }
353
+
354
+ .cols-thumbnail {
355
+ grid-template-columns: repeat(2, minmax(0, 1fr));
356
+ gap: 1rem;
357
+ }
358
+
359
+ @media (min-width: 640px) {
360
+ .cols-thumbnail {
361
+ grid-template-columns: repeat(3, minmax(0, 1fr));
362
+ }
363
+ }
364
+
365
+ @media (min-width: 1024px) {
366
+ .cols-thumbnail {
367
+ grid-template-columns: repeat(4, minmax(0, 1fr));
368
+ }
369
+ }
370
+
371
+ @media (min-width: 1280px) {
372
+ .cols-thumbnail {
373
+ grid-template-columns: repeat(5, minmax(0, 1fr));
374
+ }
375
+ }
376
+
377
+ .grid-item {
378
+ display: flex;
379
+ flex-direction: column;
380
+ align-items: center;
381
+ justify-content: center;
382
+ text-align: center;
383
+ margin: 0 auto;
384
+ width: 100%;
385
+ }
386
+
387
+ .grid-figure {
388
+ width: 100%;
389
+ display: flex;
390
+ flex-direction: column;
391
+ align-items: center;
392
+ justify-content: center;
393
+ margin: 0;
394
+ }
395
+
396
+ .grid-link {
397
+ text-decoration: none;
398
+ color: inherit;
399
+ display: block;
400
+ width: 100%;
401
+ }
402
+
403
+ .grid-link:hover {
404
+ opacity: 0.9;
405
+ }
406
+
407
+ .grid-img {
408
+ max-width: 100% !important;
409
+ box-sizing: border-box;
410
+ border-radius: 8px;
411
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
412
+ display: block;
413
+ height: auto;
414
+ margin: 0 auto;
415
+ }
416
+
417
+ .clickable {
418
+ cursor: pointer !important;
419
+ }
420
+
421
+ .grid-img.clickable:hover {
422
+ transform: scale(1.01);
423
+ transition: transform 0.2s ease-in-out;
424
+ }
425
+
426
+ .grid-caption {
427
+ font-size: 0.85rem;
428
+ margin-top: 0.5rem;
429
+ text-align: center;
430
+ color: #64748b;
431
+ font-weight: 500;
432
+ }
433
+
434
+ /* Gallery layout style */
435
+ .gallery-layout-wrapper {
436
+ display: flex;
437
+ flex-direction: column;
438
+ gap: 1rem;
439
+ width: 100%;
440
+ }
441
+
442
+ .featured-wrapper {
443
+ width: 100%;
444
+ overflow: hidden;
445
+ border-radius: 8px;
446
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
447
+ border: 1px solid #f1f5f9;
448
+ }
449
+
450
+ .featured-wrapper:hover {
451
+ opacity: 0.95;
452
+ transition: opacity 0.2s ease-in-out;
453
+ }
454
+
455
+ .featured-img {
456
+ width: 100%;
457
+ height: auto;
458
+ display: block;
459
+ object-fit: cover;
460
+ }
461
+
462
+ .thumbnail-item {
463
+ aspect-ratio: 16 / 9;
464
+ overflow: hidden;
465
+ border-radius: 8px;
466
+ border: 1px solid #f1f5f9;
467
+ background-color: #f8fafc;
468
+ display: flex;
469
+ align-items: center;
470
+ justify-content: center;
471
+ }
472
+
473
+ .thumbnail-item:hover {
474
+ transform: scale(1.02);
475
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
476
+ transition: all 0.2s ease-in-out;
477
+ }
478
+
479
+ .thumbnail-img {
480
+ width: 100%;
481
+ height: 100%;
482
+ object-fit: cover;
483
+ display: block;
484
+ }
485
+
486
+ /* Lightbox backdrop overlay */
487
+ .lightbox-overlay {
488
+ position: fixed;
489
+ top: 0;
490
+ left: 0;
491
+ width: 100%;
492
+ height: 100%;
493
+ background-color: rgba(0, 0, 0, 0.9);
494
+ z-index: 99999;
495
+ display: flex;
496
+ flex-direction: column;
497
+ align-items: center;
498
+ justify-content: center;
499
+ user-select: none;
500
+ }
501
+
502
+ .lightbox-header {
503
+ position: absolute;
504
+ top: 0;
505
+ left: 0;
506
+ right: 0;
507
+ height: 64px;
508
+ display: flex;
509
+ align-items: center;
510
+ justify-content: space-between;
511
+ padding: 0 1.5rem;
512
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), transparent);
513
+ z-index: 100001;
514
+ color: #ffffff;
515
+ font-family: sans-serif;
516
+ }
517
+
518
+ .lightbox-counter {
519
+ font-size: 0.875rem;
520
+ font-weight: 500;
521
+ letter-spacing: 0.05em;
522
+ }
523
+
524
+ .lightbox-btn {
525
+ background-color: rgba(0, 0, 0, 0.3);
526
+ border: none;
527
+ color: rgba(255, 255, 255, 0.8);
528
+ cursor: pointer !important;
529
+ display: flex;
530
+ align-items: center;
531
+ justify-content: center;
532
+ border-radius: 50%;
533
+ transition: all 0.2s ease-in-out;
534
+ }
535
+
536
+ .lightbox-btn:hover {
537
+ background-color: rgba(255, 255, 255, 0.2);
538
+ color: #ffffff;
539
+ }
540
+
541
+ .lightbox-close {
542
+ width: 40px;
543
+ height: 40px;
544
+ padding: 0;
545
+ }
546
+
547
+ .lightbox-icon {
548
+ width: 24px;
549
+ height: 24px;
550
+ }
551
+
552
+ .lightbox-nav {
553
+ position: absolute;
554
+ top: 50%;
555
+ transform: translateY(-50%);
556
+ width: 48px !important;
557
+ height: 48px !important;
558
+ min-width: 48px !important;
559
+ max-width: 48px !important;
560
+ min-height: 48px !important;
561
+ max-height: 48px !important;
562
+ flex-shrink: 0 !important;
563
+ box-sizing: border-box !important;
564
+ border-radius: 50% !important;
565
+ z-index: 100002;
566
+ }
567
+
568
+ .lightbox-prev {
569
+ left: 1.5rem;
570
+ }
571
+
572
+ .lightbox-next {
573
+ right: 1.5rem;
574
+ }
575
+
576
+ .lightbox-nav-icon {
577
+ width: 28px;
578
+ height: 28px;
579
+ }
580
+
581
+ .lightbox-content {
582
+ max-width: 90%;
583
+ max-height: 80%;
584
+ display: flex;
585
+ flex-direction: column;
586
+ align-items: center;
587
+ justify-content: center;
588
+ z-index: 100000;
589
+ }
590
+
591
+ .lightbox-image {
592
+ max-width: 100%;
593
+ max-height: 75vh;
594
+ object-fit: contain;
595
+ border-radius: 4px;
596
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
597
+ }
598
+
599
+ .lightbox-caption {
600
+ color: rgba(255, 255, 255, 0.9);
601
+ font-size: 0.875rem;
602
+ margin-top: 1rem;
603
+ text-align: center;
604
+ max-width: 36rem;
605
+ padding: 0 1rem;
606
+ }
607
+
608
+ /* Transition Animations */
609
+ .fade-enter-active,
610
+ .fade-leave-active {
611
+ transition: opacity 0.25s ease;
612
+ }
613
+
614
+ .fade-enter-from,
615
+ .fade-leave-to {
616
+ opacity: 0;
617
+ }
618
+
619
+ .mt-4 {
620
+ margin-top: 1rem;
621
+ }
622
+ </style>
@@ -97,7 +97,7 @@
97
97
  {{ t(plan.basePrice, theme.i18n) }}
98
98
  </h6>
99
99
  <h4 v-else class="primary-text bold" :style="dynamicPriceStyleH4">
100
- € {{ calculatePrice(plan) }},-
100
+ {{ formatPrice(calculatePrice(plan)) }}
101
101
  </h4>
102
102
  <span
103
103
  v-if="
@@ -236,7 +236,7 @@
236
236
  class="primary-text bold"
237
237
  :style="dynamicPriceStyleH6"
238
238
  >
239
- + € {{ plan.additionalPrice }},-
239
+ + {{ formatPrice(plan.additionalPrice) }}
240
240
  </h6>
241
241
  </div>
242
242
  <template
@@ -285,14 +285,14 @@
285
285
  </template>
286
286
 
287
287
  <script>
288
- import { t } from "../helpers.js";
288
+ import { t, formatPrice } from "../helpers.js";
289
289
  import { useData } from "vitepress";
290
290
 
291
291
  export default {
292
292
  name: "PricingTable",
293
293
  setup() {
294
- const { theme } = useData();
295
- return { theme, t };
294
+ const { theme, lang } = useData();
295
+ return { theme, lang, t };
296
296
  },
297
297
  props: {
298
298
  config: {
@@ -346,7 +346,7 @@ export default {
346
346
  if (typeof plan.basePrice === "string") {
347
347
  text = this.t(plan.basePrice, this.theme.i18n) || "";
348
348
  } else {
349
- text = `€ ${this.calculatePrice(plan)},-`;
349
+ text = this.formatPrice(this.calculatePrice(plan));
350
350
  }
351
351
  if (text.length > maxLen) maxLen = text.length;
352
352
  });
@@ -354,7 +354,7 @@ export default {
354
354
  if (detail.plans) {
355
355
  detail.plans.forEach((plan) => {
356
356
  if (plan.additionalPrice) {
357
- const text = `+ € ${plan.additionalPrice},-`;
357
+ const text = `+ ${this.formatPrice(plan.additionalPrice)}`;
358
358
  if (text.length > maxLen) maxLen = text.length;
359
359
  }
360
360
  });
@@ -433,6 +433,9 @@ export default {
433
433
  }
434
434
  return basePrice + additionalOptionPrice + additionalPlanModifier;
435
435
  },
436
+ formatPrice(price) {
437
+ return formatPrice(price, this.lang);
438
+ },
436
439
  getRowKeys(plansArray) {
437
440
  const keys = new Set();
438
441
  plansArray.forEach((plan) => {
@@ -0,0 +1,27 @@
1
+ import SingleTweet from "./SingleTweet.vue";
2
+
3
+ describe("<SingleTweet />", () => {
4
+ it("renders consent fallback if cookies are not allowed", () => {
5
+ cy.mount(SingleTweet, {
6
+ props: {
7
+ id: "12345",
8
+ mockConsent: false,
9
+ },
10
+ });
11
+
12
+ cy.contains("Cookie Consent Required").should("be.visible");
13
+ cy.get("blockquote").should("not.exist");
14
+ });
15
+
16
+ it("renders tweet blockquote if cookies are allowed", () => {
17
+ cy.mount(SingleTweet, {
18
+ props: {
19
+ id: "12345",
20
+ mockConsent: true,
21
+ },
22
+ });
23
+
24
+ cy.get("blockquote.twitter-tweet").should("exist");
25
+ cy.contains("Cookie Consent Required").should("not.exist");
26
+ });
27
+ });