@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,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
+ }
@@ -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
+ });