diff --git a/modules/blox/blox/team-showcase/README.md b/modules/blox/blox/team-showcase/README.md index 9dc8d2af..e1f82064 100644 --- a/modules/blox/blox/team-showcase/README.md +++ b/modules/blox/blox/team-showcase/README.md @@ -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/.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/.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/.*`, ~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 diff --git a/modules/blox/blox/team-showcase/block.html b/modules/blox/blox/team-showcase/block.html index a694e89b..2dbee566 100644 --- a/modules/blox/blox/team-showcase/block.html +++ b/modules/blox/blox/team-showcase/block.html @@ -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" }}
{{/* Section Header */}} -
+
{{ with $title }}

{{ . }} @@ -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) }}

- {{ $group | markdownify }} + {{ $groupName | markdownify }}

{{ end }} -
- {{ range $profile := $groupProfiles }} + {{ if or (gt (len $groupProfiles) 0) $show_empty_groups }} +
+ {{ if eq (len $groupProfiles) 0 }} + {{ if $show_empty_groups }} +
{{ i18n "no_entries" | default "No entries yet." }}
+ {{ 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) }}
- {{ if $avatar }} - - {{ $image := $avatar.Fill "400x400 Center webp" }} - {{ $profile.title }} -
+
+ {{ if $avatar }} + {{ $image := $avatar.Fill "400x400 Center webp" }} + {{ $profile.title }} +
+ {{ else }} +
+ {{ $initial }} +
+ {{ end }}
- {{ end }}

- + {{ $profile.title }}

{{ if $show_role }}
- {{ with $profile.role }}

{{ . }}

{{ end }} + {{ $roleText := "" }} + {{ with $profile.role }}{{ $roleText = . }}{{ end }} + {{ if and (not $roleText) $profile.affiliations }} + {{ range first 1 $profile.affiliations }} + {{ $roleText = .name }} + {{ end }} + {{ end }} + {{ with $roleText }}

{{ . }}

{{ end }} {{ if and $show_organizations $profile.affiliations }} {{ range first 1 $profile.affiliations }}

{{ .name }}

@@ -112,10 +199,12 @@ {{ if and $show_interests $profile.interests }}
- {{ range first 3 $profile.interests }} - - {{ . }} - + {{ if gt $max_interests_int 0 }} + {{ range first $max_interests_int $profile.interests }} + + {{ . }} + + {{ end }} {{ end }}
@@ -149,17 +238,49 @@
{{ end }}
+ {{ end }} {{ end }} {{ else }} - {{ $allProfiles = sort $allProfiles "title" }} -
- {{ 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 }} +
+ {{ 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) }}
- {{ if $avatar }} - + + {{ if $avatar }} {{ $image := $avatar.Fill "400x400 Center webp" }} {{ $profile.title }}
+ {{ else }} +
+ {{ $initial }} +
+ {{ end }}
- {{ end }}

- + {{ $profile.title }}

@@ -192,8 +317,10 @@ {{ if and $show_interests $profile.interests }}
- {{ range first 3 $profile.interests }} - {{ . }} + {{ if gt $max_interests_int 0 }} + {{ range first $max_interests_int $profile.interests }} + {{ . }} + {{ end }} {{ end }}
@@ -225,7 +352,7 @@ {{/* Call to Action */}} {{ with $block.content.cta }} -
+
{{ .text }} {{ with .icon }} diff --git a/modules/blox/blox/team-showcase/schema.json b/modules/blox/blox/team-showcase/schema.json index 7beecf73..45d78476 100644 --- a/modules/blox/blox/team-showcase/schema.json +++ b/modules/blox/blox/team-showcase/schema.json @@ -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 diff --git a/modules/blox/layouts/_partials/functions/get_author_profile.html b/modules/blox/layouts/_partials/functions/get_author_profile.html index 6686204a..5314c269 100644 --- a/modules/blox/layouts/_partials/functions/get_author_profile.html +++ b/modules/blox/layouts/_partials/functions/get_author_profile.html @@ -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))