@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,76 @@
1
+ import TestimonialsSection from "./TestimonialsSection.vue";
2
+
3
+ describe("<TestimonialsSection />", () => {
4
+ it("renders with default props and switches testimonials", () => {
5
+ cy.mount(TestimonialsSection);
6
+
7
+ // Verify title
8
+ cy.get(".headline h2").should("contain", "What our customers are saying");
9
+
10
+ // Verify first testimonial quote
11
+ cy.get(".quote-text").should(
12
+ "contain",
13
+ "The VirES platform has completely revolutionized",
14
+ );
15
+ cy.get(".author-name").should("contain", "Dr. Fiona Harrison");
16
+
17
+ // Click next button
18
+ cy.get("button[aria-label='Next testimonial']").click();
19
+
20
+ // Verify second testimonial quote
21
+ cy.get(".quote-text").should(
22
+ "contain",
23
+ "Deploying our Area Monitoring System",
24
+ );
25
+ cy.get(".author-name").should("contain", "Dr. Liam O'Connor");
26
+
27
+ // Click prev button
28
+ cy.get("button[aria-label='Previous testimonial']").click();
29
+
30
+ // Verify first testimonial quote again
31
+ cy.get(".quote-text").should(
32
+ "contain",
33
+ "The VirES platform has completely revolutionized",
34
+ );
35
+ cy.get(".author-name").should("contain", "Dr. Fiona Harrison");
36
+ });
37
+
38
+ it("renders custom title and custom testimonials list", () => {
39
+ const customTitle = "What Our Amazing Clients Think";
40
+ const customTestimonials = [
41
+ {
42
+ quote: "This is a custom quote that is extremely positive.",
43
+ author: "Alice Smith",
44
+ role: "Software Architect",
45
+ },
46
+ {
47
+ quote: "Another custom quote for testing purposes.",
48
+ author: "Bob Jones",
49
+ role: "Data Analyst",
50
+ },
51
+ ];
52
+
53
+ cy.mount(TestimonialsSection, {
54
+ props: {
55
+ title: customTitle,
56
+ testimonials: customTestimonials,
57
+ },
58
+ });
59
+
60
+ // Check title
61
+ cy.get(".headline h2").should("contain", customTitle);
62
+
63
+ // Check first custom testimonial
64
+ cy.get(".quote-text").should("contain", "This is a custom quote");
65
+ cy.get(".author-name").should("contain", "Alice Smith");
66
+ cy.get(".author-role").should("contain", "Software Architect");
67
+
68
+ // Go next
69
+ cy.get("button[aria-label='Next testimonial']").click();
70
+
71
+ // Check second custom testimonial
72
+ cy.get(".quote-text").should("contain", "Another custom quote");
73
+ cy.get(".author-name").should("contain", "Bob Jones");
74
+ cy.get(".author-role").should("contain", "Data Analyst");
75
+ });
76
+ });
@@ -0,0 +1,368 @@
1
+ <template>
2
+ <section class="testimonials-section full-width surface-container-lowest">
3
+ <div class="holder">
4
+ <!-- Left Side: Headline -->
5
+ <div class="headline">
6
+ <h2>
7
+ {{ title }}
8
+ </h2>
9
+ </div>
10
+
11
+ <!-- Right Side: Text Block -->
12
+ <div
13
+ v-if="testimonials && testimonials.length > 0"
14
+ class="text-block"
15
+ role="region"
16
+ aria-roledescription="carousel"
17
+ :aria-label="title"
18
+ >
19
+ <!-- Testimonial Quotes Overlapping Grid -->
20
+ <div class="quotes-grid" aria-live="polite">
21
+ <div
22
+ v-for="(item, index) in testimonials"
23
+ :key="index"
24
+ class="quote-slide"
25
+ :class="{
26
+ active: index === currentIndex,
27
+ 'quote-text': index === currentIndex,
28
+ }"
29
+ :aria-hidden="index !== currentIndex"
30
+ >
31
+ &ldquo;{{ item.quote }}&rdquo;
32
+ </div>
33
+ </div>
34
+
35
+ <!-- Divider / Separator -->
36
+ <div class="divider"></div>
37
+
38
+ <!-- Frame 469: Author info & sliders -->
39
+ <div class="frame-469">
40
+ <!-- Frame 468: Author Name and Role Overlapping Grid -->
41
+ <div class="frame-468">
42
+ <div
43
+ v-for="(item, index) in testimonials"
44
+ :key="index"
45
+ class="author-slide"
46
+ :class="{ active: index === currentIndex }"
47
+ :aria-hidden="index !== currentIndex"
48
+ >
49
+ <span
50
+ class="author-name-text"
51
+ :class="{ 'author-name': index === currentIndex }"
52
+ >
53
+ {{ item.author }}
54
+ </span>
55
+ <span
56
+ class="author-role-text"
57
+ :class="{ 'author-role': index === currentIndex }"
58
+ >
59
+ {{ item.role }}
60
+ </span>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- sliders: Navigation Buttons -->
65
+ <div class="sliders">
66
+ <!-- Left Arrow Button -->
67
+ <button
68
+ class="nav-button"
69
+ @click="prev"
70
+ aria-label="Previous testimonial"
71
+ >
72
+ <i class="mdi mdi-arrow-left"></i>
73
+ </button>
74
+
75
+ <!-- Right Arrow Button -->
76
+ <button
77
+ class="nav-button"
78
+ @click="next"
79
+ aria-label="Next testimonial"
80
+ >
81
+ <i class="mdi mdi-arrow-right"></i>
82
+ </button>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </section>
88
+ </template>
89
+
90
+ <script setup>
91
+ import { ref, computed, watch } from "vue";
92
+
93
+ const props = defineProps({
94
+ title: {
95
+ type: String,
96
+ default: "What our customers are saying",
97
+ },
98
+ testimonials: {
99
+ type: Array,
100
+ default: () => [
101
+ {
102
+ quote:
103
+ "The VirES platform has completely revolutionized how Swarm mission data is accessed and analyzed by the global scientific community. Its cloud-native performance and seamless visualization are unmatched.",
104
+ author: "Dr. Fiona Harrison",
105
+ role: "Lead Swarm Researcher, Scientific Advisory Group",
106
+ },
107
+ {
108
+ quote:
109
+ "Deploying our Area Monitoring System on EOxHub workspaces allowed us to process millions of parcels in near real-time. This level of automation and reliability is precisely what we needed for the CAP implementation.",
110
+ author: "Dr. Liam O'Connor",
111
+ role: "Senior Spatial Scientist, Agricultural Paying Agency",
112
+ },
113
+ {
114
+ quote:
115
+ "The EOX base map layers are an essential asset for our global dashboard. Their Sentinel-2 cloudless composites provide a pristine, cloud-free visual experience that our users absolutely love.",
116
+ author: "Sarah Jenkins",
117
+ role: "Chief Product Officer, Global Earth Explorer Platform",
118
+ },
119
+ ],
120
+ },
121
+ });
122
+
123
+ const currentIndex = ref(0);
124
+
125
+ // Clamp currentIndex if testimonials prop array changes dynamically
126
+ watch(
127
+ () => props.testimonials?.length,
128
+ (len) => {
129
+ if (len && currentIndex.value >= len) {
130
+ currentIndex.value = 0;
131
+ }
132
+ },
133
+ );
134
+
135
+ const currentTestimonial = computed(() => {
136
+ if (!props.testimonials || props.testimonials.length === 0) return null;
137
+ return props.testimonials[currentIndex.value];
138
+ });
139
+
140
+ function prev() {
141
+ if (!props.testimonials || props.testimonials.length === 0) return;
142
+ currentIndex.value =
143
+ (currentIndex.value - 1 + props.testimonials.length) %
144
+ props.testimonials.length;
145
+ }
146
+
147
+ function next() {
148
+ if (!props.testimonials || props.testimonials.length === 0) return;
149
+ currentIndex.value = (currentIndex.value + 1) % props.testimonials.length;
150
+ }
151
+ </script>
152
+
153
+ <style scoped>
154
+ section.testimonials-section {
155
+ display: flex;
156
+ justify-content: center;
157
+ overflow-x: hidden;
158
+ padding: 3rem 1.5rem;
159
+ margin-block-start: 6rem;
160
+ margin-block-end: 6rem;
161
+ box-sizing: border-box;
162
+ background-color: var(--surface-container-lowest, #ffffff);
163
+ }
164
+
165
+ @media (min-width: 1024px) {
166
+ section.testimonials-section {
167
+ padding: 6rem 1.5rem;
168
+ }
169
+ }
170
+
171
+ .holder {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 32px;
175
+ width: 100%;
176
+ }
177
+
178
+ @media (min-width: 1024px) {
179
+ .holder {
180
+ flex-direction: row;
181
+ align-items: flex-start;
182
+ justify-content: space-between;
183
+ gap: 64px;
184
+ }
185
+ }
186
+
187
+ .headline {
188
+ flex: 1 1 50%;
189
+ max-width: 620px;
190
+ }
191
+
192
+ .headline h2 {
193
+ font-family: "Inter", sans-serif;
194
+ font-style: normal;
195
+ font-weight: 700;
196
+ font-size: 36px !important;
197
+ line-height: 44px;
198
+ letter-spacing: 0.5008px;
199
+ color: #00060a;
200
+ margin: 0 !important;
201
+ padding: 0 !important;
202
+ border-top: none !important;
203
+ }
204
+
205
+ .text-block {
206
+ flex: 1 1 50%;
207
+ max-width: 620px;
208
+ display: flex;
209
+ flex-direction: column;
210
+ gap: 24px;
211
+ }
212
+
213
+ .quotes-grid {
214
+ display: grid;
215
+ grid-template-columns: 1fr;
216
+ width: 100%;
217
+ }
218
+
219
+ .quote-slide {
220
+ grid-column: 1;
221
+ grid-row: 1;
222
+ align-self: start;
223
+ font-family: "Inter", sans-serif;
224
+ font-style: normal;
225
+ font-weight: 400;
226
+ font-size: 16px;
227
+ line-height: 24px;
228
+ letter-spacing: 0.5px;
229
+ color: #00060a;
230
+ min-height: 72px;
231
+ opacity: 0;
232
+ visibility: hidden;
233
+ transition:
234
+ opacity 0.25s ease,
235
+ visibility 0.25s ease;
236
+ pointer-events: none;
237
+ }
238
+
239
+ .quote-slide.active {
240
+ opacity: 1;
241
+ visibility: visible;
242
+ pointer-events: auto;
243
+ }
244
+
245
+ .quote-text {
246
+ font-family: "Inter", sans-serif;
247
+ font-style: normal;
248
+ font-weight: 400;
249
+ font-size: 16px;
250
+ line-height: 24px;
251
+ letter-spacing: 0.5px;
252
+ color: #00060a;
253
+ min-height: 72px;
254
+ }
255
+
256
+ .divider {
257
+ width: 100%;
258
+ height: 1px;
259
+ background: #ccd9e2;
260
+ }
261
+
262
+ .frame-469 {
263
+ display: flex;
264
+ flex-direction: row;
265
+ align-items: center;
266
+ justify-content: space-between;
267
+ gap: 16px;
268
+ width: 100%;
269
+ }
270
+
271
+ .frame-468 {
272
+ display: grid;
273
+ grid-template-columns: 1fr;
274
+ align-items: flex-start;
275
+ flex-grow: 1;
276
+ }
277
+
278
+ .author-slide {
279
+ grid-column: 1;
280
+ grid-row: 1;
281
+ display: flex;
282
+ flex-direction: column;
283
+ align-items: flex-start;
284
+ gap: 8px;
285
+ opacity: 0;
286
+ visibility: hidden;
287
+ transition:
288
+ opacity 0.25s ease,
289
+ visibility 0.25s ease;
290
+ pointer-events: none;
291
+ }
292
+
293
+ .author-slide.active {
294
+ opacity: 1;
295
+ visibility: visible;
296
+ pointer-events: auto;
297
+ }
298
+
299
+ .author-name,
300
+ .author-name-text {
301
+ font-family: "Inter", sans-serif;
302
+ font-style: normal;
303
+ font-weight: 700;
304
+ font-size: 16px;
305
+ line-height: 24px;
306
+ letter-spacing: 0.5px;
307
+ color: #00060a;
308
+ }
309
+
310
+ .author-role,
311
+ .author-role-text {
312
+ font-family: "Inter", sans-serif;
313
+ font-style: normal;
314
+ font-weight: 400;
315
+ font-size: 16px;
316
+ line-height: 24px;
317
+ letter-spacing: 0.5px;
318
+ color: #00060a;
319
+ }
320
+
321
+ @media (prefers-reduced-motion: reduce) {
322
+ .quote-slide,
323
+ .author-slide {
324
+ transition: none;
325
+ }
326
+ }
327
+
328
+ .sliders {
329
+ display: flex;
330
+ flex-direction: row;
331
+ align-items: center;
332
+ gap: 24px;
333
+ flex-shrink: 0;
334
+ }
335
+
336
+ .nav-button {
337
+ display: flex !important;
338
+ flex-direction: row !important;
339
+ justify-content: center !important;
340
+ align-items: center !important;
341
+ padding: 0 !important;
342
+ width: 48px !important;
343
+ min-width: 48px !important;
344
+ max-width: 48px !important;
345
+ height: 48px !important;
346
+ min-height: 48px !important;
347
+ max-height: 48px !important;
348
+ background: #004170 !important;
349
+ box-shadow:
350
+ 0px 24px 48px -8px rgba(62, 52, 69, 0.12),
351
+ 0px 2px 4px rgba(62, 52, 69, 0.04) !important;
352
+ border-radius: 999px !important;
353
+ border: none !important;
354
+ cursor: pointer !important;
355
+ transition: background-color 0.2s ease !important;
356
+ box-sizing: border-box !important;
357
+ }
358
+
359
+ .nav-button:hover {
360
+ background: #002e52;
361
+ }
362
+
363
+ .nav-button i {
364
+ color: #ffffff;
365
+ font-size: 24px;
366
+ line-height: 1;
367
+ }
368
+ </style>
@@ -0,0 +1,115 @@
1
+ import { h } from "vue";
2
+ import TextSection from "./TextSection.vue";
3
+
4
+ describe("<TextSection />", () => {
5
+ it("renders title, tagline and button", () => {
6
+ cy.mount(TextSection, {
7
+ props: {
8
+ title: "Test Section",
9
+ tagline: "Test Tagline",
10
+ buttonText: "Click Me",
11
+ buttonLink: "https://example.com",
12
+ },
13
+ });
14
+
15
+ cy.contains("Test Section").should("exist");
16
+ cy.contains("Test Tagline").should("exist");
17
+ cy.contains("a.button", "Click Me")
18
+ .should("have.attr", "href", "https://example.com")
19
+ .and("have.attr", "target", "_blank");
20
+ });
21
+
22
+ it("renders slot content and image", () => {
23
+ cy.mount(TextSection, {
24
+ props: {
25
+ image: "/test.jpg",
26
+ imageAlt: "Test Image",
27
+ },
28
+ slots: {
29
+ default: "<p>This is paragraph text.</p>",
30
+ },
31
+ });
32
+
33
+ cy.contains("This is paragraph text.").should("exist");
34
+ cy.get("img.section-image")
35
+ .should("have.attr", "src", "/test.jpg")
36
+ .and("have.attr", "alt", "Test Image");
37
+ });
38
+
39
+ it("applies reverse class when reverse prop is true", () => {
40
+ cy.mount(TextSection, {
41
+ props: {
42
+ image: "/test.jpg",
43
+ reverse: true,
44
+ },
45
+ });
46
+
47
+ cy.get(".text-section").should("have.class", "is-reverse");
48
+ cy.get(".text-section-grid").should("have.class", "grid-reverse");
49
+ });
50
+
51
+ it("applies single column styling when no image is provided", () => {
52
+ cy.mount(TextSection, {
53
+ slots: {
54
+ default: "<p>Single column text</p>",
55
+ },
56
+ });
57
+
58
+ cy.get(".text-section").should("have.class", "no-image");
59
+ cy.get(".text-section-grid").should("have.class", "grid-single");
60
+ cy.get(".media-column").should("not.exist");
61
+ });
62
+
63
+ it("applies extra padding-block-end to a single section", () => {
64
+ cy.mount(TextSection, {
65
+ props: {
66
+ title: "Single Section",
67
+ },
68
+ });
69
+
70
+ cy.get(".text-section").should("have.css", "padding-bottom", "80px");
71
+ cy.get(".text-section").should("have.css", "padding-top", "56px");
72
+ });
73
+
74
+ it("applies extra padding-block-end only to the last section when multiple are consecutive", () => {
75
+ const TestWrapper = {
76
+ render() {
77
+ return h("div", [
78
+ h(TextSection, { title: "Section 1" }),
79
+ h(TextSection, { title: "Section 2" }),
80
+ h(TextSection, { title: "Section 3" }),
81
+ ]);
82
+ },
83
+ };
84
+
85
+ cy.mount(TestWrapper);
86
+
87
+ // First section: standard padding-bottom 56px, normal padding-top 56px
88
+ cy.get(".text-section").eq(0).should("have.css", "padding-bottom", "56px");
89
+ cy.get(".text-section").eq(0).should("have.css", "padding-top", "56px");
90
+
91
+ // Second section: standard padding-bottom 56px, tightened padding-top 16px
92
+ cy.get(".text-section").eq(1).should("have.css", "padding-bottom", "56px");
93
+ cy.get(".text-section").eq(1).should("have.css", "padding-top", "16px");
94
+
95
+ // Third (last) section: extra padding-bottom 80px, tightened padding-top 16px
96
+ cy.get(".text-section").eq(2).should("have.css", "padding-bottom", "80px");
97
+ cy.get(".text-section").eq(2).should("have.css", "padding-top", "16px");
98
+ });
99
+
100
+ it("handles compact sections with appropriate padding-block-end", () => {
101
+ const TestWrapper = {
102
+ render() {
103
+ return h("div", [
104
+ h(TextSection, { title: "Compact 1", compact: true }),
105
+ h(TextSection, { title: "Compact 2", compact: true }),
106
+ ]);
107
+ },
108
+ };
109
+
110
+ cy.mount(TestWrapper);
111
+
112
+ cy.get(".text-section").eq(0).should("have.css", "padding-bottom", "28px");
113
+ cy.get(".text-section").eq(1).should("have.css", "padding-bottom", "40px");
114
+ });
115
+ });