@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.
Files changed (53) hide show
  1. package/.gitlab-ci.yml +4 -1
  2. package/README.md +88 -11
  3. package/cypress/support/mocks/authors.data.js +14 -0
  4. package/cypress/support/mocks/helpers.js +65 -4
  5. package/cypress/support/mocks/vitepress.js +8 -1
  6. package/cypress.config.js +14 -0
  7. package/package.json +3 -1
  8. package/src/Layout.vue +69 -1
  9. package/src/authors.data.js +14 -0
  10. package/src/components/BlogList.vue +655 -0
  11. package/src/components/BlogPost.vue +472 -0
  12. package/src/components/CTASection.vue +18 -4
  13. package/src/components/FeatureCard/Index.vue +462 -31
  14. package/src/components/FeatureCard/Poster.vue +25 -3
  15. package/src/components/FeatureSection.cy.js +16 -0
  16. package/src/components/FeatureSection.vue +3 -8
  17. package/src/components/FeaturesGallery.vue +34 -2
  18. package/src/components/Footer.cy.js +52 -2
  19. package/src/components/Footer.vue +17 -5
  20. package/src/components/HeroSection.cy.js +20 -0
  21. package/src/components/HeroSection.vue +140 -25
  22. package/src/components/ImageCarousel.vue +11 -2
  23. package/src/components/ImageElement.vue +16 -4
  24. package/src/components/ImageGallery.vue +29 -4
  25. package/src/components/LogoSection.vue +26 -3
  26. package/src/components/MobileNavDropdown.vue +13 -3
  27. package/src/components/NavBar.cy.js +15 -0
  28. package/src/components/NavBar.vue +149 -21
  29. package/src/components/NavDropdown.vue +12 -2
  30. package/src/components/OpenPositions.vue +292 -0
  31. package/src/components/SectionHeader.cy.js +54 -0
  32. package/src/components/SectionHeader.vue +115 -0
  33. package/src/components/TagList.vue +311 -0
  34. package/src/components/TeamPage.vue +347 -0
  35. package/src/components/TestimonialsSection.cy.js +76 -0
  36. package/src/components/TestimonialsSection.vue +368 -0
  37. package/src/components/TextSection.cy.js +115 -0
  38. package/src/components/TextSection.vue +396 -0
  39. package/src/helpers.js +182 -37
  40. package/src/helpers.test.js +181 -0
  41. package/src/index.js +16 -0
  42. package/src/pages/author/[author].md +5 -0
  43. package/src/pages/author/[author].paths.js +60 -0
  44. package/src/pages/blog.md +5 -0
  45. package/src/pages/tag/[tag].md +5 -0
  46. package/src/pages/tag/[tag].paths.js +49 -0
  47. package/src/pages/tag.md +5 -0
  48. package/src/pages/team.md +5 -0
  49. package/src/posts.data.js +134 -0
  50. package/src/style.css +389 -8
  51. package/src/tagUtils.js +148 -0
  52. package/src/team.data.js +1 -1
  53. package/src/vitepressConfig.mjs +101 -1
@@ -0,0 +1,292 @@
1
+ <template>
2
+ <div class="open-positions-container">
3
+ <h2 v-if="title" id="open-positions" class="open-positions-title">
4
+ {{ title }}
5
+ </h2>
6
+
7
+ <!-- List of Open Positions if available -->
8
+ <div v-if="positions && positions.length > 0" class="positions-list">
9
+ <div
10
+ v-for="(position, index) in positions"
11
+ :key="index"
12
+ class="position-item"
13
+ >
14
+ <h3 class="position-title">{{ position.title }}</h3>
15
+
16
+ <div v-if="position.type || position.location" class="position-meta">
17
+ <span v-if="position.type" class="meta-item">
18
+ <i class="mdi mdi-calendar-blank-outline"></i>
19
+ {{ position.type }}
20
+ </span>
21
+ <span v-if="position.location" class="meta-item">
22
+ <i class="mdi mdi-map-marker-outline"></i>
23
+ {{ position.location }}
24
+ </span>
25
+ </div>
26
+
27
+ <p class="position-description">
28
+ {{ position.description || position.content }}
29
+ </p>
30
+
31
+ <a
32
+ :href="resolveApplyLink(position)"
33
+ :target="
34
+ isExternal(resolveApplyLink(position)) ? '_blank' : undefined
35
+ "
36
+ :rel="
37
+ isExternal(resolveApplyLink(position))
38
+ ? 'noopener noreferrer'
39
+ : undefined
40
+ "
41
+ class="position-apply"
42
+ >
43
+ <span>{{ position.linkText || "Apply for this position" }}</span>
44
+ <i class="mdi mdi-arrow-right"></i>
45
+ </a>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Fallback when no positions are open -->
50
+ <div v-else class="empty-positions">
51
+ <slot name="empty">
52
+ <p class="empty-title">
53
+ {{ emptyTitle }}
54
+ </p>
55
+ <p class="empty-description">
56
+ {{ emptyText }}
57
+ </p>
58
+ <p v-if="showDevjobs" class="empty-devjobs">
59
+ Also feel free to explore our Team page on
60
+ <a :href="devjobsGerman" target="_blank" rel="noopener noreferrer"
61
+ >Devjobs.at (German)</a
62
+ >
63
+ or in
64
+ <a :href="devjobsEnglish" target="_blank" rel="noopener noreferrer"
65
+ >English</a
66
+ >.
67
+ </p>
68
+ </slot>
69
+ </div>
70
+
71
+ <!-- Always display "Send us your CV" button -->
72
+ <div v-if="showCvButton" class="cv-action">
73
+ <a :href="`mailto:${email}`" class="cv-button">
74
+ <span>{{ cvButtonText }}</span>
75
+ <i class="mdi mdi-email-outline"></i>
76
+ </a>
77
+ </div>
78
+ </div>
79
+ </template>
80
+
81
+ <script setup>
82
+ import { withBase } from "vitepress";
83
+
84
+ const props = defineProps({
85
+ title: {
86
+ type: String,
87
+ default: "",
88
+ },
89
+ positions: {
90
+ type: Array,
91
+ default: () => [],
92
+ },
93
+ email: {
94
+ type: String,
95
+ default: "office@eox.at",
96
+ },
97
+ cvButtonText: {
98
+ type: String,
99
+ default: "Send us your CV",
100
+ },
101
+ showCvButton: {
102
+ type: Boolean,
103
+ default: true,
104
+ },
105
+ showDevjobs: {
106
+ type: Boolean,
107
+ default: true,
108
+ },
109
+ devjobsGerman: {
110
+ type: String,
111
+ default: "https://devjobs.at/team/eox",
112
+ },
113
+ devjobsEnglish: {
114
+ type: String,
115
+ default: "https://en.devjobs.at/team/eox",
116
+ },
117
+ emptyTitle: {
118
+ type: String,
119
+ default: "Great to see you are interested in joining us!",
120
+ },
121
+ emptyText: {
122
+ type: String,
123
+ default:
124
+ "Currently, we are not looking to fill a particular position, but you're welcome to send us your CV and tell us why you'd be a good fit for our team.",
125
+ },
126
+ });
127
+
128
+ function resolveApplyLink(position) {
129
+ if (position.link) {
130
+ if (
131
+ position.link.startsWith("http://") ||
132
+ position.link.startsWith("https://") ||
133
+ position.link.startsWith("mailto:")
134
+ ) {
135
+ return position.link;
136
+ }
137
+ return withBase(
138
+ position.link.startsWith("/") ? position.link : "/" + position.link,
139
+ );
140
+ }
141
+ return `mailto:${props.email}?subject=Application for ${encodeURIComponent(position.title || "Open Position")}`;
142
+ }
143
+
144
+ function isExternal(url) {
145
+ return (
146
+ typeof url === "string" &&
147
+ (url.startsWith("http://") || url.startsWith("https://"))
148
+ );
149
+ }
150
+ </script>
151
+
152
+ <style scoped>
153
+ .open-positions-container {
154
+ margin-top: 1.5rem;
155
+ margin-bottom: 2.5rem;
156
+ }
157
+
158
+ .open-positions-title {
159
+ margin-bottom: 1.5rem !important;
160
+ }
161
+
162
+ .positions-list {
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: 2.5rem;
166
+ margin-bottom: 2rem;
167
+ }
168
+
169
+ .position-item {
170
+ display: flex;
171
+ flex-direction: column;
172
+ align-items: flex-start;
173
+ }
174
+
175
+ .position-item:not(:last-child) {
176
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
177
+ padding-bottom: 2rem;
178
+ }
179
+
180
+ .position-title {
181
+ font-size: 1.35rem !important;
182
+ font-weight: 700 !important;
183
+ color: var(--on-surface, #0f172a) !important;
184
+ margin: 0 0 0.5rem 0 !important;
185
+ border-top: none !important;
186
+ padding-top: 0 !important;
187
+ line-height: 1.3 !important;
188
+ }
189
+
190
+ .position-meta {
191
+ display: flex;
192
+ flex-wrap: wrap;
193
+ align-items: center;
194
+ gap: 1.5rem;
195
+ color: var(--on-surface-variant, #475569);
196
+ font-size: 0.95rem;
197
+ margin-bottom: 1rem;
198
+ }
199
+
200
+ .meta-item {
201
+ display: inline-flex;
202
+ align-items: center;
203
+ gap: 0.35rem;
204
+ }
205
+
206
+ .meta-item i {
207
+ font-size: 1.15rem;
208
+ line-height: 1;
209
+ }
210
+
211
+ .position-description {
212
+ font-size: 1rem !important;
213
+ line-height: 1.6 !important;
214
+ color: var(--on-surface-variant, #334155) !important;
215
+ margin: 0 0 1.25rem 0 !important;
216
+ }
217
+
218
+ .position-apply {
219
+ display: inline-flex;
220
+ align-items: center;
221
+ gap: 0.4rem;
222
+ font-weight: 700;
223
+ color: var(--primary, #004170) !important;
224
+ text-decoration: none !important;
225
+ font-size: 1rem;
226
+ transition:
227
+ gap 0.2s ease,
228
+ color 0.2s ease;
229
+ }
230
+
231
+ .position-apply:hover {
232
+ gap: 0.65rem;
233
+ text-decoration: underline !important;
234
+ }
235
+
236
+ .position-apply i {
237
+ font-size: 1.1rem;
238
+ transition: transform 0.2s ease;
239
+ }
240
+
241
+ .empty-positions {
242
+ margin-bottom: 1.5rem;
243
+ }
244
+
245
+ .empty-title {
246
+ font-weight: 700 !important;
247
+ font-size: 1.15rem !important;
248
+ color: var(--on-surface, #0f172a) !important;
249
+ margin: 0 0 0.75rem 0 !important;
250
+ }
251
+
252
+ .empty-description,
253
+ .empty-devjobs {
254
+ font-size: 1rem !important;
255
+ line-height: 1.6 !important;
256
+ color: var(--on-surface-variant, #334155) !important;
257
+ margin: 0 0 1rem 0 !important;
258
+ }
259
+
260
+ .cv-action {
261
+ margin-top: 1.5rem;
262
+ }
263
+
264
+ .cv-button {
265
+ display: inline-flex;
266
+ align-items: center;
267
+ gap: 0.6rem;
268
+ background-color: var(--primary, #004170);
269
+ color: #ffffff !important;
270
+ font-weight: 600;
271
+ font-size: 0.95rem;
272
+ padding: 0.7rem 1.5rem;
273
+ border-radius: 9999px;
274
+ text-decoration: none !important;
275
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
276
+ transition:
277
+ transform 0.2s ease,
278
+ box-shadow 0.2s ease,
279
+ background-color 0.2s ease;
280
+ }
281
+
282
+ .cv-button:hover {
283
+ transform: translateY(-2px);
284
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.18);
285
+ background-color: color-mix(in srgb, var(--primary, #004170) 88%, black);
286
+ }
287
+
288
+ .cv-button i {
289
+ font-size: 1.15rem;
290
+ line-height: 1;
291
+ }
292
+ </style>
@@ -0,0 +1,54 @@
1
+ import SectionHeader from "./SectionHeader.vue";
2
+
3
+ describe("<SectionHeader />", () => {
4
+ it("renders standard props correctly", () => {
5
+ const tag = "Viewing Ready & Basic";
6
+ const icon = "mdi-star-check-outline";
7
+ const title = "What our users say about Viewing Products";
8
+ const subtitle =
9
+ "Optional paragraph providing context and detail for this section.";
10
+
11
+ cy.mount(SectionHeader, {
12
+ props: {
13
+ tag,
14
+ icon,
15
+ title,
16
+ subtitle,
17
+ },
18
+ });
19
+
20
+ // Check that tag badge exists and contains correct icon and text classes
21
+ cy.get(".section-badge-tag").should("exist");
22
+ cy.get(".section-badge-icon").should("have.class", icon);
23
+ cy.get(".section-badge-text").should("contain", tag);
24
+
25
+ // Check title
26
+ cy.get(".section-display-title").should("contain", title);
27
+
28
+ // Check subtitle
29
+ cy.get(".section-subtitle").should("contain", subtitle);
30
+ });
31
+
32
+ it("renders with title slot and default slot", () => {
33
+ cy.mount(SectionHeader, {
34
+ slots: {
35
+ title: "Slot-based Title",
36
+ default: "Slot-based Subtitle",
37
+ },
38
+ });
39
+
40
+ cy.get(".section-display-title").should("contain", "Slot-based Title");
41
+ cy.get(".section-subtitle").should("contain", "Slot-based Subtitle");
42
+ });
43
+
44
+ it("hides badge if no tag or icon is provided", () => {
45
+ cy.mount(SectionHeader, {
46
+ props: {
47
+ title: "Just Title",
48
+ },
49
+ });
50
+
51
+ cy.get(".section-badge-tag").should("not.exist");
52
+ cy.get(".section-subtitle").should("not.exist");
53
+ });
54
+ });
@@ -0,0 +1,115 @@
1
+ <template>
2
+ <div class="section-header-group">
3
+ <!-- Section Tag Badge -->
4
+ <div v-if="tag || icon" class="section-badge-tag">
5
+ <i v-if="icon" :class="['mdi', icon, 'section-badge-icon']"></i>
6
+ <span class="section-badge-text">{{ tag }}</span>
7
+ </div>
8
+
9
+ <!-- Display Title -->
10
+ <h2 class="section-display-title">
11
+ <slot name="title">{{ title }}</slot>
12
+ </h2>
13
+
14
+ <!-- Subtitle / Body text -->
15
+ <p v-if="subtitle || $slots.default" class="section-subtitle">
16
+ <slot>{{ subtitle }}</slot>
17
+ </p>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup>
22
+ defineProps({
23
+ tag: {
24
+ type: String,
25
+ required: false,
26
+ default: "",
27
+ },
28
+ icon: {
29
+ type: String,
30
+ required: false,
31
+ default: "",
32
+ },
33
+ title: {
34
+ type: String,
35
+ required: false,
36
+ default: "",
37
+ },
38
+ subtitle: {
39
+ type: String,
40
+ required: false,
41
+ default: "",
42
+ },
43
+ });
44
+ </script>
45
+
46
+ <style scoped>
47
+ .section-header-group {
48
+ display: flex;
49
+ flex-direction: column;
50
+ align-items: flex-start;
51
+ padding: 0 !important;
52
+ margin: 0 0 40px 0 !important;
53
+ gap: 16px;
54
+ width: 100%;
55
+ }
56
+
57
+ .section-badge-tag {
58
+ display: flex;
59
+ flex-direction: row;
60
+ align-items: center;
61
+ padding: 0 !important;
62
+ margin: 0 !important;
63
+ gap: 12px;
64
+ height: 24px;
65
+ }
66
+
67
+ .section-badge-icon {
68
+ width: 24px;
69
+ height: 24px;
70
+ font-size: 24px;
71
+ line-height: 1;
72
+ display: inline-flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ margin: 0 !important;
76
+ padding: 0 !important;
77
+ color: var(--primary, #004170);
78
+ }
79
+
80
+ .section-badge-text {
81
+ font-family: "Inter", sans-serif;
82
+ font-style: normal;
83
+ font-weight: 400;
84
+ font-size: 16px;
85
+ line-height: 24px;
86
+ letter-spacing: 0.5px;
87
+ margin: 0 !important;
88
+ padding: 0 !important;
89
+ color: var(--primary, #004170);
90
+ }
91
+
92
+ .section-display-title {
93
+ font-family: "Inter", sans-serif;
94
+ font-style: normal;
95
+ font-weight: 700;
96
+ font-size: 36px !important;
97
+ line-height: 44px;
98
+ letter-spacing: 0.5008px;
99
+ margin: 0 !important;
100
+ padding: 0 !important;
101
+ border-top: none !important;
102
+ }
103
+
104
+ .section-subtitle {
105
+ font-family: "Inter", sans-serif;
106
+ font-style: normal;
107
+ font-weight: 400;
108
+ font-size: 16px;
109
+ line-height: 24px;
110
+ letter-spacing: 0.5px;
111
+ max-width: 620px;
112
+ margin: 0 !important;
113
+ padding: 0 !important;
114
+ }
115
+ </style>