feat(team-showcase): polish sorting, layout, and docs for labs/startups

- default to last-name sort with numeric-friendly keys for weight/graduation_year
- support per-group sorting, optional empty-state, and role fallback to affiliation
- add grid/interest caps, alignment controls, and avatar initial placeholders
- expose graduation_year/weight/name_family_pref in profiles; update schema/README
This commit is contained in:
George Cushen committed 2026-01-07 16:04:26 +00:00
1 parent 4a758fb82e
commit 8ca8e40a8f
4 files changed
+276 -65

No files matched your search

+46 -22
View File
@@ -8,7 +8,7 @@ Display your team members with a beautiful, responsive grid layout featuring ric
- **User Groups**: Organize team members by role (e.g., Principal Investigators, PhD Students)
- **Rich Profiles**: Display avatar, name, role, bio, interests, and social links
- **Customizable Display**: Toggle visibility of role, organization, interests, and social links
- **Sorting**: Sort team members by last name or custom field
- **Sorting**: Sort by last name, graduation year, weight, or any author field (no `Params.` prefix needed); per-group sorting supported
- **Hover Effects**: Smooth image zoom and shadow effects on hover
- **Call to Action**: Optional button to recruitment or team page
@@ -25,53 +25,55 @@ sections:
- Principal Investigators
- Postdoctoral Researchers
- PhD Students
sort_by: 'Params.last_name'
sort_ascending: true
- name: Alumni # optional per-group sort override
sort_by: graduation_year
sort_ascending: false
sort_by: 'graduation_year' # legacy 'Params.' prefix optional
sort_ascending: false
cta:
text: Join Our Team
url: /opportunities
icon: user-plus
design:
show_role: true
show_organizations: false
show_organizations: true
show_interests: true
max_interests: 3 # set 0 to hide interests even if provided
align: center # or "left" to align header + CTA left
max_columns: 4 # 2, 3, or 4
show_social: true
show_empty_groups: false # show a placeholder when a group has no members
# Section background color (CSS class)
css_class: "bg-gray-50 dark:bg-gray-900"
```
## Content Structure
Team members should be created in the `content/authors/` directory. Each team member needs:
Team members are defined in `data/authors/<slug>.yaml`. All fields in the data file can be sorted on directly—no `Params.` prefix is required.
```yaml
# content/authors/jane-doe/_index.md
---
title: Dr. Jane Doe
# data/authors/jane-doe.yaml
name:
display: Dr. Jane Doe
role: Postdoctoral Researcher
bio: Research interests include machine learning and computational biology.
interests:
- Machine Learning
- Computational Biology
- Data Science
organizations:
affiliations:
- name: University of Excellence
url: https://example.edu
social:
links:
- icon: envelope
icon_pack: fas
link: 'mailto:jane@example.edu'
url: 'mailto:jane@example.edu'
- icon: twitter
icon_pack: fab
link: https://x.com/janedoe
url: https://x.com/janedoe
- icon: github
icon_pack: fab
link: https://github.com/janedoe
url: https://github.com/janedoe
user_groups:
- Postdoctoral Researchers
---
Full biography content here...
graduation_year: 2024
```
## Options
@@ -81,17 +83,39 @@ Full biography content here...
- **title**: Main heading for the team section
- **subtitle**: Optional subtitle
- **text**: Optional description text (supports Markdown)
- **user_groups**: Array of group names to display
- **sort_by**: Field to sort by (default: "Params.last_name")
- **user_groups**: Array of group names to display, or objects `{ name, sort_by?, sort_ascending? }` for per-group sorting
- **sort_by**: Field to sort by (default: "name_family", falling back to the last word of `title`). Use plain field names from `data/authors`—the legacy `Params.` prefix is optional for backwards compatibility. Recommended: `name_family` (last name), `graduation_year` (alumni), `weight` (manual order).
- **sort_ascending**: Sort direction (default: true)
- **cta**: Optional call-to-action button with text, url, and icon
### Design Options
- **show_role**: Display team member roles (default: true)
- **show_organizations**: Display affiliated organizations (default: false)
- **show_organizations**: Display affiliated organizations (default: true)
- **show_interests**: Display research interests (default: false)
- **max_interests**: Maximum number of interests to show (default: 3; 0 hides interests)
- **show_social**: Display social media links (default: true)
- **align**: Align header and CTA (`center` or `left`, default `center`)
- **max_columns**: Limit grid columns on wide screens (2, 3, or 4; default 4)
- **show_empty_groups**: If true, render an empty-state message for groups with no members
## Recipes
- **Lab alumni list**: `user_groups: ['Alumni']`, `sort_by: graduation_year`, `sort_ascending: false`, set `graduation_year` in each author file.
- **Startup leadership-first**: set `weight` on leadership (e.g., 1–10), `sort_by: weight`, `sort_ascending: true`, leave others empty or higher.
- **Classic last-name sort**: ensure `name.family` is set; leave defaults or set `sort_by: name_family`.
- **Hide interests but keep data**: `show_interests: true`, `max_interests: 0`.
- **Left-aligned hero style**: `design.align: left` (header + CTA).
- **Per-group sorting**: mix strings and objects in `user_groups`, e.g. `- name: Alumni; sort_by: graduation_year; sort_ascending: false`.
## Best Practices
- Keep `data/authors/<slug>.yaml` tidy: set `name.display`, `name.family`, `role`, short `bio`, `affiliations` (first shown), `links` with icons (`github`, `linkedin`, `orcid`, `google-scholar`), and `user_groups`.
- Use square WebP avatars in `assets/media/authors/<slug>.*`, ~400×400px, optimized (<200 KB). Names center-cropped.
- Prefer high-signal interests (3 or fewer); longer lists belong on the profile page.
- For credibility, show organizations (default on) and keep roles concise.
- Review people data each semester/quarter to keep roles and photos fresh.
- Use valid icon names for `links.icon` (e.g., `github`, `linkedin`, `orcid`, `google-scholar`, `twitter`) to avoid broken social icons.
## Styling
+166 -39
View File
@@ -8,16 +8,38 @@
{{ $show_social := $block.design.show_social | default true }}
{{ $show_interests := $block.design.show_interests | default false }}
{{ $show_role := $block.design.show_role | default true }}
{{ $show_organizations := $block.design.show_organizations | default false }}
{{ $show_organizations := $block.design.show_organizations | default true }}
{{ $max_interests := $block.design.max_interests | default 3 }}
{{ $max_interests_int := int $max_interests }}
{{ $align := $block.design.align | default "center" }}
{{ $max_columns := $block.design.max_columns | default 4 }}
{{ $show_empty_groups := $block.design.show_empty_groups | default false }}
{{ $sort_field := $block.content.sort_by | default "name_family" }}
{{ $sort_asc := $block.content.sort_ascending | default true }}
{{ $sort_field_lc := lower $sort_field }}
{{ if hasPrefix $sort_field_lc "params." }}
{{/* Backwards compatibility with Params.* used when authors were pages */}}
{{ $sort_field = substr $sort_field 7 }}
{{ $sort_field_lc = lower $sort_field }}
{{ end }}
{{ $title := $block.content.title | emojify | $page.RenderString }}
{{ $subtitle := $block.content.subtitle | emojify | $page.RenderString }}
{{ $text := $block.content.text | emojify | $page.RenderString }}
{{ $alignClass := cond (eq $align "left") "items-start text-left" "items-center text-center" }}
{{ $ctaAlignClass := cond (eq $align "left") "text-left" "text-center" }}
{{ $columnsClasses := dict
2 "grid grid-cols-1 gap-8 sm:grid-cols-2"
3 "grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3"
4 "grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
}}
{{ $max_columns_int := int $max_columns }}
{{ $gridClass := index $columnsClasses $max_columns_int | default "grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" }}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{{/* Section Header */}}
<div class="flex flex-col items-center text-center mb-12">
<div class="flex flex-col {{ $alignClass }} mb-12">
{{ with $title }}
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white tracking-tight">
{{ . }}
@@ -40,61 +62,126 @@
{{/* Build profiles from data/authors */}}
{{ $allProfiles := slice }}
{{ range $slug, $_ := site.Data.authors }}
{{ $allProfiles = $allProfiles | append (partial "functions/get_author_profile" (dict "slug" $slug)) }}
{{ $profile := partial "functions/get_author_profile" (dict "slug" $slug) }}
{{ $profileData := index site.Data.authors $slug }}
{{ $allProfiles = $allProfiles | append (dict "profile" $profile "profileData" $profileData) }}
{{ end }}
{{ $groups := $block.content.user_groups }}
{{ if $groups }}
{{ range $group := $groups }}
{{ range $groupItem := $groups }}
{{ $groupName := "" }}
{{ $groupSortField := $sort_field }}
{{ $groupSortAsc := $sort_asc }}
{{ $groupSortFieldLc := "" }}
{{ $itemIsMap := reflect.IsMap $groupItem }}
{{ if $itemIsMap }}
{{ $groupName = index $groupItem "name" | default (index $groupItem "title") | default "" }}
{{ $groupSortField = index $groupItem "sort_by" | default $groupSortField }}
{{ $groupSortAsc = index $groupItem "sort_ascending" | default $groupSortAsc }}
{{ else }}
{{ $groupName = $groupItem }}
{{ end }}
{{ $groupSortFieldLc = lower $groupSortField }}
{{ if hasPrefix $groupSortFieldLc "params." }}
{{ $groupSortField = substr $groupSortField 7 }}
{{ $groupSortFieldLc = lower $groupSortField }}
{{ end }}
{{ $groupIsNumeric := or (eq $groupSortFieldLc "weight") (eq $groupSortFieldLc "graduation_year") }}
{{ $groupOrder := cond (and (not $groupIsNumeric) (not $groupSortAsc)) "desc" "asc" }}
{{ $groupProfiles := slice }}
{{ range $allProfiles }}
{{ $u := .user_groups | default (slice) }}
{{ if gt (len (intersect $u (slice $group))) 0 }}
{{ $groupProfiles = $groupProfiles | append . }}
{{ $profile := .profile }}
{{ $u := $profile.user_groups | default (slice) }}
{{ if gt (len (intersect $u (slice $groupName))) 0 }}
{{ $profileData := .profileData }}
{{ $family := $profile.name_family_pref | default $profile.name_family | default (delimit (last 1 (split $profile.title " ")) "") | default $profile.title }}
{{ $familyLower := lower $family }}
{{ $raw := index $profile $groupSortField | default (index $profileData $groupSortField) }}
{{ $isNumeric := $groupIsNumeric }}
{{ $primary := "" }}
{{ if or (eq $groupSortFieldLc "name.family") (eq $groupSortFieldLc "family") (eq $groupSortFieldLc "last_name") (eq $groupSortFieldLc "name_family") (eq $groupSortFieldLc "family_name") }}
{{ $primary = $familyLower }}
{{ else if $isNumeric }}
{{ $primary = or (and (ne $raw nil) (int $raw)) 999999 }}
{{ else if ne $raw nil }}
{{ $primary = lower (printf "%v" $raw) }}
{{ else }}
{{ $primary = lower $profile.title }}
{{ end }}
{{ $sortKey := "" }}
{{ if $isNumeric }}
{{ $num := $primary | default 999999 }}
{{ if not $groupSortAsc }}{{ $num = sub 999999999 $num }}{{ end }}
{{ $sortKey = printf "%010d|%s" $num $familyLower }}
{{ else }}
{{ $sortKey = printf "%s|%s" $primary $familyLower }}
{{ end }}
{{ $groupProfiles = $groupProfiles | append (dict "profile" $profile "sort_val" $sortKey) }}
{{ end }}
{{ end }}
{{ $groupProfiles = sort $groupProfiles "title" }}
{{ $groupProfiles = sort $groupProfiles "sort_val" $groupOrder }}
{{ if and $groupProfiles (gt (len $groups) 1) }}
{{ if and (or (gt (len $groupProfiles) 0) $show_empty_groups) (gt (len $groups) 1) }}
<div class="col-span-full mb-4">
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-6 pb-2 border-b border-gray-200 dark:border-gray-700">
{{ $group | markdownify }}
{{ $groupName | markdownify }}
</h3>
</div>
{{ end }}
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 w-full">
{{ range $profile := $groupProfiles }}
{{ if or (gt (len $groupProfiles) 0) $show_empty_groups }}
<div class="{{ $gridClass }} w-full">
{{ if eq (len $groupProfiles) 0 }}
{{ if $show_empty_groups }}
<div class="text-gray-600 dark:text-gray-400">{{ i18n "no_entries" | default "No entries yet." }}</div>
{{ end }}
{{ end }}
{{ range $groupProfile := $groupProfiles }}
{{ $profile := $groupProfile.profile }}
{{ $avatar := $profile.avatar }}
{{ $link := printf "/authors/%s/" $profile.slug }}
{{ $initial := upper (substr ($profile.name_given | default $profile.title) 0 1) }}
<div class="group relative">
<div class="relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
{{ if $avatar }}
<a href="{{ $link }}" class="block relative h-64 overflow-hidden">
{{ $image := $avatar.Fill "400x400 Center webp" }}
<img src="{{ $image.RelPermalink }}"
alt="{{ $profile.title }}"
width="{{ $image.Width }}"
height="{{ $image.Height }}"
loading="lazy"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110">
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<a href="{{ $link }}" class="block relative h-64 overflow-hidden" aria-label="{{ $profile.title }}">
{{ if $avatar }}
{{ $image := $avatar.Fill "400x400 Center webp" }}
<img src="{{ $image.RelPermalink }}"
alt="{{ $profile.title }}"
width="{{ $image.Width }}"
height="{{ $image.Height }}"
loading="lazy"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110">
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{{ else }}
<div class="absolute inset-0 bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-4xl font-semibold text-gray-600 dark:text-gray-200">
{{ $initial }}
</div>
{{ end }}
</a>
{{ end }}
<div class="p-6">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-1">
<a href="{{ $link }}" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
<a href="{{ $link }}" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors" aria-label="{{ $profile.title }}">
{{ $profile.title }}
</a>
</h3>
{{ if $show_role }}
<div class="text-gray-600 dark:text-gray-400 mb-3">
{{ with $profile.role }}<p class="font-medium">{{ . }}</p>{{ end }}
{{ $roleText := "" }}
{{ with $profile.role }}{{ $roleText = . }}{{ end }}
{{ if and (not $roleText) $profile.affiliations }}
{{ range first 1 $profile.affiliations }}
{{ $roleText = .name }}
{{ end }}
{{ end }}
{{ with $roleText }}<p class="font-medium">{{ . }}</p>{{ end }}
{{ if and $show_organizations $profile.affiliations }}
{{ range first 1 $profile.affiliations }}
<p class="text-sm mt-1">{{ .name }}</p>
@@ -112,10 +199,12 @@
{{ if and $show_interests $profile.interests }}
<div class="mb-4">
<div class="flex flex-wrap gap-1">
{{ range first 3 $profile.interests }}
<span class="inline-block bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs px-2 py-1 rounded">
{{ . }}
</span>
{{ if gt $max_interests_int 0 }}
{{ range first $max_interests_int $profile.interests }}
<span class="inline-block bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs px-2 py-1 rounded">
{{ . }}
</span>
{{ end }}
{{ end }}
</div>
</div>
@@ -149,17 +238,49 @@
</div>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ else }}
{{ $allProfiles = sort $allProfiles "title" }}
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{{ range $profile := $allProfiles }}
{{ $isNumeric := or (eq $sort_field_lc "weight") (eq $sort_field_lc "graduation_year") }}
{{ $order := cond (and (not $isNumeric) (not $sort_asc)) "desc" "asc" }}
{{ $sorted := slice }}
{{ range $allProfiles }}
{{ $profile := .profile }}
{{ $profileData := .profileData }}
{{ $family := $profile.name_family_pref | default $profile.name_family | default (delimit (last 1 (split $profile.title " ")) "") | default $profile.title }}
{{ $familyLower := lower $family }}
{{ $raw := index $profile $sort_field | default (index $profileData $sort_field) }}
{{ $primary := "" }}
{{ if or (eq $sort_field_lc "name.family") (eq $sort_field_lc "family") (eq $sort_field_lc "last_name") (eq $sort_field_lc "name_family") (eq $sort_field_lc "family_name") }}
{{ $primary = $familyLower }}
{{ else if $isNumeric }}
{{ $primary = or (and (ne $raw nil) (int $raw)) 999999 }}
{{ else if ne $raw nil }}
{{ $primary = lower (printf "%v" $raw) }}
{{ else }}
{{ $primary = lower $profile.title }}
{{ end }}
{{ $sortKey := "" }}
{{ if $isNumeric }}
{{ $num := $primary | default 999999 }}
{{ if not $sort_asc }}{{ $num = sub 999999999 $num }}{{ end }}
{{ $sortKey = printf "%010d|%s" $num $familyLower }}
{{ else }}
{{ $sortKey = printf "%s|%s" $primary $familyLower }}
{{ end }}
{{ $sorted = $sorted | append (dict "profile" $profile "sort_val" $sortKey) }}
{{ end }}
{{ $sorted = sort $sorted "sort_val" $order }}
<div class="{{ $gridClass }}">
{{ range $sortedProfile := $sorted }}
{{ $profile := $sortedProfile.profile }}
{{ $avatar := $profile.avatar }}
{{ $link := printf "/authors/%s/" $profile.slug }}
{{ $initial := upper (substr ($profile.name_given | default $profile.title) 0 1) }}
<div class="group relative">
<div class="relative overflow-hidden rounded-2xl bg-white dark:bg-gray-800 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
{{ if $avatar }}
<a href="{{ $link }}" class="block relative h-64 overflow-hidden">
<a href="{{ $link }}" class="block relative h-64 overflow-hidden" aria-label="{{ $profile.title }}">
{{ if $avatar }}
{{ $image := $avatar.Fill "400x400 Center webp" }}
<img src="{{ $image.RelPermalink }}"
alt="{{ $profile.title }}"
@@ -168,11 +289,15 @@
loading="lazy"
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110">
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{{ else }}
<div class="absolute inset-0 bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-4xl font-semibold text-gray-600 dark:text-gray-200">
{{ $initial }}
</div>
{{ end }}
</a>
{{ end }}
<div class="p-6">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-1">
<a href="{{ $link }}" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">
<a href="{{ $link }}" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors" aria-label="{{ $profile.title }}">
{{ $profile.title }}
</a>
</h3>
@@ -192,8 +317,10 @@
{{ if and $show_interests $profile.interests }}
<div class="mb-4">
<div class="flex flex-wrap gap-1">
{{ range first 3 $profile.interests }}
<span class="inline-block bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs px-2 py-1 rounded">{{ . }}</span>
{{ if gt $max_interests_int 0 }}
{{ range first $max_interests_int $profile.interests }}
<span class="inline-block bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs px-2 py-1 rounded">{{ . }}</span>
{{ end }}
{{ end }}
</div>
</div>
@@ -225,7 +352,7 @@
{{/* Call to Action */}}
{{ with $block.content.cta }}
<div class="mt-12 text-center">
<div class="mt-12 {{ $ctaAlignClass }}">
<a href="{{ .url }}" class="inline-flex items-center px-6 py-3 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg shadow-lg hover:shadow-xl transition-all duration-300">
{{ .text }}
{{ with .icon }}
+58 -4
View File
@@ -32,11 +32,42 @@
},
"user_groups": {
"type": "array",
"description": "Filter to authors that belong to these user groups (defaults to all authors)",
"description": "Filter to authors that belong to these user groups (defaults to all authors). Accepts strings or objects { name, sort_by?, sort_ascending? } for per-group sorting.",
"items": {
"type": "string"
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"sort_by": {
"type": "string",
"description": "Per-group override of sort field"
},
"sort_ascending": {
"type": "boolean",
"description": "Per-group override of sort direction"
}
},
"required": ["name"],
"additionalProperties": false
}
]
},
"examples": [["Principal Investigators", "PhD Students"]]
"examples": [
["Principal Investigators", "PhD Students"],
[{ "name": "Alumni", "sort_by": "graduation_year", "sort_ascending": false }, "Research Engineers"]
]
},
"sort_by": {
"type": "string",
"description": "Field to sort authors by (e.g. 'name_family', 'graduation_year', 'weight'); the legacy 'Params.' prefix is optional for compatibility",
"default": "name_family"
},
"sort_ascending": {
"type": "boolean",
"description": "Sort direction (true for ascending, false for descending)",
"default": true
},
"cta": {
"type": "object",
@@ -73,7 +104,7 @@
"show_organizations": {
"type": "boolean",
"description": "Display the first organization/affiliation per profile",
"default": false
"default": true
},
"show_interests": {
"type": "boolean",
@@ -84,6 +115,29 @@
"type": "boolean",
"description": "Show social media icons pulled from the author profile links",
"default": true
},
"max_interests": {
"type": "integer",
"description": "Maximum number of interests to display per profile (0 hides interests; default 3)",
"default": 3,
"minimum": 0
},
"align": {
"type": "string",
"description": "Align the section header and CTA",
"enum": ["center", "left"],
"default": "center"
},
"max_columns": {
"type": "integer",
"description": "Maximum number of columns on wide screens (2-4)",
"enum": [2, 3, 4],
"default": 4
},
"show_empty_groups": {
"type": "boolean",
"description": "If true, show an empty-state message for groups with no matching authors",
"default": false
}
},
"additionalProperties": true
@@ -85,6 +85,9 @@ Output (dict):
{{- $skills := $data.skills -}}
{{- $languages := $data.languages -}}
{{- $awards := $data.awards -}}
{{- $graduationYear := $data.graduation_year -}}
{{- $weight := $data.weight -}}
{{- $nameFamily := $data.name.family | default $family -}}
{{- $postnominals := slice -}}
{{- if $data.postnominals -}}
{{- if (reflect.IsSlice $data.postnominals) -}}
@@ -103,6 +106,7 @@ Output (dict):
"name_display" $display
"name_given" $given
"name_family" $family
"name_family_pref" $nameFamily
"name_middle" $middle
"name_alternate" $alternate
"name_pronunciation" $namePronunciation
@@ -120,6 +124,8 @@ Output (dict):
"skills" $skills
"languages" $languages
"awards" $awards
"graduation_year" $graduationYear
"weight" $weight
"postnominals" $postnominals
"is_owner" $isOwner
"has_data" (not (eq $hasData nil))