@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.
- package/.gitlab-ci.yml +8 -0
- package/CHANGELOG.md +16 -0
- package/README.md +124 -0
- package/cypress/support/mocks/helpers.js +46 -0
- package/package.json +4 -1
- package/src/components/ImageCarousel.cy.js +22 -0
- package/src/components/ImageCarousel.vue +268 -0
- package/src/components/ImageElement.cy.js +46 -0
- package/src/components/ImageElement.vue +269 -0
- package/src/components/ImageGallery.cy.js +52 -0
- package/src/components/ImageGallery.vue +622 -0
- package/src/components/PricingTable.vue +10 -7
- package/src/components/SingleTweet.cy.js +27 -0
- package/src/components/SingleTweet.vue +155 -0
- package/src/helpers.js +52 -0
- package/src/index.js +9 -0
- package/src/style.css +9 -0
- package/src/team.data.js +51 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="single-tweet-container">
|
|
3
|
+
<div class="tweet-card">
|
|
4
|
+
<!-- Embed Mode (Cookies accepted or SSR) -->
|
|
5
|
+
<div v-if="cookiesAllowed" class="tweet-embed-wrapper">
|
|
6
|
+
<blockquote class="twitter-tweet" data-theme="light">
|
|
7
|
+
<a :href="`https://twitter.com/x/status/${id}`"></a>
|
|
8
|
+
</blockquote>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- No Consent Fallback -->
|
|
12
|
+
<div v-else class="fallback-card">
|
|
13
|
+
<svg class="fallback-icon" viewBox="0 0 24 24" fill="currentColor">
|
|
14
|
+
<path
|
|
15
|
+
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
|
16
|
+
/>
|
|
17
|
+
</svg>
|
|
18
|
+
<div class="fallback-text">
|
|
19
|
+
<p class="fallback-title">Cookie Consent Required</p>
|
|
20
|
+
<p class="fallback-desc">
|
|
21
|
+
Please accept optional cookies to load the interactive Tweet. You
|
|
22
|
+
can review your preferences on the
|
|
23
|
+
<a href="/cookie-settings" class="cookie-link">Cookie Settings</a>
|
|
24
|
+
page.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup>
|
|
33
|
+
import { ref, onMounted, watch } from "vue";
|
|
34
|
+
|
|
35
|
+
const props = defineProps({
|
|
36
|
+
id: {
|
|
37
|
+
type: String,
|
|
38
|
+
required: true,
|
|
39
|
+
},
|
|
40
|
+
mockConsent: {
|
|
41
|
+
type: Boolean,
|
|
42
|
+
default: null,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const cookiesAllowed = ref(false);
|
|
47
|
+
|
|
48
|
+
const checkConsent = () => {
|
|
49
|
+
if (props.mockConsent !== null) {
|
|
50
|
+
cookiesAllowed.value = props.mockConsent;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (typeof document !== "undefined") {
|
|
54
|
+
// If no mtm cookie banner is present or user has consented
|
|
55
|
+
cookiesAllowed.value =
|
|
56
|
+
document.cookie.includes("mtm_cookie_consent") ||
|
|
57
|
+
!document.querySelector(".cookie-banner");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
onMounted(() => {
|
|
62
|
+
checkConsent();
|
|
63
|
+
|
|
64
|
+
// Dynamically load Twitter widgets script if allowed
|
|
65
|
+
watch(
|
|
66
|
+
cookiesAllowed,
|
|
67
|
+
(allowed) => {
|
|
68
|
+
if (allowed && typeof window !== "undefined") {
|
|
69
|
+
if (window.twttr) {
|
|
70
|
+
window.twttr.widgets.load();
|
|
71
|
+
} else {
|
|
72
|
+
const script = document.createElement("script");
|
|
73
|
+
script.src = "https://platform.twitter.com/widgets.js";
|
|
74
|
+
script.async = true;
|
|
75
|
+
script.charset = "utf-8";
|
|
76
|
+
document.head.appendChild(script);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{ immediate: true },
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<style scoped>
|
|
86
|
+
.single-tweet-container {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
margin: 1.5rem 0;
|
|
91
|
+
width: 100%;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.tweet-card {
|
|
95
|
+
width: 100%;
|
|
96
|
+
max-width: 500px;
|
|
97
|
+
min-height: 150px;
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.tweet-embed-wrapper {
|
|
104
|
+
width: 100%;
|
|
105
|
+
display: flex;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.fallback-card {
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
padding: 2rem;
|
|
115
|
+
background-color: #f1f5f9;
|
|
116
|
+
border: 1px solid #e2e8f0;
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
text-align: center;
|
|
119
|
+
color: #475569;
|
|
120
|
+
gap: 1rem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.fallback-icon {
|
|
124
|
+
width: 40px;
|
|
125
|
+
height: 40px;
|
|
126
|
+
color: #94a3b8;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.fallback-text {
|
|
130
|
+
font-size: 0.875rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.fallback-title {
|
|
134
|
+
font-weight: 600;
|
|
135
|
+
color: #1e293b;
|
|
136
|
+
margin: 0 0 0.25rem 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.fallback-desc {
|
|
140
|
+
color: #64748b;
|
|
141
|
+
font-size: 0.75rem;
|
|
142
|
+
line-height: 1.5;
|
|
143
|
+
margin: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.cookie-link {
|
|
147
|
+
color: var(--primary, #004170);
|
|
148
|
+
font-weight: 500;
|
|
149
|
+
text-decoration: none;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.cookie-link:hover {
|
|
153
|
+
text-decoration: underline;
|
|
154
|
+
}
|
|
155
|
+
</style>
|
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/index.js
CHANGED
|
@@ -13,6 +13,10 @@ import CookieSettings from "./components/CookieSettings.vue";
|
|
|
13
13
|
import NotFound from "./components/NotFound.vue";
|
|
14
14
|
import DataTable from "./components/DataTable.vue";
|
|
15
15
|
import Tutorial from "./components/Tutorial.vue";
|
|
16
|
+
import ImageElement from "./components/ImageElement.vue";
|
|
17
|
+
import ImageGallery from "./components/ImageGallery.vue";
|
|
18
|
+
import ImageCarousel from "./components/ImageCarousel.vue";
|
|
19
|
+
import SingleTweet from "./components/SingleTweet.vue";
|
|
16
20
|
import Layout from "./Layout.vue";
|
|
17
21
|
import "./style.css";
|
|
18
22
|
|
|
@@ -34,6 +38,11 @@ export default {
|
|
|
34
38
|
app.component("NotFound", NotFound);
|
|
35
39
|
app.component("DataTable", DataTable);
|
|
36
40
|
app.component("Tutorial", Tutorial);
|
|
41
|
+
app.component("ImageElement", ImageElement);
|
|
42
|
+
app.component("ImageGallery", ImageGallery);
|
|
43
|
+
app.component("GalleryGrid", ImageGallery);
|
|
44
|
+
app.component("ImageCarousel", ImageCarousel);
|
|
45
|
+
app.component("SingleTweet", SingleTweet);
|
|
37
46
|
|
|
38
47
|
router.onAfterRouteChanged = () => {
|
|
39
48
|
if (!import.meta.env.SSR) {
|
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
|
+
}
|
package/src/team.data.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createContentLoader } from "vitepress";
|
|
2
|
+
|
|
3
|
+
export default createContentLoader("team/*.md", {
|
|
4
|
+
transform(rawData) {
|
|
5
|
+
return rawData
|
|
6
|
+
.filter((page) => {
|
|
7
|
+
// Exclude guest members
|
|
8
|
+
return !(page.frontmatter.guest === true);
|
|
9
|
+
})
|
|
10
|
+
.map((page) => {
|
|
11
|
+
// Find job description inside the meta array
|
|
12
|
+
const metaList = page.frontmatter.meta || [];
|
|
13
|
+
const jobDescMeta = Array.isArray(metaList)
|
|
14
|
+
? metaList.find((m) => m && m.name === "jobDescription")
|
|
15
|
+
: null;
|
|
16
|
+
const role = jobDescMeta ? jobDescMeta.content : "";
|
|
17
|
+
|
|
18
|
+
// Transform sns array of objects into a single object of key-value pairs
|
|
19
|
+
const sns = {};
|
|
20
|
+
const snsList = page.frontmatter.sns || [];
|
|
21
|
+
if (Array.isArray(snsList)) {
|
|
22
|
+
snsList.forEach((item) => {
|
|
23
|
+
if (item && typeof item === "object") {
|
|
24
|
+
Object.entries(item).forEach(([platform, url]) => {
|
|
25
|
+
sns[platform] = url;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Return the clean schema
|
|
32
|
+
return {
|
|
33
|
+
name: page.frontmatter.title || "",
|
|
34
|
+
id: page.frontmatter.id || "",
|
|
35
|
+
image: page.frontmatter.image || "",
|
|
36
|
+
bio: page.frontmatter.description || "",
|
|
37
|
+
role,
|
|
38
|
+
sns,
|
|
39
|
+
url: page.url,
|
|
40
|
+
};
|
|
41
|
+
})
|
|
42
|
+
.sort((a, b) => {
|
|
43
|
+
// "0_new" (id: new_member) should be in the 1st position.
|
|
44
|
+
if (a.id === "new_member") return -1;
|
|
45
|
+
if (b.id === "new_member") return 1;
|
|
46
|
+
|
|
47
|
+
// Otherwise sort alphabetically by ID (which maps to filename / last name)
|
|
48
|
+
return a.id.localeCompare(b.id);
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
});
|