@eox/pages-theme-eox 1.9.0 → 2.0.0-alpha.2

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,293 @@
1
+ <template>
2
+ <div class="tags-page">
3
+ <header class="tags-header">
4
+ <h1 class="tags-title">
5
+ Blog Tags
6
+ </h1>
7
+ <p class="tags-subtitle">
8
+ Explore our blog posts by topics and technology pillars.
9
+ </p>
10
+ </header>
11
+
12
+ <!-- Unified flat list of all tags -->
13
+ <div class="tags-container">
14
+ <div class="tags-flex">
15
+ <a
16
+ v-for="tag in allTagsList"
17
+ :key="tag.name"
18
+ :href="`/tag/${tag.name}/`"
19
+ :style="getTagStyle(tag.name)"
20
+ class="tag-item"
21
+ >
22
+ <span>{{ tag.name }}</span>
23
+ <span class="tag-count">{{ tag.count }}</span>
24
+ </a>
25
+ </div>
26
+ </div>
27
+
28
+ <!-- Tech pillars legend overview footer -->
29
+ <footer class="tags-footer">
30
+ <h3 class="legend-title">Technology Pillars:</h3>
31
+ <ul class="legend-grid">
32
+ <li class="legend-item">
33
+ <span
34
+ class="legend-dot"
35
+ style="background-color: rgb(255, 94, 94)"
36
+ ></span>
37
+ <span>Managed cloud workspace</span>
38
+ </li>
39
+ <li class="legend-item">
40
+ <span
41
+ class="legend-dot"
42
+ style="background-color: rgb(0, 177, 176)"
43
+ ></span>
44
+ <span>Data management</span>
45
+ </li>
46
+ <li class="legend-item">
47
+ <span
48
+ class="legend-dot"
49
+ style="background-color: rgb(219, 173, 66)"
50
+ ></span>
51
+ <span>Data visualization</span>
52
+ </li>
53
+ <li class="legend-item">
54
+ <span
55
+ class="legend-dot"
56
+ style="background-color: rgb(218, 71, 228)"
57
+ ></span>
58
+ <span>Data products</span>
59
+ </li>
60
+ </ul>
61
+ <p class="legend-note">
62
+ For more detailed information on our technology solutions, visit the
63
+ <a href="/software-products" class="legend-link">Solutions & Services</a>
64
+ page.
65
+ </p>
66
+ </footer>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup>
71
+ import { computed } from "vue";
72
+ import { data as posts } from "../posts.data.js";
73
+ import { getTagColor, getTagStyle } from "../tagUtils.js";
74
+
75
+ const allTagsList = computed(() => {
76
+ // Aggregate all tag occurrences
77
+ const tagCounts = {};
78
+ posts.forEach((p) => {
79
+ if (Array.isArray(p.tags)) {
80
+ p.tags.forEach((t) => {
81
+ const clean = t.trim().toLowerCase();
82
+ if (clean) {
83
+ tagCounts[clean] = (tagCounts[clean] || 0) + 1;
84
+ }
85
+ });
86
+ }
87
+ });
88
+
89
+ // Convert to sorted list
90
+ return Object.entries(tagCounts)
91
+ .map(([name, count]) => ({
92
+ name,
93
+ count,
94
+ color: getTagColor(name),
95
+ }))
96
+ .sort((a, b) => a.name.localeCompare(b.name));
97
+ });
98
+ </script>
99
+
100
+ <style scoped>
101
+ .tags-page {
102
+ width: 100%;
103
+ max-width: 1200px;
104
+ margin-left: auto;
105
+ margin-right: auto;
106
+ padding-left: 1rem;
107
+ padding-right: 1rem;
108
+ padding-top: calc(var(--vp-nav-height) + 24px);
109
+ padding-bottom: 48px;
110
+ box-sizing: border-box;
111
+ }
112
+
113
+ @media (min-width: 640px) {
114
+ .tags-page {
115
+ padding-left: 1.5rem;
116
+ padding-right: 1.5rem;
117
+ padding-top: calc(var(--vp-nav-height) + 40px);
118
+ padding-bottom: 64px;
119
+ }
120
+ }
121
+
122
+ @media (min-width: 960px) {
123
+ .tags-page {
124
+ padding-left: 2rem;
125
+ padding-right: 2rem;
126
+ padding-top: calc(var(--vp-nav-height) + 58px);
127
+ padding-bottom: 96px;
128
+ }
129
+ }
130
+
131
+ /* Page Header */
132
+ .tags-header {
133
+ margin-bottom: 2rem;
134
+ border-bottom: 1px solid #f1f5f9; /* slate-100 */
135
+ padding-bottom: 1.5rem;
136
+ text-align: center;
137
+ max-width: 42rem; /* 2xl equivalent */
138
+ margin-left: auto;
139
+ margin-right: auto;
140
+ }
141
+
142
+ .tags-title {
143
+ font-family: "Inter", sans-serif;
144
+ font-style: normal;
145
+ font-weight: 700;
146
+ font-size: 28px;
147
+ line-height: 36px;
148
+ color: #020617; /* slate-950 */
149
+ margin-top: 0;
150
+ margin-bottom: 0.5rem;
151
+ letter-spacing: 0.5px;
152
+ }
153
+
154
+ @media (min-width: 768px) {
155
+ .tags-title {
156
+ font-size: 32px;
157
+ line-height: 40px;
158
+ }
159
+ }
160
+
161
+ .tags-subtitle {
162
+ font-family: "Inter", sans-serif;
163
+ font-style: normal;
164
+ font-weight: 400;
165
+ font-size: 16px;
166
+ line-height: 24px;
167
+ color: #64748b; /* slate-500 */
168
+ max-width: 620px;
169
+ margin-left: auto;
170
+ margin-right: auto;
171
+ margin-top: 0;
172
+ margin-bottom: 0;
173
+ padding: 0;
174
+ letter-spacing: 0.5px;
175
+ }
176
+
177
+ /* Tag Collection Container */
178
+ .tags-container {
179
+ background-color: var(--surface-container-low);
180
+ padding: 2.5rem 2rem;
181
+ border-radius: 1rem;
182
+ border: 1px solid rgba(226, 232, 240, 0.5); /* border-slate-100/50 */
183
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05); /* shadow-sm */
184
+ margin-bottom: 3rem;
185
+ }
186
+
187
+ .tags-flex {
188
+ display: flex;
189
+ flex-wrap: wrap;
190
+ gap: 0.75rem;
191
+ justify-content: center;
192
+ }
193
+
194
+ /* Tag Item Style */
195
+ .tag-item {
196
+ display: inline-flex;
197
+ align-items: center;
198
+ gap: 0.5rem;
199
+ font-size: 0.875rem; /* text-sm */
200
+ font-weight: 600;
201
+ padding: 0.375rem 0.875rem;
202
+ border-radius: 4px; /* small rounding to distinguish from full-rounded buttons */
203
+ text-decoration: none !important;
204
+ line-height: 1.2;
205
+ transition: opacity 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
206
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
207
+ }
208
+
209
+ .tag-item:hover {
210
+ opacity: 0.85;
211
+ transform: translateY(-1px);
212
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
213
+ }
214
+
215
+ .tag-count {
216
+ font-size: 0.75rem;
217
+ padding: 0.125rem 0.375rem;
218
+ background-color: rgba(255, 255, 255, 0.4);
219
+ color: inherit;
220
+ border-radius: 9999px;
221
+ font-weight: 700;
222
+ }
223
+
224
+ /* Legend Overview Footer */
225
+ .tags-footer {
226
+ background-color: var(--surface-container-low);
227
+ padding: 2rem;
228
+ border-radius: 1rem;
229
+ border: 1px solid rgba(226, 232, 240, 0.5);
230
+ max-width: 48rem; /* 3xl equivalent */
231
+ margin-left: auto;
232
+ margin-right: auto;
233
+ margin-top: 4rem;
234
+ }
235
+
236
+ .legend-title {
237
+ font-family: "Inter", sans-serif;
238
+ font-size: 1.125rem; /* text-lg */
239
+ font-weight: 700;
240
+ color: #020617; /* slate-950 */
241
+ margin-top: 0;
242
+ margin-bottom: 1rem;
243
+ }
244
+
245
+ .legend-grid {
246
+ display: grid;
247
+ grid-template-columns: 1fr;
248
+ gap: 1rem;
249
+ padding-left: 0;
250
+ list-style: none;
251
+ margin: 0;
252
+ }
253
+
254
+ @media (min-width: 640px) {
255
+ .legend-grid {
256
+ grid-template-columns: repeat(2, minmax(0, 1fr));
257
+ }
258
+ }
259
+
260
+ .legend-item {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 0.625rem;
264
+ font-size: 0.875rem; /* text-sm */
265
+ color: #475569; /* slate-600 */
266
+ }
267
+
268
+ .legend-dot {
269
+ width: 0.875rem;
270
+ height: 0.875rem;
271
+ border-radius: 9999px;
272
+ flex-shrink: 0;
273
+ }
274
+
275
+ .legend-note {
276
+ font-size: 0.875rem;
277
+ color: #64748b; /* slate-500 */
278
+ margin-top: 1.5rem;
279
+ padding-top: 1rem;
280
+ border-top: 1px solid rgba(226, 232, 240, 0.6);
281
+ line-height: 1.5;
282
+ }
283
+
284
+ .legend-link {
285
+ color: var(--primary, #004170);
286
+ font-weight: 600;
287
+ text-decoration: none;
288
+ }
289
+
290
+ .legend-link:hover {
291
+ text-decoration: underline;
292
+ }
293
+ </style>
@@ -0,0 +1,333 @@
1
+ <template>
2
+ <div class="team-page">
3
+ <!-- Header -->
4
+ <header class="team-header">
5
+ <h1 class="team-title">
6
+ {{ frontmatter.title || "Our Team" }}
7
+ </h1>
8
+ <p class="team-subtitle">
9
+ Meet the diverse team of geospatial experts, engineers, and visionaries
10
+ driving EOX forward.
11
+ </p>
12
+ </header>
13
+
14
+ <!-- Team Photo Section -->
15
+ <section class="team-photo-section">
16
+ <img
17
+ :src="withBase('/images/team/team.jpg')"
18
+ alt="EOX Team Photo"
19
+ class="team-photo"
20
+ />
21
+ </section>
22
+
23
+ <!-- Team Grid - max 3 columns on large screens -->
24
+ <div class="team-grid">
25
+ <FeatureCard
26
+ v-for="member in sortedTeamMembers"
27
+ :key="member.id"
28
+ :title="member.name"
29
+ :image="member.image ? withBase(member.image) : withBase('/images/team/new_member.jpg')"
30
+ :link="memberHasPosts(member.id) ? {
31
+ text: 'View blog posts',
32
+ href: `/author/${member.id}/`,
33
+ target: '_self'
34
+ } : undefined"
35
+ variant="cover"
36
+ class="bg-surface-container-low team-member-card"
37
+ >
38
+ <!-- Role / Job Title -->
39
+ <span v-if="member.role" class="member-role">
40
+ {{ member.role }}
41
+ </span>
42
+
43
+ <span class="member-divider"></span>
44
+
45
+ <!-- Description / Bio -->
46
+ <span
47
+ v-if="member.bio"
48
+ class="member-bio"
49
+ v-html="member.bio"
50
+ ></span>
51
+
52
+ <!-- Social Links row at the bottom of slot -->
53
+ <span class="member-footer">
54
+ <!-- Social Icons - aligned to the left matching other content -->
55
+ <span class="social-links-row">
56
+ <template v-for="(url, platform) in member.sns" :key="platform">
57
+ <a
58
+ v-if="url"
59
+ :href="url"
60
+ target="_blank"
61
+ rel="noopener noreferrer"
62
+ class="social-link-btn"
63
+ :title="`${member.name} on ${platform}`"
64
+ >
65
+ <!-- Bluesky custom SVG -->
66
+ <svg
67
+ v-if="platform === 'bluesky'"
68
+ viewBox="0 0 512 512"
69
+ style="width: 18px; height: 18px; fill: currentColor"
70
+ >
71
+ <path
72
+ d="M111.8 109.6C166.1 154.2 226.8 231.1 256 271.7c29.2-40.6 89.9-117.5 144.2-162.1C443.3 74.3 512 57.4 512 121.7c0 23.3-12.7 101.4-19 123.5-16 56.6-70.3 70.8-111.8 62.4C443 328.1 512 361.3 512 411.1c0 51.5-68.7 65.1-111.8 18.2-70.3 76.5-161.4 11-200.2-29-38.8 40-129.9 105.5-200.2 29C56.7 476.2 0 462.6 0 411.1c0-49.8 69-83 130.8-103.5C89.3 316 35 301.8 19 245.2 12.7 223 0 145 0 121.7c0-64.3 68.7-47.4 111.8-12.1z"
73
+ />
74
+ </svg>
75
+ <i
76
+ v-else-if="platform === 'linkedin'"
77
+ class="mdi mdi-linkedin text-lg"
78
+ ></i>
79
+ <i
80
+ v-else-if="platform === 'github'"
81
+ class="mdi mdi-github text-lg"
82
+ ></i>
83
+ <i
84
+ v-else-if="platform === 'twitter'"
85
+ class="mdi mdi-twitter text-lg"
86
+ ></i>
87
+ <i
88
+ v-else-if="platform === 'facebook'"
89
+ class="mdi mdi-facebook text-lg"
90
+ ></i>
91
+ <i v-else :class="`mdi mdi-${platform} text-lg`"></i>
92
+ </a>
93
+ </template>
94
+ </span>
95
+ </span>
96
+ </FeatureCard>
97
+ </div>
98
+ </div>
99
+ </template>
100
+
101
+ <script setup>
102
+ import { computed } from "vue";
103
+ import { useData, withBase } from "vitepress";
104
+ import { data as teamMembers } from "../team.data.js";
105
+ import { data as posts } from "../posts.data.js";
106
+ import FeatureCard from "./FeatureCard/Index.vue";
107
+
108
+ const { frontmatter } = useData();
109
+
110
+ const sortedTeamMembers = computed(() => {
111
+ return teamMembers;
112
+ });
113
+
114
+ const memberHasPosts = (memberId) => {
115
+ if (!memberId || memberId === "new_member") return false;
116
+ return posts.some((post) => {
117
+ return Array.isArray(post.authors) && post.authors.includes(memberId);
118
+ });
119
+ };
120
+ </script>
121
+
122
+ <style scoped>
123
+ .team-page {
124
+ width: 100%;
125
+ max-width: 1200px !important;
126
+ margin-left: auto !important;
127
+ margin-right: auto !important;
128
+ padding-left: 1rem !important;
129
+ padding-right: 1rem !important;
130
+ box-sizing: border-box !important;
131
+ padding-top: calc(var(--vp-nav-height) + 28px) !important;
132
+ padding-bottom: 48px !important;
133
+ word-break: break-word;
134
+ }
135
+
136
+ @media (min-width: 640px) {
137
+ .team-page {
138
+ padding-left: 1.5rem !important;
139
+ padding-right: 1.5rem !important;
140
+ padding-top: calc(var(--vp-nav-height) + 40px) !important;
141
+ padding-bottom: 64px !important;
142
+ }
143
+ }
144
+
145
+ @media (min-width: 960px) {
146
+ .team-page {
147
+ padding-left: 2rem !important;
148
+ padding-right: 2rem !important;
149
+ padding-top: calc(var(--vp-nav-height) + 58px) !important;
150
+ padding-bottom: 96px !important;
151
+ }
152
+ }
153
+
154
+ /* Page Header */
155
+ .team-header {
156
+ margin-bottom: 2.5rem;
157
+ border-bottom: 1px solid #f1f5f9; /* slate-100 */
158
+ padding-bottom: 1.5rem !important;
159
+ padding-top: 0 !important;
160
+ padding-left: 0 !important;
161
+ padding-right: 0 !important;
162
+ background: transparent !important;
163
+ min-height: auto !important;
164
+ height: auto !important;
165
+ }
166
+
167
+ .team-title {
168
+ font-family: "Inter", sans-serif;
169
+ font-style: normal;
170
+ font-weight: 700;
171
+ font-size: 28px;
172
+ line-height: 36px;
173
+ letter-spacing: 0.5px;
174
+ color: #020617; /* slate-950 */
175
+ margin-top: 0;
176
+ margin-bottom: 0.5rem;
177
+ }
178
+
179
+ @media (min-width: 768px) {
180
+ .team-title {
181
+ font-size: 32px;
182
+ line-height: 40px;
183
+ }
184
+ }
185
+
186
+ .team-subtitle {
187
+ font-family: "Inter", sans-serif;
188
+ font-style: normal;
189
+ font-weight: 400;
190
+ font-size: 16px;
191
+ line-height: 24px;
192
+ letter-spacing: 0.5px;
193
+ color: #64748b; /* slate-500 */
194
+ max-width: 620px;
195
+ margin: 0;
196
+ padding: 0;
197
+ }
198
+
199
+ /* Team Photo Section */
200
+ .team-photo-section {
201
+ margin-bottom: 3.5rem;
202
+ overflow: hidden;
203
+ border-radius: 1rem; /* 16px */
204
+ border: 1px solid rgba(226, 232, 240, 0.5); /* border-slate-100/50 */
205
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
206
+ }
207
+
208
+ .team-photo {
209
+ width: 100%;
210
+ height: auto;
211
+ object-fit: cover;
212
+ max-height: 500px;
213
+ }
214
+
215
+ /* Team Grid - custom grid definition to prevent conflict with Beercss global .grid */
216
+ .team-grid {
217
+ display: grid;
218
+ grid-template-columns: repeat(1, minmax(0, 1fr));
219
+ gap: 2rem;
220
+ }
221
+
222
+ @media (min-width: 640px) {
223
+ .team-grid {
224
+ grid-template-columns: repeat(2, minmax(0, 1fr));
225
+ }
226
+ }
227
+
228
+ @media (min-width: 1024px) {
229
+ .team-grid {
230
+ grid-template-columns: repeat(3, minmax(0, 1fr));
231
+ }
232
+ }
233
+
234
+ /* Deep overrides to integrate with theme-provided FeatureCard */
235
+ .team-member-card {
236
+ height: 100% !important;
237
+ }
238
+
239
+ .team-member-card :deep(.card-cover-link) {
240
+ aspect-ratio: 1 / 1 !important; /* Override cover layout aspect ratio to square portrait */
241
+ }
242
+
243
+ /* Left-align card titles and override margin */
244
+ .team-member-card :deep(.card-title-text) {
245
+ text-align: left !important;
246
+ margin-top: 0 !important;
247
+ margin-bottom: 0.5rem !important;
248
+ font-size: 1.25rem !important; /* exactly 20px matching card expectations */
249
+ font-weight: 700 !important;
250
+ }
251
+
252
+ /* Deep override card navigation to align properly */
253
+ .team-member-card :deep(.card-navigation) {
254
+ margin-top: 1rem !important;
255
+ padding-left: 1.5rem !important;
256
+ padding-bottom: 1.5rem !important;
257
+ }
258
+
259
+ .team-member-card :deep(.card-content-wrapper) {
260
+ display: flex !important;
261
+ flex-direction: column !important;
262
+ height: 100% !important;
263
+ }
264
+
265
+ .team-member-card :deep(.card-body-text) {
266
+ display: flex !important;
267
+ flex-direction: column !important;
268
+ flex-grow: 1 !important;
269
+ margin: 0 !important;
270
+ text-align: left !important;
271
+ }
272
+
273
+ /* Member Content Slot Elements (all left-aligned) */
274
+ .member-role {
275
+ display: block;
276
+ color: var(--primary) !important;
277
+ font-weight: 600 !important;
278
+ font-size: 0.875rem !important; /* 14px */
279
+ margin-top: 0 !important;
280
+ margin-bottom: 1rem !important;
281
+ text-align: left !important;
282
+ }
283
+
284
+ .member-divider {
285
+ display: block;
286
+ border: 0;
287
+ border-top: 1px solid #f1f5f9 !important; /* slate-100 */
288
+ margin-top: 0.5rem !important;
289
+ margin-bottom: 0.75rem !important;
290
+ }
291
+
292
+ .member-bio {
293
+ display: block;
294
+ font-size: 0.875rem !important; /* 14px */
295
+ line-height: 1.6 !important;
296
+ color: #475569 !important; /* slate-600 */
297
+ margin-top: 0 !important;
298
+ margin-bottom: 1.5rem !important;
299
+ text-align: left !important;
300
+ flex-grow: 1;
301
+ }
302
+
303
+ .member-footer {
304
+ display: flex;
305
+ flex-direction: column;
306
+ gap: 1rem;
307
+ margin-top: auto !important;
308
+ }
309
+
310
+ .social-links-row {
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: flex-start; /* Aligned to left */
314
+ gap: 0.5rem;
315
+ }
316
+
317
+ .social-link-btn {
318
+ width: 2.25rem;
319
+ height: 2.25rem;
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: center;
323
+ border-radius: 9999px;
324
+ color: #94a3b8; /* slate-400 */
325
+ transition: all 0.15s ease;
326
+ margin-left: -0.5rem; /* subtle offset alignment for first icon */
327
+ }
328
+
329
+ .social-link-btn:hover {
330
+ color: var(--primary);
331
+ background-color: var(--surface-container-high);
332
+ }
333
+ </style>
@@ -0,0 +1,79 @@
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(".section-display-title").should(
9
+ "contain",
10
+ "What our customers are saying",
11
+ );
12
+
13
+ // Verify first testimonial quote
14
+ cy.get(".quote-text").should(
15
+ "contain",
16
+ "The VirES platform has completely revolutionized",
17
+ );
18
+ cy.get(".author-name").should("contain", "Dr. Fiona Harrison");
19
+
20
+ // Click next button
21
+ cy.get("button[aria-label='Next testimonial']").click();
22
+
23
+ // Verify second testimonial quote
24
+ cy.get(".quote-text").should(
25
+ "contain",
26
+ "Deploying our Area Monitoring System",
27
+ );
28
+ cy.get(".author-name").should("contain", "Dr. Liam O'Connor");
29
+
30
+ // Click prev button
31
+ cy.get("button[aria-label='Previous testimonial']").click();
32
+
33
+ // Verify first testimonial quote again
34
+ cy.get(".quote-text").should(
35
+ "contain",
36
+ "The VirES platform has completely revolutionized",
37
+ );
38
+ cy.get(".author-name").should("contain", "Dr. Fiona Harrison");
39
+ });
40
+
41
+ it("renders custom title and custom testimonials list", () => {
42
+ const customTitle = "What Our Amazing Clients Think";
43
+ const customTestimonials = [
44
+ {
45
+ quote: "This is a custom quote that is extremely positive.",
46
+ author: "Alice Smith",
47
+ role: "Software Architect",
48
+ },
49
+ {
50
+ quote: "Another custom quote for testing purposes.",
51
+ author: "Bob Jones",
52
+ role: "Data Analyst",
53
+ },
54
+ ];
55
+
56
+ cy.mount(TestimonialsSection, {
57
+ props: {
58
+ title: customTitle,
59
+ testimonials: customTestimonials,
60
+ },
61
+ });
62
+
63
+ // Check title
64
+ cy.get(".section-display-title").should("contain", customTitle);
65
+
66
+ // Check first custom testimonial
67
+ cy.get(".quote-text").should("contain", "This is a custom quote");
68
+ cy.get(".author-name").should("contain", "Alice Smith");
69
+ cy.get(".author-role").should("contain", "Software Architect");
70
+
71
+ // Go next
72
+ cy.get("button[aria-label='Next testimonial']").click();
73
+
74
+ // Check second custom testimonial
75
+ cy.get(".quote-text").should("contain", "Another custom quote");
76
+ cy.get(".author-name").should("contain", "Bob Jones");
77
+ cy.get(".author-role").should("contain", "Data Analyst");
78
+ });
79
+ });