@eox/pages-theme-eox 1.6.0 → 1.8.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/.gitlab-ci.yml +8 -0
- package/CHANGELOG.md +18 -0
- package/README.md +54 -0
- package/cypress/support/mocks/helpers.js +46 -0
- package/package.json +4 -1
- package/src/components/FeatureCard/Index.vue +6 -3
- package/src/components/FeatureCard/Poster.vue +4 -3
- package/src/components/FeaturesGallery.vue +5 -5
- package/src/components/NavBar.vue +4 -4
- package/src/components/PricingTable.vue +91 -10
- package/src/helpers.js +52 -0
- package/src/style.css +9 -0
package/.gitlab-ci.yml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
image: cypress/base:20.14.0
|
|
2
2
|
|
|
3
3
|
stages:
|
|
4
|
+
- security
|
|
4
5
|
- test
|
|
5
6
|
|
|
6
7
|
cache:
|
|
@@ -13,6 +14,13 @@ cache:
|
|
|
13
14
|
variables:
|
|
14
15
|
npm_config_cache: "$CI_PROJECT_DIR/.npm"
|
|
15
16
|
|
|
17
|
+
security:
|
|
18
|
+
stage: security
|
|
19
|
+
image: node:24-alpine
|
|
20
|
+
script:
|
|
21
|
+
- npm ci
|
|
22
|
+
- npm audit --audit-level=critical
|
|
23
|
+
|
|
16
24
|
format:
|
|
17
25
|
stage: test
|
|
18
26
|
script:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [1.8.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.7.0...v1.8.0) (2026-06-08)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- **style:** z-index for sidebar/footer ([6326866](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/632686679cd4db43a50e8e97b4557eb072409fd1))
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- price formatting helper ([287a28a](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/287a28abe3c8611d36685bae2efb75973e94d6c1))
|
|
12
|
+
|
|
13
|
+
# [1.7.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.6.0...v1.7.0) (2026-05-11)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- **style:** card link/border/elevation ([004611f](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/004611f20e46dd47030a6c835bd6380fd045369b))
|
|
18
|
+
- **style:** nav menu rounding harmonization ([a5ff2e7](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/a5ff2e7a3b63e98b44f7b763ad79c2111c748286))
|
|
19
|
+
- **style:** sizing adjustments for prices and plan names ([d61ef7a](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/d61ef7a37ae0a3cede844875b582965eb820e704))
|
|
20
|
+
|
|
3
21
|
# [1.6.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.5.1...v1.6.0) (2026-04-30)
|
|
4
22
|
|
|
5
23
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -483,3 +483,57 @@ feedback form can be disabled on individual pages via frontmatter configuration
|
|
|
483
483
|
feedback: false
|
|
484
484
|
---
|
|
485
485
|
```
|
|
486
|
+
|
|
487
|
+
## Helper Functions
|
|
488
|
+
|
|
489
|
+
The theme exports a set of shared utility and helper functions from its helpers module. You can import and use these helpers in your custom VitePress components, checkout pages, or inline script setups:
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
import { formatPrice, trackEvent, t } from "@eox/pages-theme-eox/src/helpers";
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Available Helpers
|
|
496
|
+
|
|
497
|
+
#### `formatPrice(price, lang)`
|
|
498
|
+
|
|
499
|
+
Formats a numeric or string-based price following either British/American style or Continental European style based on the active language code.
|
|
500
|
+
|
|
501
|
+
- **Parameters**:
|
|
502
|
+
- `price` (`number|string`): The numeric price (or numeric string) to format.
|
|
503
|
+
- `lang` (`string`): The active language prefix (defaults to `'en'`).
|
|
504
|
+
- **Returns**: `string` - Formatted price string.
|
|
505
|
+
- **Behavior**:
|
|
506
|
+
- **English (`'en'`)**: Uses commas as thousands separators, dots as decimals, and adds a `.-` suffix for whole numbers (e.g., `1500` formatted to `€ 1,500.-`, `1500.50` formatted to `€ 1,500.50`).
|
|
507
|
+
- **Other languages (e.g., German/Continental)**: Uses dots as thousands separators, commas as decimals, and adds a `,-` suffix for whole numbers (e.g., `1500` formatted to `€ 1.500,-`, `1500.5` formatted to `€ 1.500,50`).
|
|
508
|
+
|
|
509
|
+
##### Example Usage:
|
|
510
|
+
|
|
511
|
+
```javascript
|
|
512
|
+
import { formatPrice } from "@eox/pages-theme-eox/src/helpers";
|
|
513
|
+
|
|
514
|
+
formatPrice(12000, "en"); // "€ 12,000.-"
|
|
515
|
+
formatPrice(49.99, "de"); // "€ 49,99"
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
#### `trackEvent(eventDetails)`
|
|
519
|
+
|
|
520
|
+
Sends a custom tracking event to Matomo Analytics.
|
|
521
|
+
|
|
522
|
+
- **Parameters**:
|
|
523
|
+
- `eventDetails` (`Array<string>`): An array representing the event payload: `[Category, Action, Name?, Value?]`.
|
|
524
|
+
|
|
525
|
+
##### Example Usage:
|
|
526
|
+
|
|
527
|
+
```javascript
|
|
528
|
+
import { trackEvent } from "@eox/pages-theme-eox/src/helpers";
|
|
529
|
+
|
|
530
|
+
trackEvent(["CTA", "Click", "Technical Specifications", "Viewing Ready"]);
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
#### `t(key, i18n)`
|
|
534
|
+
|
|
535
|
+
Retrieves localized translation strings from an i18n mapping object using dot-notation. Falls back to the key itself if no translation is defined.
|
|
536
|
+
|
|
537
|
+
- **Parameters**:
|
|
538
|
+
- `key` (`string`): The lookup key (supports dot notation, e.g., `'legal.privacy'`).
|
|
539
|
+
- `i18n` (`object`): The translation object/table.
|
|
@@ -47,6 +47,50 @@ window.__helpersMock = {
|
|
|
47
47
|
}
|
|
48
48
|
return result ? result : key;
|
|
49
49
|
},
|
|
50
|
+
formatPrice: (price, lang = "en") => {
|
|
51
|
+
if (typeof price === "string") {
|
|
52
|
+
const trimmed = price.trim();
|
|
53
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
54
|
+
price = Number(trimmed);
|
|
55
|
+
} else {
|
|
56
|
+
return price;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (typeof price === "number" && !isNaN(price)) {
|
|
60
|
+
const isEnglish = !lang || lang.startsWith("en");
|
|
61
|
+
const hasCents = price % 1 !== 0;
|
|
62
|
+
if (isEnglish) {
|
|
63
|
+
if (hasCents) {
|
|
64
|
+
const formatted = new Intl.NumberFormat("en-US", {
|
|
65
|
+
minimumFractionDigits: 2,
|
|
66
|
+
maximumFractionDigits: 2,
|
|
67
|
+
}).format(price);
|
|
68
|
+
return `€ ${formatted}`;
|
|
69
|
+
} else {
|
|
70
|
+
const formatted = new Intl.NumberFormat("en-US", {
|
|
71
|
+
minimumFractionDigits: 0,
|
|
72
|
+
maximumFractionDigits: 0,
|
|
73
|
+
}).format(price);
|
|
74
|
+
return `€ ${formatted}.-`;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
if (hasCents) {
|
|
78
|
+
const formatted = new Intl.NumberFormat("de-DE", {
|
|
79
|
+
minimumFractionDigits: 2,
|
|
80
|
+
maximumFractionDigits: 2,
|
|
81
|
+
}).format(price);
|
|
82
|
+
return `€ ${formatted}`;
|
|
83
|
+
} else {
|
|
84
|
+
const formatted = new Intl.NumberFormat("de-DE", {
|
|
85
|
+
minimumFractionDigits: 0,
|
|
86
|
+
maximumFractionDigits: 0,
|
|
87
|
+
}).format(price);
|
|
88
|
+
return `€ ${formatted},-`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return price;
|
|
93
|
+
},
|
|
50
94
|
};
|
|
51
95
|
|
|
52
96
|
export const trackEvent = (...args) => window.__helpersMock.trackEvent(...args);
|
|
@@ -61,3 +105,5 @@ export const isActive = (...args) => window.__helpersMock.isActive(...args);
|
|
|
61
105
|
export const getFlatList = (...args) =>
|
|
62
106
|
window.__helpersMock.getFlatList(...args);
|
|
63
107
|
export const t = (...args) => window.__helpersMock.t(...args);
|
|
108
|
+
export const formatPrice = (...args) =>
|
|
109
|
+
window.__helpersMock.formatPrice(...args);
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eox/pages-theme-eox",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vitepress Theme with EOX branding",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"author": "EOX",
|
|
8
8
|
"license": "MIT",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=24.0.0"
|
|
11
|
+
},
|
|
9
12
|
"scripts": {
|
|
10
13
|
"format": "npx prettier --write .",
|
|
11
14
|
"format:check": "npx prettier --check .",
|
|
@@ -40,7 +40,10 @@ const iconStyle = {
|
|
|
40
40
|
};
|
|
41
41
|
</script>
|
|
42
42
|
<template>
|
|
43
|
-
<article
|
|
43
|
+
<article
|
|
44
|
+
class="vertical large-padding no-margin"
|
|
45
|
+
:class="link && !link.text ? 'small-elevate' : 'no-elevate'"
|
|
46
|
+
>
|
|
44
47
|
<div>
|
|
45
48
|
<i
|
|
46
49
|
v-if="typeof icon === 'string' && icon.startsWith('mdi-')"
|
|
@@ -92,10 +95,10 @@ const iconStyle = {
|
|
|
92
95
|
<nav
|
|
93
96
|
v-if="link || secondaryLink"
|
|
94
97
|
class="row wrap align-center"
|
|
95
|
-
style="gap: 1.5rem; margin-right: auto"
|
|
98
|
+
style="gap: 1.5rem; margin-right: auto; position: relative; z-index: 11"
|
|
96
99
|
>
|
|
97
100
|
<a
|
|
98
|
-
v-if="link"
|
|
101
|
+
v-if="link && !!link.text"
|
|
99
102
|
:href="link.href"
|
|
100
103
|
:target="link.target ?? '_blank'"
|
|
101
104
|
class="button transparent bold primary-text no-padding"
|
|
@@ -48,7 +48,8 @@ const cardStyle = computed(() => ({
|
|
|
48
48
|
|
|
49
49
|
<template>
|
|
50
50
|
<article
|
|
51
|
-
class="no-padding fill
|
|
51
|
+
class="no-padding no-margin fill image-overlay-card"
|
|
52
|
+
:class="link && !link.text ? 'small-elevate' : 'no-elevate'"
|
|
52
53
|
:style="cardStyle"
|
|
53
54
|
>
|
|
54
55
|
<img v-if="image" :src="image" alt="" class="image-overlay-card-image" />
|
|
@@ -97,10 +98,10 @@ const cardStyle = computed(() => ({
|
|
|
97
98
|
<nav
|
|
98
99
|
v-if="link || secondaryLink"
|
|
99
100
|
class="row wrap align-center"
|
|
100
|
-
style="gap: 0.5rem"
|
|
101
|
+
style="gap: 0.5rem; position: relative; z-index: 11"
|
|
101
102
|
>
|
|
102
103
|
<a
|
|
103
|
-
v-if="link"
|
|
104
|
+
v-if="link && link.text"
|
|
104
105
|
class="button small white black-text"
|
|
105
106
|
:href="link.href"
|
|
106
107
|
:target="link.target ?? '_blank'"
|
|
@@ -179,19 +179,19 @@ section {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
.link-card {
|
|
182
|
-
display:
|
|
182
|
+
display: flex;
|
|
183
|
+
flex: 1;
|
|
183
184
|
text-decoration: none;
|
|
184
|
-
border: 0.0625rem solid transparent;
|
|
185
185
|
position: relative;
|
|
186
186
|
}
|
|
187
187
|
.link-overlay {
|
|
188
188
|
position: absolute;
|
|
189
189
|
inset: 0;
|
|
190
|
-
z-index:
|
|
190
|
+
z-index: 10;
|
|
191
191
|
}
|
|
192
192
|
.link-card:hover article {
|
|
193
|
-
box-shadow:
|
|
194
|
-
|
|
193
|
+
box-shadow: var(--elevate2);
|
|
194
|
+
transform: translateY(-2px);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
.card-wrapper {
|
|
@@ -138,19 +138,19 @@
|
|
|
138
138
|
<li v-for="item in theme.nav.filter((i) => !i.action)">
|
|
139
139
|
<div v-if="item.items">
|
|
140
140
|
<button
|
|
141
|
-
class="button text"
|
|
141
|
+
class="button text no-round"
|
|
142
142
|
:class="{ active: isActive(item, route.path) }"
|
|
143
143
|
>
|
|
144
144
|
<span>{{ t(item.text, theme.i18n) }}</span>
|
|
145
145
|
<i class="mdi mdi-chevron-down"></i>
|
|
146
146
|
</button>
|
|
147
|
-
<menu class="no-wrap surface-container-lowest">
|
|
147
|
+
<menu class="no-wrap surface-container-lowest no-round">
|
|
148
148
|
<NavDropdown :items="item.items" />
|
|
149
149
|
</menu>
|
|
150
150
|
</div>
|
|
151
151
|
<a
|
|
152
152
|
v-else
|
|
153
|
-
class="button text"
|
|
153
|
+
class="button text no-round"
|
|
154
154
|
:class="{ active: isActive(item, route.path) }"
|
|
155
155
|
:href="withBase(item.link)"
|
|
156
156
|
:target="item.target"
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
<ul class="left-align no-margin" v-if="langs && langs.length > 1">
|
|
200
200
|
<li>
|
|
201
201
|
<div>
|
|
202
|
-
<button class="button text">
|
|
202
|
+
<button class="button text no-round">
|
|
203
203
|
<i class="mdi mdi-translate small"></i>
|
|
204
204
|
<i class="mdi mdi-chevron-down small"></i>
|
|
205
205
|
</button>
|
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
:key="`header-${index}`"
|
|
43
43
|
class="s12 m3 center-align"
|
|
44
44
|
>
|
|
45
|
-
<h6 class="primary-text bold"
|
|
45
|
+
<h6 class="primary-text bold" style="word-break: break-word">
|
|
46
|
+
{{ plan.name }}
|
|
47
|
+
</h6>
|
|
46
48
|
<ClientOnly>
|
|
47
49
|
<a
|
|
48
50
|
:href="addPlanConfig(contactLink, plan)"
|
|
@@ -72,7 +74,9 @@
|
|
|
72
74
|
class="cell surface-container-low top-cell bold top-margin"
|
|
73
75
|
:class="`orig-col-${index + 2}`"
|
|
74
76
|
>
|
|
75
|
-
<h6 class="primary-text bold top-margin"
|
|
77
|
+
<h6 class="primary-text bold top-margin" style="word-break: break-word">
|
|
78
|
+
{{ plan.name }}
|
|
79
|
+
</h6>
|
|
76
80
|
</div>
|
|
77
81
|
|
|
78
82
|
<div v-if="showDescriptionColumn" class="cell orig-col-1 l">
|
|
@@ -88,11 +92,12 @@
|
|
|
88
92
|
<h6
|
|
89
93
|
v-if="typeof plan.basePrice === 'string'"
|
|
90
94
|
class="primary-text bold"
|
|
95
|
+
:style="dynamicPriceStyleH6"
|
|
91
96
|
>
|
|
92
97
|
{{ t(plan.basePrice, theme.i18n) }}
|
|
93
98
|
</h6>
|
|
94
|
-
<h4 v-else class="primary-text bold">
|
|
95
|
-
|
|
99
|
+
<h4 v-else class="primary-text bold" :style="dynamicPriceStyleH4">
|
|
100
|
+
{{ formatPrice(calculatePrice(plan)) }}
|
|
96
101
|
</h4>
|
|
97
102
|
<span
|
|
98
103
|
v-if="
|
|
@@ -223,9 +228,15 @@
|
|
|
223
228
|
class="surface-container-low primary-text bold top-margin top-cell"
|
|
224
229
|
:class="`cell orig-col-${detailPlanIndex + 2}`"
|
|
225
230
|
>
|
|
226
|
-
<h6 class="primary-text bold s"
|
|
227
|
-
|
|
228
|
-
|
|
231
|
+
<h6 class="primary-text bold s" style="word-break: break-word">
|
|
232
|
+
{{ plan.name }}
|
|
233
|
+
</h6>
|
|
234
|
+
<h6
|
|
235
|
+
v-if="plan.additionalPrice"
|
|
236
|
+
class="primary-text bold"
|
|
237
|
+
:style="dynamicPriceStyleH6"
|
|
238
|
+
>
|
|
239
|
+
+ {{ formatPrice(plan.additionalPrice) }}
|
|
229
240
|
</h6>
|
|
230
241
|
</div>
|
|
231
242
|
<template
|
|
@@ -274,14 +285,14 @@
|
|
|
274
285
|
</template>
|
|
275
286
|
|
|
276
287
|
<script>
|
|
277
|
-
import { t } from "../helpers.js";
|
|
288
|
+
import { t, formatPrice } from "../helpers.js";
|
|
278
289
|
import { useData } from "vitepress";
|
|
279
290
|
|
|
280
291
|
export default {
|
|
281
292
|
name: "PricingTable",
|
|
282
293
|
setup() {
|
|
283
|
-
const { theme } = useData();
|
|
284
|
-
return { theme, t };
|
|
294
|
+
const { theme, lang } = useData();
|
|
295
|
+
return { theme, lang, t };
|
|
285
296
|
},
|
|
286
297
|
props: {
|
|
287
298
|
config: {
|
|
@@ -328,6 +339,73 @@ export default {
|
|
|
328
339
|
};
|
|
329
340
|
}
|
|
330
341
|
},
|
|
342
|
+
maxPriceLength() {
|
|
343
|
+
let maxLen = 0;
|
|
344
|
+
this.localPlans.forEach((plan) => {
|
|
345
|
+
let text = "";
|
|
346
|
+
if (typeof plan.basePrice === "string") {
|
|
347
|
+
text = this.t(plan.basePrice, this.theme.i18n) || "";
|
|
348
|
+
} else {
|
|
349
|
+
text = this.formatPrice(this.calculatePrice(plan));
|
|
350
|
+
}
|
|
351
|
+
if (text.length > maxLen) maxLen = text.length;
|
|
352
|
+
});
|
|
353
|
+
this.localDetails.forEach((detail) => {
|
|
354
|
+
if (detail.plans) {
|
|
355
|
+
detail.plans.forEach((plan) => {
|
|
356
|
+
if (plan.additionalPrice) {
|
|
357
|
+
const text = `+ ${this.formatPrice(plan.additionalPrice)}`;
|
|
358
|
+
if (text.length > maxLen) maxLen = text.length;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return maxLen || 1;
|
|
364
|
+
},
|
|
365
|
+
dynamicPriceStyleH4() {
|
|
366
|
+
let fontSize = "";
|
|
367
|
+
if (!this.isMobile) {
|
|
368
|
+
// "€ 18000,-" is 11 chars. We want container to define this perfectly without pure JS guessing.
|
|
369
|
+
// Wait, pure CSS container queries / fluid typography is better here.
|
|
370
|
+
// Instead of calculating ratios manually, we can utilize container width scaling or vw based scaling
|
|
371
|
+
// if it exceeds length. Let's do a more robust JS calc.
|
|
372
|
+
|
|
373
|
+
const cols =
|
|
374
|
+
this.localPlans.length + (this.showDescriptionColumn ? 1 : 0);
|
|
375
|
+
|
|
376
|
+
// Base max chars before scaling based on how many cols are squished together
|
|
377
|
+
// 2 cols: allows ~16 chars
|
|
378
|
+
// 3 cols: allows ~12 chars
|
|
379
|
+
// 4 cols: allows ~9 chars
|
|
380
|
+
// 5 cols: allows ~7 chars
|
|
381
|
+
const maxCharsAtDefault = 19 - cols * 2.5;
|
|
382
|
+
|
|
383
|
+
if (this.maxPriceLength > maxCharsAtDefault) {
|
|
384
|
+
const ratio = maxCharsAtDefault / this.maxPriceLength;
|
|
385
|
+
fontSize = `${Math.max(ratio * 2.125, 0.8)}rem`; // Scale down, min ~12px equivalent
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
whiteSpace: "nowrap",
|
|
390
|
+
...(fontSize ? { fontSize } : {}),
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
dynamicPriceStyleH6() {
|
|
394
|
+
let fontSize = "";
|
|
395
|
+
if (!this.isMobile) {
|
|
396
|
+
const cols =
|
|
397
|
+
this.localPlans.length + (this.showDescriptionColumn ? 1 : 0);
|
|
398
|
+
const maxCharsAtDefault = 24 - cols * 3;
|
|
399
|
+
if (this.maxPriceLength > maxCharsAtDefault) {
|
|
400
|
+
const ratio = maxCharsAtDefault / this.maxPriceLength;
|
|
401
|
+
fontSize = `${Math.max(ratio * 1.25, 0.6)}rem`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
whiteSpace: "nowrap",
|
|
406
|
+
...(fontSize ? { fontSize } : {}),
|
|
407
|
+
};
|
|
408
|
+
},
|
|
331
409
|
},
|
|
332
410
|
methods: {
|
|
333
411
|
calculatePrice(plan) {
|
|
@@ -355,6 +433,9 @@ export default {
|
|
|
355
433
|
}
|
|
356
434
|
return basePrice + additionalOptionPrice + additionalPlanModifier;
|
|
357
435
|
},
|
|
436
|
+
formatPrice(price) {
|
|
437
|
+
return formatPrice(price, this.lang);
|
|
438
|
+
},
|
|
358
439
|
getRowKeys(plansArray) {
|
|
359
440
|
const keys = new Set();
|
|
360
441
|
plansArray.forEach((plan) => {
|
package/src/helpers.js
CHANGED
|
@@ -198,3 +198,55 @@ export const t = (key, i18n) => {
|
|
|
198
198
|
}
|
|
199
199
|
return typeof obj === "string" ? obj : key;
|
|
200
200
|
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Format price according to localized English (American/British) or European (German) styles.
|
|
204
|
+
*
|
|
205
|
+
* @param {number|string} price - Price to format
|
|
206
|
+
* @param {string} lang - Language code
|
|
207
|
+
* @returns {string} Formatted price with correct separators and suffixes
|
|
208
|
+
*/
|
|
209
|
+
export const formatPrice = (price, lang = "en") => {
|
|
210
|
+
if (typeof price === "string") {
|
|
211
|
+
const trimmed = price.trim();
|
|
212
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
213
|
+
price = Number(trimmed);
|
|
214
|
+
} else {
|
|
215
|
+
return price;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (typeof price === "number" && !isNaN(price)) {
|
|
219
|
+
const isEnglish = !lang || lang.startsWith("en");
|
|
220
|
+
const hasCents = price % 1 !== 0;
|
|
221
|
+
if (isEnglish) {
|
|
222
|
+
if (hasCents) {
|
|
223
|
+
const formatted = new Intl.NumberFormat("en-US", {
|
|
224
|
+
minimumFractionDigits: 2,
|
|
225
|
+
maximumFractionDigits: 2,
|
|
226
|
+
}).format(price);
|
|
227
|
+
return `€ ${formatted}`;
|
|
228
|
+
} else {
|
|
229
|
+
const formatted = new Intl.NumberFormat("en-US", {
|
|
230
|
+
minimumFractionDigits: 0,
|
|
231
|
+
maximumFractionDigits: 0,
|
|
232
|
+
}).format(price);
|
|
233
|
+
return `€ ${formatted}.-`;
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
if (hasCents) {
|
|
237
|
+
const formatted = new Intl.NumberFormat("de-DE", {
|
|
238
|
+
minimumFractionDigits: 2,
|
|
239
|
+
maximumFractionDigits: 2,
|
|
240
|
+
}).format(price);
|
|
241
|
+
return `€ ${formatted}`;
|
|
242
|
+
} else {
|
|
243
|
+
const formatted = new Intl.NumberFormat("de-DE", {
|
|
244
|
+
minimumFractionDigits: 0,
|
|
245
|
+
maximumFractionDigits: 0,
|
|
246
|
+
}).format(price);
|
|
247
|
+
return `€ ${formatted},-`;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return price;
|
|
252
|
+
};
|
package/src/style.css
CHANGED
|
@@ -336,3 +336,12 @@ details[open] > summary > i.mdi-chevron-down {
|
|
|
336
336
|
eox-feedback-button {
|
|
337
337
|
z-index: calc(var(--vp-z-index-footer) + 1);
|
|
338
338
|
}
|
|
339
|
+
|
|
340
|
+
@media (min-width: 960px) {
|
|
341
|
+
footer {
|
|
342
|
+
z-index: calc(var(--vp-z-index-sidebar) + 1);
|
|
343
|
+
}
|
|
344
|
+
eox-feedback-button {
|
|
345
|
+
z-index: calc(var(--vp-z-index-sidebar) + 2);
|
|
346
|
+
}
|
|
347
|
+
}
|