@markuxt/markuxt 0.1.4

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 (94) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +168 -0
  3. package/app.config.d.ts +33 -0
  4. package/nuxt.config.ts +170 -0
  5. package/package.json +43 -0
  6. package/src/components/AppFooter.vue +225 -0
  7. package/src/components/AppHeader.vue +342 -0
  8. package/src/components/Hero.vue +438 -0
  9. package/src/components/Icon.vue +131 -0
  10. package/src/components/LanguageSwitcher.vue +71 -0
  11. package/src/components/MemberCard.vue +198 -0
  12. package/src/components/MembersGrid.vue +129 -0
  13. package/src/components/MermaidDiagram.vue +99 -0
  14. package/src/components/NewsCard.vue +119 -0
  15. package/src/components/PublicationCard.vue +245 -0
  16. package/src/components/SectionTitle.vue +75 -0
  17. package/src/components/content/ProseImg.vue +29 -0
  18. package/src/components/content/ProsePre.vue +58 -0
  19. package/src/components/content/ProseVideo.vue +45 -0
  20. package/src/composables/resolveContentImage.ts +35 -0
  21. package/src/content-transformers/binary-assets.ts +20 -0
  22. package/src/error.vue +58 -0
  23. package/src/layouts/default.vue +37 -0
  24. package/src/middleware/navigation-guard.ts +22 -0
  25. package/src/pages/index.vue +232 -0
  26. package/src/pages/members/[...slug].vue +542 -0
  27. package/src/pages/members/index.vue +147 -0
  28. package/src/pages/news/[...slug].vue +280 -0
  29. package/src/pages/news/index.vue +102 -0
  30. package/src/pages/positions/[...slug].vue +425 -0
  31. package/src/pages/positions/index.vue +266 -0
  32. package/src/pages/projects/[...slug].vue +441 -0
  33. package/src/pages/projects/index.vue +499 -0
  34. package/src/pages/publications/[...slug].vue +435 -0
  35. package/src/pages/publications/index.vue +145 -0
  36. package/src/plugins/mathml-components.ts +33 -0
  37. package/src/plugins/suppress-warnings.ts +40 -0
  38. package/src/public/_markuxt/components/AppFooter.vue +225 -0
  39. package/src/public/_markuxt/components/AppHeader.vue +342 -0
  40. package/src/public/_markuxt/components/Hero.vue +430 -0
  41. package/src/public/_markuxt/components/Icon.vue +131 -0
  42. package/src/public/_markuxt/components/LanguageSwitcher.vue +71 -0
  43. package/src/public/_markuxt/components/MemberCard.vue +198 -0
  44. package/src/public/_markuxt/components/MembersGrid.vue +129 -0
  45. package/src/public/_markuxt/components/MermaidDiagram.vue +99 -0
  46. package/src/public/_markuxt/components/NewsCard.vue +119 -0
  47. package/src/public/_markuxt/components/PublicationCard.vue +245 -0
  48. package/src/public/_markuxt/components/SectionTitle.vue +75 -0
  49. package/src/public/_markuxt/components/content/ProseImg.vue +29 -0
  50. package/src/public/_markuxt/components/content/ProsePre.vue +58 -0
  51. package/src/public/_markuxt/components/content/ProseVideo.vue +45 -0
  52. package/src/public/_markuxt/composables/resolveContentImage.ts +35 -0
  53. package/src/public/_markuxt/content-transformers/binary-assets.ts +20 -0
  54. package/src/public/_markuxt/error.vue +58 -0
  55. package/src/public/_markuxt/layouts/default.vue +37 -0
  56. package/src/public/_markuxt/middleware/navigation-guard.ts +22 -0
  57. package/src/public/_markuxt/pages/index.vue +232 -0
  58. package/src/public/_markuxt/pages/members/[...slug].vue +542 -0
  59. package/src/public/_markuxt/pages/members/index.vue +147 -0
  60. package/src/public/_markuxt/pages/news/[...slug].vue +280 -0
  61. package/src/public/_markuxt/pages/news/index.vue +102 -0
  62. package/src/public/_markuxt/pages/positions/[...slug].vue +425 -0
  63. package/src/public/_markuxt/pages/positions/index.vue +266 -0
  64. package/src/public/_markuxt/pages/projects/[...slug].vue +441 -0
  65. package/src/public/_markuxt/pages/projects/index.vue +499 -0
  66. package/src/public/_markuxt/pages/publications/[...slug].vue +435 -0
  67. package/src/public/_markuxt/pages/publications/index.vue +145 -0
  68. package/src/public/_markuxt/plugins/mathml-components.ts +33 -0
  69. package/src/public/_markuxt/plugins/suppress-warnings.ts +40 -0
  70. package/src/public/_markuxt/server/plugins/content-locale.ts +47 -0
  71. package/src/public/_markuxt/server/plugins/fix-content-anchors.ts +63 -0
  72. package/src/public/_markuxt/styles/_animations.css +99 -0
  73. package/src/public/_markuxt/styles/_base.css +31 -0
  74. package/src/public/_markuxt/styles/_code.css +109 -0
  75. package/src/public/_markuxt/styles/_components.css +109 -0
  76. package/src/public/_markuxt/styles/_layout.css +220 -0
  77. package/src/public/_markuxt/styles/_markdown.css +52 -0
  78. package/src/public/_markuxt/styles/_tokens.css +62 -0
  79. package/src/public/_markuxt/styles/_typography.css +144 -0
  80. package/src/public/_markuxt/styles/_utilities.css +110 -0
  81. package/src/public/_markuxt/styles/main.css +784 -0
  82. package/src/public/images/logo.png +0 -0
  83. package/src/server/plugins/content-locale.ts +47 -0
  84. package/src/server/plugins/fix-content-anchors.ts +63 -0
  85. package/src/styles/_animations.css +99 -0
  86. package/src/styles/_base.css +31 -0
  87. package/src/styles/_code.css +109 -0
  88. package/src/styles/_components.css +109 -0
  89. package/src/styles/_layout.css +220 -0
  90. package/src/styles/_markdown.css +52 -0
  91. package/src/styles/_tokens.css +62 -0
  92. package/src/styles/_typography.css +144 -0
  93. package/src/styles/_utilities.css +110 -0
  94. package/src/styles/main.css +784 -0
@@ -0,0 +1,542 @@
1
+ <template>
2
+ <main class="member-profile-page" v-if="member">
3
+ <!-- Decorative background -->
4
+ <div class="member-profile__bg">
5
+ <div class="member-profile__pattern"></div>
6
+ <div class="member-profile__shapes">
7
+ <span class="member-profile__shape member-profile__shape--1"></span>
8
+ <span class="member-profile__shape member-profile__shape--2"></span>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="container profile-page__container">
13
+ <!-- Back button -->
14
+ <NuxtLink to="/members" class="member-profile__back">
15
+ <ArrowLeft class="icon-inline" theme="outline" :size="16" fill="currentColor" :stroke-width="2" />
16
+ {{ t('members.backTo') }}
17
+ </NuxtLink>
18
+
19
+ <!-- Profile Header -->
20
+ <div class="profile-header animate-fade-in-up">
21
+ <div class="profile-header__image-wrapper">
22
+ <div class="profile-header__image-inner">
23
+ <img
24
+ v-if="memberImage"
25
+ :src="memberImage"
26
+ :alt="member.name"
27
+ class="profile-header__image"
28
+ />
29
+ <div class="profile-header__actions">
30
+ <a v-if="member.email" :href="`mailto:${member.email}`" class="profile-header__action" :aria-label="t('members.email')"> <Mail class="icon-inline" theme="outline" :size="18" fill="currentColor" :stroke-width="3" />
31
+ </a>
32
+ <a v-if="member.scholar" :href="member.scholar" target="_blank" rel="noopener" class="profile-header__action" :aria-label="t('members.googleScholar')">
33
+ <Google class="icon-inline" theme="outline" :size="18" fill="currentColor" :stroke-width="2.5" />
34
+ </a>
35
+ </div>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="profile-header__info">
40
+ <span class="profile-header__badge">{{ member.category || t('members.teamMember') }}</span>
41
+ <h1 class="profile-header__name">{{ member.name }}</h1>
42
+ <p class="profile-header__title">{{ member.title || member.role }}</p>
43
+ </div>
44
+ </div>
45
+
46
+ <!-- Content Sections -->
47
+ <div class="profile-content">
48
+ <!-- Research Interests -->
49
+ <div v-if="member.interests && member.interests.length" class="profile-section animate-fade-in-up delay-200">
50
+ <div class="profile-section__header">
51
+ <Search class="icon-inline" theme="outline" :size="22" fill="white" :stroke-width="2.8" />
52
+ <h3>{{ t('members.researchFocus') }}</h3>
53
+ </div>
54
+ <div class="profile-section__body">
55
+ <div class="interest-tags">
56
+ <span v-for="interest in member.interests" :key="interest" class="interest-tag">
57
+ {{ interest }}
58
+ </span>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- About/Bio with Markdown Content -->
64
+ <div v-if="member.body || (member.content && member.content.length > 0)" class="profile-section animate-fade-in-up delay-300">
65
+ <div class="profile-section__header">
66
+ <FileStaff class="icon-inline" theme="outline" :size="22" fill="white" :stroke-width="2.8" />
67
+ <h3>{{ t('members.about') }}</h3>
68
+ </div>
69
+ <div class="profile-section__body profile-section__body--content">
70
+ <ContentRenderer :value="member" />
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </main>
76
+
77
+ <!-- Not Found -->
78
+ <div v-else class="not-found-page">
79
+ <div class="container">
80
+ <div class="not-found">
81
+ <Help class="icon-inline" theme="outline" :size="80" fill="var(--color-accent)" :stroke-width="3" />
82
+ <h1>{{ t('members.notFound') }}</h1>
83
+ <p>{{ t('members.notFoundDesc') }}</p>
84
+ <NuxtLink to="/members" class="btn btn-primary">{{ t('members.browseAll') }}</NuxtLink>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </template>
89
+
90
+ <script setup lang="ts">
91
+ import ArrowLeft from '@icon-park/vue-next/es/icons/ArrowLeft'
92
+ import Mail from '@icon-park/vue-next/es/icons/Mail'
93
+ import Google from '@icon-park/vue-next/es/icons/Google'
94
+ import Search from '@icon-park/vue-next/es/icons/Search'
95
+ import FileStaff from '@icon-park/vue-next/es/icons/FileStaff'
96
+ import Help from '@icon-park/vue-next/es/icons/Help'
97
+
98
+ const { t } = useI18n()
99
+ const route = useRoute()
100
+ const config = useRuntimeConfig()
101
+
102
+ // Get member by file path
103
+ // For catch-all route [...slug], params.slug is an array like ['staff', 'salman-ijaz']
104
+ const slug = computed(() => {
105
+ const slugParam = route.params.slug
106
+ return Array.isArray(slugParam) ? slugParam.join('/') : slugParam
107
+ })
108
+
109
+ const { data: memberData } = await useAsyncData(`member-${slug.value}`, async () => {
110
+ try {
111
+ const fullPath = `/members/${slug.value}`
112
+ return await queryContent(fullPath).findOne()
113
+ } catch (e) {
114
+ console.error('Error fetching member:', e)
115
+ return null
116
+ }
117
+ }, {
118
+ watch: [slug]
119
+ })
120
+
121
+ const member = computed(() => memberData.value)
122
+
123
+ // Provide content ID for ProseImg/ProseVideo to resolve relative asset paths
124
+ provide('contentId', computed(() => member.value?._id || ''))
125
+
126
+ const memberImage = computed(() => {
127
+ const resolved = resolveContentImage(member.value?.image, member.value?._id)
128
+ if (!resolved) return ''
129
+ const basePath = config.app.baseURL || ''
130
+ if (!basePath || basePath === '/') return resolved
131
+ return basePath + resolved
132
+ })
133
+
134
+ useHead({
135
+ title: computed(() => member.value ? `${member.value.name} - ${t('site.shortName')}` : 'Member Not Found'),
136
+ meta: computed(() => {
137
+ const description = member.value?.body || member.value?.content
138
+ ? (typeof (member.value.body || member.value.content) === 'string'
139
+ ? (member.value.body || member.value.content).substring(0, 160).replace(/<[^>]*>/g, '')
140
+ : `${t('site.shortName')} member`)
141
+ : `${t('site.shortName')} member page`
142
+ return [
143
+ { name: 'description', content: description }
144
+ ]
145
+ })
146
+ })
147
+ </script>
148
+
149
+ <style scoped>
150
+ .member-profile-page {
151
+ min-height: 100vh;
152
+ position: relative;
153
+ }
154
+
155
+ /* Page Container with top padding for fixed header */
156
+ .profile-page__container {
157
+ padding-top: 100px;
158
+ }
159
+
160
+ /* Background */
161
+ .member-profile__bg {
162
+ position: fixed;
163
+ inset: 0;
164
+ background: linear-gradient(180deg, var(--color-bg-alt) 0%, var(--color-bg) 100%);
165
+ z-index: -2;
166
+ pointer-events: none;
167
+ }
168
+
169
+ .member-profile__pattern {
170
+ position: absolute;
171
+ inset: 0;
172
+ background-image:
173
+ linear-gradient(rgba(10, 37, 64, 0.03) 1px, transparent 1px),
174
+ linear-gradient(90deg, rgba(10, 37, 64, 0.03) 1px, transparent 1px);
175
+ background-size: 40px 40px;
176
+ }
177
+
178
+ .member-profile__shapes {
179
+ position: absolute;
180
+ inset: 0;
181
+ overflow: hidden;
182
+ }
183
+
184
+ .member-profile__shape {
185
+ position: absolute;
186
+ border-radius: 50%;
187
+ animation: float 8s ease-in-out infinite;
188
+ }
189
+
190
+ .member-profile__shape--1 {
191
+ width: 200px;
192
+ height: 200px;
193
+ top: 10%;
194
+ right: 8%;
195
+ background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-secondary) 100%);
196
+ opacity: 0.12;
197
+ animation-delay: 0s;
198
+ }
199
+
200
+ .member-profile__shape--2 {
201
+ width: 120px;
202
+ height: 120px;
203
+ bottom: 20%;
204
+ left: 5%;
205
+ background: var(--color-primary);
206
+ opacity: 0.08;
207
+ animation-delay: 2.5s;
208
+ }
209
+
210
+ @keyframes float {
211
+ 0%, 100% {
212
+ transform: translateY(0) rotate(0deg);
213
+ }
214
+ 50% {
215
+ transform: translateY(-20px) rotate(3deg);
216
+ }
217
+ }
218
+
219
+ /* Back Button */
220
+ .member-profile__back {
221
+ display: inline-flex;
222
+ align-items: center;
223
+ gap: var(--spacing-sm);
224
+ padding: var(--spacing-sm) var(--spacing-md);
225
+ background: rgba(255, 255, 255, 0.9);
226
+ border: 1px solid var(--color-border);
227
+ border-radius: var(--radius-full);
228
+ color: var(--color-text);
229
+ font-size: 0.875rem;
230
+ font-weight: 500;
231
+ text-decoration: none;
232
+ transition: all var(--transition-base);
233
+ margin-bottom: var(--spacing-xl);
234
+ }
235
+
236
+ .member-profile__back:hover {
237
+ background: var(--color-primary);
238
+ border-color: var(--color-primary);
239
+ color: white;
240
+ transform: translateX(-3px);
241
+ }
242
+
243
+ /* Profile Header */
244
+ .profile-header {
245
+ display: grid;
246
+ grid-template-columns: 160px 1fr;
247
+ gap: var(--spacing-2xl);
248
+ align-items: center;
249
+ margin-bottom: var(--spacing-3xl);
250
+ }
251
+
252
+ .profile-header__image-wrapper {
253
+ position: relative;
254
+ }
255
+
256
+ .profile-header__image-inner {
257
+ position: relative;
258
+ width: 180px;
259
+ height: auto;
260
+ max-width: 180px;
261
+ max-height: 240px;
262
+ }
263
+
264
+ .profile-header__image {
265
+ width: 100%;
266
+ height: 100%;
267
+ object-fit: contain;
268
+ object-position: center top;
269
+ border-radius: var(--radius-lg);
270
+ box-shadow: var(--shadow-lg);
271
+ }
272
+
273
+ .profile-header__actions {
274
+ position: absolute;
275
+ bottom: 15px;
276
+ right: 10px;
277
+ display: flex;
278
+ gap: var(--spacing-xs);
279
+ }
280
+
281
+ .profile-header__action {
282
+ display: flex;
283
+ align-items: center;
284
+ justify-content: center;
285
+ width: 36px;
286
+ height: 36px;
287
+ background: white;
288
+ border-radius: var(--radius-full);
289
+ color: var(--color-primary);
290
+ transition: all var(--transition-fast);
291
+ box-shadow: var(--shadow-md);
292
+ }
293
+
294
+ .profile-header__action:hover {
295
+ background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-secondary) 100%);
296
+ color: white;
297
+ transform: scale(1.1);
298
+ box-shadow: var(--shadow-lg);
299
+ }
300
+
301
+ .profile-header__info {
302
+ display: flex;
303
+ flex-direction: column;
304
+ gap: var(--spacing-sm);
305
+ }
306
+
307
+ .profile-header__badge {
308
+ display: inline-flex;
309
+ align-items: center;
310
+ padding: var(--spacing-xs) var(--spacing-sm);
311
+ font-size: 0.7rem;
312
+ font-weight: 700;
313
+ letter-spacing: 0.08em;
314
+ text-transform: uppercase;
315
+ color: white;
316
+ background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-secondary) 100%);
317
+ border-radius: var(--radius-sm);
318
+ box-shadow: 0 4px 12px rgba(0, 217, 255, 0.3);
319
+ }
320
+
321
+ .profile-header__name {
322
+ font-family: var(--font-display);
323
+ font-size: clamp(1.75rem, 4vw, 2.25rem);
324
+ font-weight: 800;
325
+ line-height: 1.15;
326
+ color: var(--color-primary);
327
+ margin: 0;
328
+ }
329
+
330
+ .profile-header__title {
331
+ font-size: 0.9375rem;
332
+ font-weight: 500;
333
+ color: var(--color-text-muted);
334
+ }
335
+
336
+ /* Content Area */
337
+ .profile-content {
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: var(--spacing-xl);
341
+ margin-bottom: var(--spacing-3xl);
342
+ }
343
+
344
+ /* Profile Sections */
345
+ .profile-section {
346
+ background: var(--color-bg-alt);
347
+ border-radius: var(--radius-xl);
348
+ border: 1px solid var(--color-border);
349
+ box-shadow: var(--shadow-md);
350
+ overflow: hidden;
351
+ transition: all var(--transition-base);
352
+ }
353
+
354
+ .profile-section:hover {
355
+ box-shadow: var(--shadow-lg);
356
+ border-color: var(--color-secondary);
357
+ }
358
+
359
+ .profile-section__header {
360
+ display: flex;
361
+ align-items: center;
362
+ gap: var(--spacing-sm);
363
+ padding: var(--spacing-lg) var(--spacing-xl);
364
+ background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-light) 100%);
365
+ border-bottom: 1px solid var(--color-border);
366
+ }
367
+
368
+ .profile-section__icon {
369
+ width: 22px;
370
+ height: 22px;
371
+ color: var(--color-accent);
372
+ }
373
+
374
+ .profile-section__header h3 {
375
+ font-family: var(--font-display);
376
+ font-size: 1.125rem;
377
+ font-weight: 600;
378
+ color: white;
379
+ margin: 0;
380
+ }
381
+
382
+ .profile-section__body {
383
+ padding: var(--spacing-xl);
384
+ }
385
+
386
+ .profile-section__body--content {
387
+ padding: var(--spacing-xl);
388
+ background: white;
389
+ }
390
+
391
+ /* Interest Tags */
392
+ .interest-tags {
393
+ display: flex;
394
+ flex-wrap: wrap;
395
+ gap: var(--spacing-sm);
396
+ }
397
+
398
+ .interest-tag {
399
+ display: inline-flex;
400
+ align-items: center;
401
+ padding: var(--spacing-xs) var(--spacing-md);
402
+ font-size: 0.875rem;
403
+ font-weight: 500;
404
+ color: var(--color-text);
405
+ background: var(--color-bg);
406
+ border: 1px solid var(--color-border);
407
+ border-radius: var(--radius-lg);
408
+ transition: all var(--transition-fast);
409
+ }
410
+
411
+ .interest-tag:hover {
412
+ background: var(--color-secondary);
413
+ border-color: var(--color-secondary);
414
+ color: white;
415
+ transform: translateY(-2px);
416
+ box-shadow: var(--shadow-sm);
417
+ }
418
+
419
+ /* ContentRenderer Markdown Styling */
420
+ .profile-section__body--content :deep(h2) {
421
+ font-family: var(--font-display);
422
+ font-size: 1.5rem;
423
+ font-weight: 700;
424
+ color: var(--color-primary);
425
+ margin-top: var(--spacing-lg);
426
+ margin-bottom: var(--spacing-md);
427
+ }
428
+
429
+ .profile-section__body--content :deep(h3) {
430
+ font-family: var(--font-display);
431
+ font-size: 1.25rem;
432
+ font-weight: 600;
433
+ color: var(--color-primary);
434
+ margin-top: var(--spacing-md);
435
+ margin-bottom: var(--spacing-sm);
436
+ }
437
+
438
+ .profile-section__body--content :deep(p) {
439
+ font-size: 1rem;
440
+ line-height: 1.8;
441
+ color: var(--color-text);
442
+ margin-bottom: var(--spacing-md);
443
+ }
444
+
445
+ .profile-section__body--content :deep(ul) {
446
+ list-style: none;
447
+ padding-left: 0;
448
+ }
449
+
450
+ .profile-section__body--content :deep(li) {
451
+ padding-left: var(--spacing-lg);
452
+ margin-bottom: var(--spacing-sm);
453
+ position: relative;
454
+ }
455
+
456
+ .profile-section__body--content :deep(li::before) {
457
+ content: '•';
458
+ position: absolute;
459
+ left: 0;
460
+ color: var(--color-accent);
461
+ font-weight: 700;
462
+ }
463
+
464
+ .profile-section__body--content :deep(a) {
465
+ color: var(--color-secondary);
466
+ text-decoration: none;
467
+ transition: color var(--transition-fast);
468
+ }
469
+
470
+ .profile-section__body--content :deep(a:hover) {
471
+ color: var(--color-accent);
472
+ }
473
+
474
+ .profile-section__body--content :deep(strong) {
475
+ font-weight: 600;
476
+ color: var(--color-primary);
477
+ }
478
+
479
+ /* Not Found */
480
+ .not-found-page {
481
+ min-height: 60vh;
482
+ display: flex;
483
+ align-items: center;
484
+ }
485
+
486
+ .not-found {
487
+ text-align: center;
488
+ padding: var(--spacing-4xl) 0;
489
+ max-width: 400px;
490
+ margin: 0 auto;
491
+ }
492
+
493
+ .not-found__icon {
494
+ width: 80px;
495
+ height: 80px;
496
+ margin: 0 auto var(--spacing-xl);
497
+ display: flex;
498
+ align-items: center;
499
+ justify-content: center;
500
+ font-family: var(--font-display);
501
+ font-size: 3rem;
502
+ font-weight: 800;
503
+ color: var(--color-accent);
504
+ background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-secondary) 100%);
505
+ border-radius: var(--radius-xl);
506
+ }
507
+
508
+ .not-found h1 {
509
+ font-family: var(--font-display);
510
+ font-size: clamp(2rem, 4vw, 2.5rem);
511
+ font-weight: 700;
512
+ color: var(--color-primary);
513
+ margin-bottom: var(--spacing-md);
514
+ }
515
+
516
+ .not-found p {
517
+ font-size: 1rem;
518
+ color: var(--color-text-muted);
519
+ margin-bottom: var(--spacing-xl);
520
+ }
521
+
522
+ /* Responsive */
523
+ @media (max-width: 768px) {
524
+ .profile-header {
525
+ grid-template-columns: 1fr;
526
+ text-align: center;
527
+ }
528
+
529
+ .profile-header__image-wrapper {
530
+ margin: 0 auto;
531
+ }
532
+
533
+ .profile-header__actions {
534
+ right: 50%;
535
+ transform: translateX(50%);
536
+ }
537
+
538
+ .profile-content {
539
+ gap: var(--spacing-lg);
540
+ }
541
+ }
542
+ </style>
@@ -0,0 +1,147 @@
1
+ <template>
2
+ <div class="members-page">
3
+ <div class="section">
4
+ <div class="container">
5
+ <SectionTitle
6
+ :overline="t('home.ourTeam')"
7
+ :title="t('members.section')"
8
+ :description="t('home.teamDescription')"
9
+ />
10
+
11
+ <!-- Category Filter -->
12
+ <div class="members-filter" v-if="categories.length > 1">
13
+ <button
14
+ v-for="category in categories"
15
+ :key="category.key"
16
+ class="filter-btn"
17
+ :class="{ 'filter-btn--active': activeCategory === category.key }"
18
+ @click="activeCategory = category.key"
19
+ >
20
+ {{ category.name }}
21
+ </button>
22
+ </div>
23
+
24
+ <!-- Members Grid -->
25
+ <MembersGrid
26
+ v-if="filteredMembers.length > 0"
27
+ :members="filteredMembers"
28
+ :groupBy="false"
29
+ />
30
+ <p v-else class="no-results">{{ t('members.noResults') }}</p>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ interface Member {
38
+ name: string
39
+ role?: string
40
+ title?: string
41
+ email?: string
42
+ scholar?: string
43
+ image?: string
44
+ interests?: string[]
45
+ category?: string
46
+ order?: number
47
+ _path?: string
48
+ slug: string
49
+ }
50
+
51
+ const { t } = useI18n()
52
+
53
+ // Fetch all members
54
+ const { data: allMembers } = await useAsyncData('members', () =>
55
+ queryContent('/members')
56
+ .where({ _hidden: { $ne: true } })
57
+ .where({ _extension: 'md' }).find()
58
+ )
59
+
60
+ const processedMembers = computed(() => {
61
+ const members = (allMembers.value || []).map(member => {
62
+ const processed = {
63
+ ...member,
64
+ name: member.name || member.title || t('members.unknown'),
65
+ category: member.category ?? undefined, // Ensure 'category' exists
66
+ slug: member._id || member._path || '' // Ensure 'slug' exists
67
+ }
68
+ return processed
69
+ })
70
+ return members
71
+ })
72
+
73
+ // Category definitions - Staff first, All Members last
74
+ const categories = computed(() => [
75
+ { key: 'staff', name: t('members.staff') },
76
+ { key: 'research-students', name: t('members.researchStudents') },
77
+ { key: 'research-assistants', name: t('members.researchAssistants') },
78
+ { key: 'alumni', name: t('members.alumni') },
79
+ { key: 'all', name: t('members.allMembers') }
80
+ ])
81
+
82
+ const activeCategory = ref('staff')
83
+
84
+ const filteredMembers = computed(() => {
85
+ if (activeCategory.value === 'all') {
86
+ return processedMembers.value
87
+ }
88
+ return processedMembers.value.filter(m => m.category === activeCategory.value)
89
+ })
90
+
91
+ useHead({
92
+ title: t('members.pageTitle'),
93
+ meta: [
94
+ { name: 'description', content: t('members.pageDescription') }
95
+ ]
96
+ })
97
+ </script>
98
+
99
+ <style scoped>
100
+ .members-page {
101
+ padding-top: var(--spacing-xl);
102
+ }
103
+
104
+ .members-filter {
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ gap: var(--spacing-sm);
108
+ margin-bottom: var(--spacing-2xl);
109
+ }
110
+
111
+ .filter-btn {
112
+ padding: var(--spacing-sm) var(--spacing-md);
113
+ font-family: var(--font-body);
114
+ font-size: 0.875rem;
115
+ font-weight: 500;
116
+ color: var(--color-text-muted);
117
+ background: var(--color-bg-alt);
118
+ border: 1px solid var(--color-border);
119
+ border-radius: var(--radius-full);
120
+ cursor: pointer;
121
+ transition: all var(--transition-fast);
122
+ }
123
+
124
+ .filter-btn:hover {
125
+ border-color: var(--color-secondary);
126
+ color: var(--color-secondary);
127
+ }
128
+
129
+ .filter-btn--active {
130
+ background: var(--color-secondary);
131
+ border-color: var(--color-secondary);
132
+ color: white;
133
+ }
134
+
135
+ .filter-btn--active:hover {
136
+ background: var(--color-primary);
137
+ border-color: var(--color-primary);
138
+ color: white;
139
+ }
140
+
141
+ .no-results {
142
+ text-align: center;
143
+ padding: var(--spacing-3xl);
144
+ font-size: 1.125rem;
145
+ color: var(--color-text-muted);
146
+ }
147
+ </style>