feat: harden block inputs (batch 1)

Hardened 1st batch of five homepage blocks by normalizing config types, clamping unsafe values, and guarding bad inputs so user‑supplied params fail safe without template errors.

Changes

coerce_bool.html and coerce_int.html: added shared coercion helpers for consistent, safe defaults across blocks.

block.html: sanitized content/design inputs, robust user_groups parsing, safer role/affiliation/interest/link handling, and CTA URL validation.

block.html: guarded avatar/banner/name/headings/bio, safe
button/link resolution, and resilient parsing for affiliations, education, interests, and social links.

block.html: hardened filters (kinds/folders/tags), safer sort/order handling, clamped numeric params, and archive link safety/fallbacks.

block.html and block.html: defensive content rendering, clamped overlay opacity, and safe button link handling.

schema.json, schema.json, schema.json: added view enum entries, added new_tab, and Biome formatting applied.
This commit is contained in:
George Cushen committed 2026-01-07 18:23:51 +00:00
1 parent 3651909ac2
commit 98f0d201c1
10 files changed
+995 -305

No files matched your search

+204 -52
View File
@@ -5,14 +5,21 @@
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $view := $block.design.view | default "card" }}
{{ $content := $block.content }}
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
{{ $design := $block.design }}
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
{{ $items_offset := $block.content.offset | default 0 }}
{{ $items_count := $block.content.count }}
{{ $view := "card" }}
{{ $view_raw := index $design "view" }}
{{ if eq (printf "%T" $view_raw) "string" }}
{{ $view = lower (strings.TrimSpace $view_raw) }}
{{ end }}
{{ $items_offset := partial "functions/coerce_int" (dict "value" (index $content "offset") "default" 0 "min" 0) }}
{{ $items_count := partial "functions/coerce_int" (dict "value" (index $content "count") "default" 5 "min" 0) }}
{{ if eq $items_count 0 }}
{{ $items_count = 65535 }}
{{ else }}
{{ $items_count = $items_count | default 5 }}
{{ end }}
{{/* Query */}}
@@ -20,22 +27,78 @@
{{ $archive_page := "" }}
{{/* Filters */}}
{{ $kinds := $block.content.filters.kinds | default (slice "page") }}
{{ $filters := index $content "filters" }}
{{ if not (reflect.IsMap $filters) }}{{ $filters = dict }}{{ end }}
{{ $kinds := slice }}
{{ $kinds_raw := index $filters "kinds" }}
{{ if reflect.IsSlice $kinds_raw }}
{{ range $kinds_raw }}
{{ $kind := strings.TrimSpace (printf "%v" .) }}
{{ if $kind }}
{{ $kinds = $kinds | append $kind }}
{{ end }}
{{ end }}
{{ else if eq (printf "%T" $kinds_raw) "string" }}
{{ $kind := strings.TrimSpace $kinds_raw }}
{{ if $kind }}
{{ $kinds = slice $kind }}
{{ end }}
{{ end }}
{{ if eq (len $kinds) 0 }}
{{ $kinds = slice "page" }}
{{ end }}
{{ $query = where $query "Kind" "in" $kinds }}
{{ if $block.content.page_type }}
{{ $query = where $query "Type" $block.content.page_type }}
{{ $archive_page = site.GetPage "Section" $block.content.page_type }}
{{ $page_type := "" }}
{{ $page_type_raw := index $content "page_type" }}
{{ if eq (printf "%T" $page_type_raw) "string" }}
{{ $page_type = strings.TrimSpace $page_type_raw }}
{{ end }}
{{ if $block.content.filters.folders }}
{{ $folders := $block.content.filters.folders }}
{{ if $page_type }}
{{ $query = where $query "Type" $page_type }}
{{ $archive_page = site.GetPage "Section" $page_type }}
{{ end }}
{{ $folders_raw := index $filters "folders" }}
{{ $folders := slice }}
{{ if reflect.IsSlice $folders_raw }}
{{ range $folders_raw }}
{{ $folder := strings.TrimSpace (printf "%v" .) }}
{{ if $folder }}
{{ $folders = $folders | append $folder }}
{{ end }}
{{ end }}
{{ else if eq (printf "%T" $folders_raw) "string" }}
{{ $folder := strings.TrimSpace $folders_raw }}
{{ if $folder }}
{{ $folders = slice $folder }}
{{ end }}
{{ end }}
{{ if gt (len $folders) 0 }}
{{ $query = where $query "Section" "in" $folders }}
{{/* Init archive page to main folder */}}
{{ $main_folder := index $folders 0 }}
{{ $archive_page = site.GetPage "Section" $main_folder }}
{{ $main_folder := strings.TrimSpace (printf "%v" (index $folders 0)) }}
{{ if $main_folder }}
{{ $archive_page = site.GetPage "Section" $main_folder }}
{{ end }}
{{ end }}
{{ if $block.content.filters.tags }}
{{ $tags := $block.content.filters.tags }}
{{ $tags_raw := index $filters "tags" }}
{{ $tags := slice }}
{{ if reflect.IsSlice $tags_raw }}
{{ range $tags_raw }}
{{ $tag := strings.TrimSpace (printf "%v" .) }}
{{ if $tag }}
{{ $tags = $tags | append $tag }}
{{ end }}
{{ end }}
{{ else if eq (printf "%T" $tags_raw) "string" }}
{{ $tag := strings.TrimSpace $tags_raw }}
{{ if $tag }}
{{ $tags = slice $tag }}
{{ end }}
{{ end }}
{{ if gt (len $tags) 0 }}
{{ $selected := slice }}
{{ range $tags }}
{{ $tag_page := site.GetPage (printf "tags/%s" (urlize .)) }}
@@ -45,47 +108,110 @@
{{ end }}
{{ if gt (len $selected) 0 }}
{{ $query = $query | intersect $selected }}
{{ $archive_page = site.GetPage (printf "tags/%s" (urlize (index $tags 0))) }}
{{ $first_tag := index $tags 0 }}
{{ $archive_page = site.GetPage (printf "tags/%s" (urlize $first_tag)) }}
{{ end }}
{{ end }}
{{ if $block.content.filters.tag }}
{{ $archive_page = site.GetPage (printf "tags/%s" (urlize $block.content.filters.tag)) }}
{{ $query = $query | intersect $archive_page.Pages }}
{{ $tag := "" }}
{{ $tag_raw := index $filters "tag" }}
{{ if eq (printf "%T" $tag_raw) "string" }}
{{ $tag = strings.TrimSpace $tag_raw }}
{{ end }}
{{ if $block.content.filters.category }}
{{ $archive_page = site.GetPage (printf "categories/%s" (urlize $block.content.filters.category)) }}
{{ $query = $query | intersect $archive_page.Pages }}
{{ if $tag }}
{{ with site.GetPage (printf "tags/%s" (urlize $tag)) }}
{{ $archive_page = . }}
{{ $query = $query | intersect .Pages }}
{{ end }}
{{ end }}
{{ if $block.content.filters.publication_type }}
{{ $archive_page = site.GetPage (printf "publication_types/%s" $block.content.filters.publication_type) }}
{{ $query = $query | intersect $archive_page.Pages }}
{{ $category := "" }}
{{ $category_raw := index $filters "category" }}
{{ if eq (printf "%T" $category_raw) "string" }}
{{ $category = strings.TrimSpace $category_raw }}
{{ end }}
{{ if $block.content.filters.exclude_publication_type }}
{{ $query = $query | complement (site.GetPage (printf "publication_types/%s" $block.content.filters.exclude_publication_type)).Pages }}
{{ if $category }}
{{ with site.GetPage (printf "categories/%s" (urlize $category)) }}
{{ $archive_page = . }}
{{ $query = $query | intersect .Pages }}
{{ end }}
{{ end }}
{{ if $block.content.filters.author }}
{{ $archive_page = site.GetPage (printf "authors/%s" (urlize $block.content.filters.author)) }}
{{ $query = $query | intersect $archive_page.Pages }}
{{ $publication_type := "" }}
{{ $publication_type_raw := index $filters "publication_type" }}
{{ if eq (printf "%T" $publication_type_raw) "string" }}
{{ $publication_type = strings.TrimSpace $publication_type_raw }}
{{ end }}
{{ if $block.content.filters.featured_only }}
{{ if $publication_type }}
{{ with site.GetPage (printf "publication_types/%s" $publication_type) }}
{{ $archive_page = . }}
{{ $query = $query | intersect .Pages }}
{{ end }}
{{ end }}
{{ $exclude_publication_type := "" }}
{{ $exclude_publication_type_raw := index $filters "exclude_publication_type" }}
{{ if eq (printf "%T" $exclude_publication_type_raw) "string" }}
{{ $exclude_publication_type = strings.TrimSpace $exclude_publication_type_raw }}
{{ end }}
{{ if $exclude_publication_type }}
{{ with site.GetPage (printf "publication_types/%s" $exclude_publication_type) }}
{{ $query = $query | complement .Pages }}
{{ end }}
{{ end }}
{{ $author_filter := "" }}
{{ $author_filter_raw := index $filters "author" }}
{{ if eq (printf "%T" $author_filter_raw) "string" }}
{{ $author_filter = strings.TrimSpace $author_filter_raw }}
{{ end }}
{{ if $author_filter }}
{{ with site.GetPage (printf "authors/%s" (urlize $author_filter)) }}
{{ $archive_page = . }}
{{ $query = $query | intersect .Pages }}
{{ end }}
{{ end }}
{{ $featured_only := partial "functions/coerce_bool" (dict "value" (index $filters "featured_only") "default" false) }}
{{ if $featured_only }}
{{ $query = where $query "Params.featured" "==" true }}
{{ end }}
{{ if $block.content.filters.exclude_featured }}
{{ $exclude_featured := partial "functions/coerce_bool" (dict "value" (index $filters "exclude_featured") "default" false) }}
{{ if $exclude_featured }}
{{ $query = where $query "Params.featured" "!=" true }}
{{ end }}
{{ if $block.content.filters.exclude_past }}
{{ $exclude_past := partial "functions/coerce_bool" (dict "value" (index $filters "exclude_past") "default" false) }}
{{ if $exclude_past }}
{{ $query = where $query "Date" ">=" now }}
{{ end }}
{{ if $block.content.filters.exclude_future }}
{{ $exclude_future := partial "functions/coerce_bool" (dict "value" (index $filters "exclude_future") "default" false) }}
{{ if $exclude_future }}
{{ $query = where $query "Date" "<" now }}
{{ end }}
{{ $count := len $query }}
{{/* Sort */}}
{{ $sort_by := $block.content.sort_by | default "Date" }}
{{ $sort_by := "Date" }}
{{ $sort_by_raw := index $content "sort_by" }}
{{ if eq (printf "%T" $sort_by_raw) "string" }}
{{ $sort_by_raw = strings.TrimSpace $sort_by_raw }}
{{ if ne $sort_by_raw "" }}
{{ $sort_by = $sort_by_raw }}
{{ end }}
{{ end }}
{{ $sort_by = partial "functions/get_sort_by_parameter" $sort_by }}
{{ $sort_ascending := $block.content.sort_ascending | default (eq $block.content.order "asc") | default false }}
{{ $sort_ascending := false }}
{{ if isset $content "sort_ascending" }}
{{ $sort_ascending = partial "functions/coerce_bool" (dict "value" (index $content "sort_ascending") "default" false) }}
{{ else }}
{{ $order_raw := index $content "order" }}
{{ if eq (printf "%T" $order_raw) "string" }}
{{ if eq (lower (strings.TrimSpace $order_raw)) "asc" }}
{{ $sort_ascending = true }}
{{ end }}
{{ end }}
{{ end }}
{{ $sort_order := cond $sort_ascending "asc" "desc" }}
{{ $query = sort $query $sort_by $sort_order }}
@@ -104,28 +230,32 @@
{{ end }}
{{ end }}
{{ $columns := $block.design.columns | default "2" }}
{{ $columns := partial "functions/coerce_int" (dict "value" (index $design "columns") "default" 2 "min" 1 "max" 4) }}
{{ $title := "" }}
{{ with index $content "title" }}{{ $title = printf "%v" . | emojify | $page.RenderString }}{{ end }}
{{ $text := "" }}
{{ with index $content "text" }}{{ $text = printf "%v" . | emojify | $page.RenderString }}{{ end }}
{{ if $block.content.title }}
{{ if $title }}
<div class="flex flex-col items-center max-w-prose mx-auto gap-3 justify-center px-6 md:px-0">
<div class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">
{{ $block.content.title | emojify | $page.RenderString }}
{{ $title }}
</div>
{{ with $block.content.text }}<p>{{ . | emojify | $page.RenderString }}</p>{{ end }}
{{ with $text }}<p>{{ . }}</p>{{ end }}
</div>
{{ end }}
<div class="flex flex-col items-center px-6">
{{ $config := dict
"columns" ($block.design.columns | default 2)
"columns" $columns
"len" (len $query)
"fill_image" ($block.design.fill_image | default true)
"show_date" ($block.design.show_date | default true)
"show_read_time" ($block.design.show_read_time | default true)
"show_read_more" ($block.design.show_read_more | default true)
"fill_image" (partial "functions/coerce_bool" (dict "value" (index $design "fill_image") "default" true))
"show_date" (partial "functions/coerce_bool" (dict "value" (index $design "show_date") "default" true))
"show_read_time" (partial "functions/coerce_bool" (dict "value" (index $design "show_read_time") "default" true))
"show_read_more" (partial "functions/coerce_bool" (dict "value" (index $design "show_read_more") "default" true))
}}
{{ partial "functions/render_view" (dict "fragment" "start" "page" $block "item" . "view" $view "config" $config) }}
@@ -138,15 +268,30 @@
</div>
{{/* Archive link */}}
{{ $show_archive_link := $block.content.archive.enable | default (gt $count $items_count) }}
{{ $archive := index $content "archive" }}
{{ if not (reflect.IsMap $archive) }}{{ $archive = dict }}{{ end }}
{{ $show_archive_link := gt $count $items_count }}
{{ if isset $archive "enable" }}
{{ $show_archive_link = partial "functions/coerce_bool" (dict "value" (index $archive "enable") "default" false) }}
{{ end }}
{{ if $show_archive_link | and $archive_page }}
{{ $archive_link := "" }}
{{ if $block.content.archive.link }}
{{ $archive_link = $block.content.archive.link | relLangURL }}
{{ else }}
{{ $archive_link_raw := index $archive "link" }}
{{ if eq (printf "%T" $archive_link_raw) "string" }}
{{ $archive_link = strings.TrimSpace $archive_link_raw }}
{{ end }}
{{ if not $archive_link }}
{{ $archive_link = $archive_page.RelPermalink }}
{{ end }}
{{ if $archive_link }}
{{ $scheme := (urls.Parse $archive_link).Scheme }}
{{ if not $scheme }}
{{ if not (hasPrefix $archive_link "#") }}
{{ $archive_link = $archive_link | relLangURL }}
{{ end }}
{{ end }}
{{ end }}
{{/* Localisation */}}
{{ $items_type := $archive_page.Type }}
@@ -161,13 +306,20 @@
{{ $i18n = "more_pages" }}
{{ end }}
{{ $archive_text := $block.content.archive.text | default (i18n $i18n) | default "See all" }}
{{ $archive_text := "" }}
{{ $archive_text_raw := index $archive "text" }}
{{ if eq (printf "%T" $archive_text_raw) "string" }}
{{ $archive_text = strings.TrimSpace $archive_text_raw }}
{{ end }}
{{ if not $archive_text }}
{{ $archive_text = i18n $i18n | default "See all" }}
{{ end }}
<div class="container mx-auto max-w-screen-lg px-8 xl:px-5 pb-5 lg:pb-8">
<div class="mt-10 flex justify-center">
<a
class="relative inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-2 pl-4 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-300"
href="{{ $archive_link }}">
href="{{ $archive_link | safeURL }}">
<span>{{ $archive_text | emojify }}</span>
</a>
</div>
+1 -1
View File
@@ -154,7 +154,7 @@
"properties": {
"view": {
"type": "string",
"enum": ["card", "compact", "showcase", "citation", "list", "masonry"],
"enum": ["card", "compact", "showcase", "citation", "list", "masonry", "article-grid", "date-title-summary", "slides-gallery"],
"description": "Display view type",
"default": "card"
},
+123 -55
View File
@@ -6,14 +6,52 @@
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $title := $block.content.title | emojify | $page.RenderString }}
{{ $text := $block.content.text | emojify | $page.RenderString | safeHTML }}
{{ $card_class := $block.design.card.css_class }}
{{ $card_style := $block.design.card.css_style }}
{{ $content := $block.content }}
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
{{ $design := $block.design }}
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
{{ $card := index $design "card" }}
{{ if not (reflect.IsMap $card) }}{{ $card = dict }}{{ end }}
{{ $title := "" }}
{{ with index $content "title" }}{{ $title = printf "%v" . | emojify | $page.RenderString }}{{ end }}
{{ $text := "" }}
{{ with index $content "text" }}{{ $text = printf "%v" . | emojify | $page.RenderString | safeHTML }}{{ end }}
{{ $card_class := "" }}
{{ $card_class_raw := index $card "css_class" }}
{{ if eq (printf "%T" $card_class_raw) "string" }}
{{ $card_class = $card_class_raw }}
{{ end }}
{{ $card_style := "" }}
{{ $card_style_raw := index $card "css_style" }}
{{ if eq (printf "%T" $card_style_raw) "string" }}
{{ $card_style = $card_style_raw }}
{{ end }}
{{/* Contrast and accessibility controls */}}
{{ $text_color_mode := $block.design.card.text_color | default "auto" }}
{{ $overlay_opacity := $block.design.card.overlay_opacity | default 0.15 }}
{{ $text_color_mode := "auto" }}
{{ $text_color_raw := index $card "text_color" }}
{{ if eq (printf "%T" $text_color_raw) "string" }}
{{ $text_color_candidate := lower (strings.TrimSpace $text_color_raw) }}
{{ if in (slice "auto" "light" "dark") $text_color_candidate }}
{{ $text_color_mode = $text_color_candidate }}
{{ end }}
{{ end }}
{{ $overlay_opacity := 0.15 }}
{{ $overlay_raw := index $card "overlay_opacity" }}
{{ if ne $overlay_raw nil }}
{{ $overlay_type := printf "%T" $overlay_raw }}
{{ if or (eq $overlay_type "int") (eq $overlay_type "int8") (eq $overlay_type "int16") (eq $overlay_type "int32") (eq $overlay_type "int64") (eq $overlay_type "uint") (eq $overlay_type "uint8") (eq $overlay_type "uint16") (eq $overlay_type "uint32") (eq $overlay_type "uint64") (eq $overlay_type "float32") (eq $overlay_type "float64") }}
{{ $overlay_opacity = float $overlay_raw }}
{{ else if eq $overlay_type "string" }}
{{ $overlay_trimmed := strings.TrimSpace $overlay_raw }}
{{ if gt (len (findRE "^-?[0-9]*\\.?[0-9]+$" $overlay_trimmed)) 0 }}
{{ $overlay_opacity = float $overlay_trimmed }}
{{ end }}
{{ end }}
{{ end }}
{{ if lt $overlay_opacity 0 }}{{ $overlay_opacity = 0 }}{{ end }}
{{ if gt $overlay_opacity 1 }}{{ $overlay_opacity = 1 }}{{ end }}
{{/* Auto-detect text color based on background */}}
{{ $text_classes := "" }}
@@ -27,8 +65,19 @@
{{ else }}
{{/* Auto mode - detect based on gradient colors */}}
{{ $is_dark_bg := false }}
{{ with $block.design.background.gradient.start }}
{{ if or (hasPrefix . "primary-7") (hasPrefix . "primary-8") (hasPrefix . "primary-9") (hasPrefix . "secondary-7") (hasPrefix . "secondary-8") (hasPrefix . "secondary-9") }}
{{ $gradient_start := "" }}
{{ $bg := index $design "background" }}
{{ if and $bg (reflect.IsMap $bg) }}
{{ $gradient := index $bg "gradient" }}
{{ if and $gradient (reflect.IsMap $gradient) }}
{{ $start_raw := index $gradient "start" }}
{{ if eq (printf "%T" $start_raw) "string" }}
{{ $gradient_start = $start_raw }}
{{ end }}
{{ end }}
{{ end }}
{{ if $gradient_start }}
{{ if or (hasPrefix $gradient_start "primary-7") (hasPrefix $gradient_start "primary-8") (hasPrefix $gradient_start "primary-9") (hasPrefix $gradient_start "secondary-7") (hasPrefix $gradient_start "secondary-8") (hasPrefix $gradient_start "secondary-9") }}
{{ $is_dark_bg = true }}
{{ end }}
{{ end }}
@@ -52,59 +101,78 @@
style="{{ $custom_style | safeCSS }}">
{{/* Modern Typography Hierarchy - Responsive text colors */}}
{{ with $title }}
<h2 class="{{ $text_classes }} text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight leading-tight">
{{ $title }}
{{ . }}
</h2>
{{ end }}
{{/* Enhanced Text with Better Spacing - Responsive colors */}}
{{ with $text }}
<div class="{{ $text_classes }}/80 mt-6 text-lg sm:text-xl lg:text-2xl max-w-3xl leading-relaxed font-light">
{{ $text }}
</div>
{{ with $block.content.button }}
{{/* Modern 2025 Button Container */}}
<div class="flex mt-10">
{{ $button_link := .url }}
{{ $button_scheme := (urls.Parse $button_link).Scheme }}
{{ $button_target := "" }}
{{ $new_tab := .new_tab | default false }}
{{ if not $button_scheme }}
{{/* Check if it's a hash fragment (page anchor) */}}
{{ if hasPrefix $button_link "#" }}
{{/* Keep hash fragments as-is */}}
{{ $button_link = $button_link }}
{{ else }}
{{/* Apply relLangURL for relative paths */}}
{{ $button_link = $button_link | relLangURL }}
{{ end }}
{{ else if in (slice "http" "https") $button_scheme }}
{{ $new_tab = true }}
{{ end }}
{{/* Apply new tab if requested or external link */}}
{{ if $new_tab }}
{{ $button_target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
{{/* 2025 Glassmorphism Button */}}
<a href="{{ $button_link | safeURL }}"
{{ $button_target | safeHTMLAttr }}
class="group relative inline-flex items-center gap-3 px-8 py-4 text-lg font-semibold {{ $button_classes }} transition-all duration-300 ease-out hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/50 dark:focus-visible:ring-white/50 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent">
{{/* Button Background with Glassmorphism */}}
<div class="absolute inset-0 bg-white/95 dark:bg-gray-900/95 backdrop-blur-md rounded-2xl ring-1 ring-gray-200/30 dark:ring-white/30 shadow-lg group-hover:bg-white dark:group-hover:bg-gray-800 group-hover:ring-gray-300/50 dark:group-hover:ring-white/50 group-hover:shadow-2xl transition-all duration-300"></div>
{{/* Button Content */}}
<span class="relative z-10">{{ .text }}</span>
{{ with .icon }}
<span class="relative z-10 transition-transform duration-300 group-hover:translate-x-1">
{{ partial "functions/get_icon" (dict "name" . "attributes" "style=\"height: 1.25em\" class='inline-block'") }}
</span>
{{ end }}
{{/* Subtle glow effect */}}
<div class="absolute inset-0 bg-white/20 dark:bg-white/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-xl"></div>
</a>
{{ . }}
</div>
{{ end }}
{{ $button_raw := index $content "button" }}
{{ if and $button_raw (reflect.IsMap $button_raw) }}
{{ $button_text := "" }}
{{ $button_url := "" }}
{{ $button_icon := "" }}
{{ $button_new_tab := partial "functions/coerce_bool" (dict "value" (index $button_raw "new_tab") "default" false) }}
{{ with index $button_raw "text" }}{{ $button_text = printf "%v" . }}{{ end }}
{{ with index $button_raw "url" }}{{ $button_url = printf "%v" . }}{{ end }}
{{ with index $button_raw "icon" }}
{{ if eq (printf "%T" .) "string" }}
{{ $button_icon = . }}
{{ end }}
{{ end }}
{{ $button_text = strings.TrimSpace $button_text }}
{{ $button_url = strings.TrimSpace $button_url }}
{{ if and $button_text $button_url }}
{{/* Modern 2025 Button Container */}}
<div class="flex mt-10">
{{ $button_link := $button_url }}
{{ $button_scheme := (urls.Parse $button_link).Scheme }}
{{ $button_target := "" }}
{{ if not $button_scheme }}
{{/* Check if it's a hash fragment (page anchor) */}}
{{ if not (hasPrefix $button_link "#") }}
{{/* Apply relLangURL for relative paths */}}
{{ $button_link = $button_link | relLangURL }}
{{ end }}
{{ if eq (path.Ext $button_link) ".pdf" }}
{{ $button_new_tab = true }}
{{ end }}
{{ else if in (slice "http" "https") $button_scheme }}
{{ $button_new_tab = true }}
{{ end }}
{{/* Apply new tab if requested or external link */}}
{{ if $button_new_tab }}
{{ $button_target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
{{/* 2025 Glassmorphism Button */}}
<a href="{{ $button_link | safeURL }}"
{{ $button_target | safeHTMLAttr }}
class="group relative inline-flex items-center gap-3 px-8 py-4 text-lg font-semibold {{ $button_classes }} transition-all duration-300 ease-out hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/50 dark:focus-visible:ring-white/50 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent">
{{/* Button Background with Glassmorphism */}}
<div class="absolute inset-0 bg-white/95 dark:bg-gray-900/95 backdrop-blur-md rounded-2xl ring-1 ring-gray-200/30 dark:ring-white/30 shadow-lg group-hover:bg-white dark:group-hover:bg-gray-800 group-hover:ring-gray-300/50 dark:group-hover:ring-white/50 group-hover:shadow-2xl transition-all duration-300"></div>
{{/* Button Content */}}
<span class="relative z-10">{{ $button_text }}</span>
{{ if $button_icon }}
<span class="relative z-10 transition-transform duration-300 group-hover:translate-x-1">
{{ partial "functions/get_icon" (dict "name" $button_icon "attributes" "style=\"height: 1.25em\" class='inline-block'") }}
</span>
{{ end }}
{{/* Subtle glow effect */}}
<div class="absolute inset-0 bg-white/20 dark:bg-white/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur-xl"></div>
</a>
</div>
{{ end }}
{{ end }}
</div>
+5
View File
@@ -41,6 +41,11 @@
"icon": {
"type": "string",
"description": "Optional button icon (icon pack format: pack/icon-name)"
},
"new_tab": {
"type": "boolean",
"description": "Open the button link in a new tab",
"default": false
}
},
"required": ["url", "text"],
+9 -3
View File
@@ -5,14 +5,20 @@
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $title := $block.content.title | emojify | $page.RenderString }}
{{ $text := $block.content.text | emojify | $page.RenderString | safeHTML }}
{{ $content := $block.content }}
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
{{ $title := "" }}
{{ with index $content "title" }}{{ $title = printf "%v" . | emojify | $page.RenderString }}{{ end }}
{{ $text := "" }}
{{ with index $content "text" }}{{ $text = printf "%v" . | emojify | $page.RenderString | safeHTML }}{{ end }}
<div class="flex flex-col items-center max-w-prose mx-auto gap-3 justify-center px-6">
{{ with $title }}
<div class="mb-6 text-3xl font-bold text-gray-900 dark:text-white">
{{ $title }}
{{ . }}
</div>
{{ end }}
{{/* We use DIV rather than P to support `prose` class and multiple paragraphs */}}
{{ with $text }}<div class="prose prose-slate lg:prose-xl dark:prose-invert max-w-prose">{{ . }}</div>{{ end }}
+305 -83
View File
@@ -6,16 +6,45 @@
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $author := $block.content.username | default "me" }}
{{ $content := $block.content }}
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
{{ $design := $block.design }}
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
{{ $author := "me" }}
{{ $authorRaw := index $content "username" }}
{{ if eq (printf "%T" $authorRaw) "string" }}
{{ $author = $authorRaw }}
{{ end }}
{{ $profile := partial "functions/get_author_profile" $author }}
{{ $avatar := $profile.avatar }}
{{ $headings := index $content "headings" }}
{{ if not (reflect.IsMap $headings) }}{{ $headings = dict }}{{ end }}
{{/* Avatar customization parameters */}}
{{ $avatar_size := $block.design.avatar.size | default "large" }}
{{ $avatar_shape := $block.design.avatar.shape | default "circle" }}
{{ $avatar_size := "large" }}
{{ $avatar_shape := "circle" }}
{{ $avatar_config := index $design "avatar" }}
{{ if and $avatar_config (reflect.IsMap $avatar_config) }}
{{ $avatar_size_raw := index $avatar_config "size" }}
{{ if eq (printf "%T" $avatar_size_raw) "string" }}
{{ $avatar_size = $avatar_size_raw }}
{{ end }}
{{ $avatar_shape_raw := index $avatar_config "shape" }}
{{ if eq (printf "%T" $avatar_shape_raw) "string" }}
{{ $avatar_shape = $avatar_shape_raw }}
{{ end }}
{{ end }}
{{/* Profile name typography */}}
{{ $name_size := $block.design.name.size | default "lg" }}
{{ $name_size := "lg" }}
{{ $name_config := index $design "name" }}
{{ if and $name_config (reflect.IsMap $name_config) }}
{{ $name_size_raw := index $name_config "size" }}
{{ if eq (printf "%T" $name_size_raw) "string" }}
{{ $name_size = $name_size_raw }}
{{ end }}
{{ end }}
{{ $name_class_map := dict
"xs" "text-xl sm:text-2xl lg:text-3xl"
"sm" "text-2xl sm:text-3xl lg:text-4xl"
@@ -35,6 +64,24 @@
{{ $shape_classes := dict "circle" "rounded-full" "square" "rounded-none" "rounded" "rounded-lg" }}
{{ $shape_class := index $shape_classes $avatar_shape | default "rounded-full" }}
{{ $banner_filename := "" }}
{{ $banner_config := index $design "banner" }}
{{ if and $banner_config (reflect.IsMap $banner_config) }}
{{ $banner_raw := index $banner_config "filename" }}
{{ if eq (printf "%T" $banner_raw) "string" }}
{{ $banner_filename = $banner_raw }}
{{ end }}
{{ end }}
{{ $bio_style := "" }}
{{ $bio_config := index $design "biography" }}
{{ if and $bio_config (reflect.IsMap $bio_config) }}
{{ $bio_style_raw := index $bio_config "style" }}
{{ if eq (printf "%T" $bio_style_raw) "string" }}
{{ $bio_style = $bio_style_raw }}
{{ end }}
{{ end }}
<div class="resume-biography py-8 px-4 sm:py-12 md:py-16 lg:py-24 xl:py-32 relative overflow-hidden">
{{/* Add subtle top gradient for navbar transition */}}
<div class="max-w-7xl mx-auto">
@@ -45,33 +92,42 @@
{{/* Banner Image */}}
{{ $img := "" }}
{{with $block.design.banner.filename}}
{{- $img = resources.Get (path.Join "media" .) -}}
{{ if $img }}
<div class="relative w-full h-64 rounded-2xl overflow-hidden shadow-xl mb-8">
{{ if ne $img.MediaType.SubType "gif" }}
{{ $responsive := partial "functions/process_responsive_image.html" (dict
"image" $img
"mode" "fill"
"aspect_ratio" "16:9"
"sizes" (slice 768 1024 1366 1920 2560)
) }}
<img class="absolute inset-0 w-full h-full object-cover"
srcset="{{ $responsive.srcset }}"
sizes="100vw"
src="{{ $responsive.fallback.RelPermalink }}"
alt="" />
{{ else }}
{{- $img = $img.Process "webp" -}}
<img class="absolute inset-0 w-full h-full object-cover"
src="{{$img.RelPermalink}}"
alt="" />
{{ end }}
</div>
{{ end }}
{{ if $banner_filename }}
{{- $img = resources.Get (path.Join "media" $banner_filename) -}}
{{ if $img }}
<div class="relative w-full h-64 rounded-2xl overflow-hidden shadow-xl mb-8">
{{ if ne $img.MediaType.SubType "gif" }}
{{ $responsive := partial "functions/process_responsive_image.html" (dict
"image" $img
"mode" "fill"
"aspect_ratio" "16:9"
"sizes" (slice 768 1024 1366 1920 2560)
) }}
<img class="absolute inset-0 w-full h-full object-cover"
srcset="{{ $responsive.srcset }}"
sizes="100vw"
src="{{ $responsive.fallback.RelPermalink }}"
alt="" />
{{ else }}
{{- $img = $img.Process "webp" -}}
<img class="absolute inset-0 w-full h-full object-cover"
src="{{$img.RelPermalink}}"
alt="" />
{{ end }}
</div>
{{ end }}
{{ end }}
{{/* Avatar */}}
{{ $status_icon := "" }}
{{ $status := $profile.status }}
{{ if and $status (reflect.IsMap $status) }}
{{ $status_icon_raw := index $status "icon" }}
{{ if eq (printf "%T" $status_icon_raw) "string" }}
{{ $status_icon = $status_icon_raw }}
{{ end }}
{{ end }}
{{ if $avatar }}
<div class="avatar-wrapper {{ if $img }}-mt-24{{else}}mb-4{{end}}" style="width: {{$display_size}}px; height: {{$display_size}}px;">
{{ $avatar_image := $avatar.Fill (printf "%sx%s Center" $generation_size $generation_size) }}
@@ -81,7 +137,7 @@
width="{{$display_size}}"
height="{{$display_size}}"
style="width: {{$display_size}}px; height: {{$display_size}}px; object-fit: cover;">
{{with $profile.status.icon}}<span class="avatar-emoji">{{.|emojify}}</span>{{end}}
{{ with $status_icon }}<span class="avatar-emoji">{{ . | emojify }}</span>{{ end }}
</div>
{{ end }}
@@ -91,52 +147,141 @@
{{ $profile.title }}
</h1>
{{- with $profile.pronouns -}}
{{ $pronouns := "" }}
{{ with $profile.pronouns }}
{{ if eq (printf "%T" .) "string" }}
{{ $pronouns = . }}
{{ end }}
{{ end }}
{{ with $pronouns }}
<p class="text-lg text-gray-600 dark:text-gray-400">({{ . }})</p>
{{- end -}}
{{ end }}
{{ $role_text := "" }}
{{ with $profile.role }}
{{ if eq (printf "%T" .) "string" }}
{{ $role_text = . }}
{{ end }}
{{ end }}
{{ with $role_text }}
<p class="text-2xl font-semibold text-primary-600 dark:text-primary-400">{{ . | markdownify | emojify }}</p>
{{ end }}
{{ range $profile.affiliations }}
<p class="text-lg text-gray-700 dark:text-gray-300">
{{ with .url }}<a href="{{ . }}" target="_blank" rel="noopener" class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">{{ end }}
{{ .name }}
{{ if .url }}</a>{{ end }}
</p>
{{ $affiliations_raw := $profile.affiliations }}
{{ $affiliations := slice }}
{{ if reflect.IsSlice $affiliations_raw }}
{{ $affiliations = $affiliations_raw }}
{{ else if eq (printf "%T" $affiliations_raw) "string" }}
{{ $affiliations = slice $affiliations_raw }}
{{ end }}
{{ range $affiliations }}
{{ $aff_name := "" }}
{{ $aff_url := "" }}
{{ if reflect.IsMap . }}
{{ $nameRaw := index . "name" | default (index . "title") }}
{{ if ne $nameRaw nil }}
{{ $aff_name = printf "%v" $nameRaw }}
{{ end }}
{{ $urlRaw := index . "url" }}
{{ if eq (printf "%T" $urlRaw) "string" }}
{{ $aff_url = $urlRaw }}
{{ end }}
{{ else if eq (printf "%T" .) "string" }}
{{ $aff_name = . }}
{{ end }}
{{ $aff_name = strings.TrimSpace $aff_name }}
{{ $aff_url = strings.TrimSpace $aff_url }}
{{ if $aff_name }}
<p class="text-lg text-gray-700 dark:text-gray-300">
{{ if $aff_url }}
{{ $link := $aff_url }}
{{ $scheme := (urls.Parse $link).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ if not (hasPrefix $link "#") }}
{{ $link = $link | relLangURL }}
{{ end }}
{{ if eq (path.Ext $link) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }} class="hover:text-primary-600 dark:hover:text-primary-400 transition-colors">{{ $aff_name }}</a>
{{ else }}
{{ $aff_name }}
{{ end }}
</p>
{{ end }}
{{ end }}
</div>
{{/* Social Icons */}}
{{ $links_raw := $profile.links }}
{{ $links := slice }}
{{ if reflect.IsSlice $links_raw }}
{{ $links = $links_raw }}
{{ else if eq (printf "%T" $links_raw) "string" }}
{{ $links = slice $links_raw }}
{{ end }}
{{ if gt (len $links) 0 }}
<div class="flex flex-wrap justify-center gap-4">
{{ range $profile.links }}
{{ $link := .url | default .link }}
{{ $scheme := (urls.Parse $link).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ $link = (.url | default .link) | relLangURL }}
{{ if eq (path.Ext $link) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ range $links }}
{{ $linkUrl := "" }}
{{ $iconName := "hero/link" }}
{{ $linkLabel := "" }}
{{ if reflect.IsMap . }}
{{ $urlRaw := index . "url" | default (index . "link") }}
{{ if eq (printf "%T" $urlRaw) "string" }}
{{ $linkUrl = $urlRaw }}
{{ end }}
{{ $iconRaw := index . "icon" }}
{{ if eq (printf "%T" $iconRaw) "string" }}
{{ $iconName = $iconRaw }}
{{ end }}
{{ $labelRaw := index . "label" }}
{{ if eq (printf "%T" $labelRaw) "string" }}
{{ $linkLabel = $labelRaw }}
{{ end }}
{{ else if eq (printf "%T" .) "string" }}
{{ $linkUrl = . }}
{{ end }}
{{ $linkUrl = strings.TrimSpace $linkUrl }}
{{ if $linkUrl }}
{{ $scheme := (urls.Parse $linkUrl).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ if not (hasPrefix $linkUrl "#") }}
{{ $linkUrl = $linkUrl | relLangURL }}
{{ end }}
{{ if eq (path.Ext $linkUrl) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<a href="{{ $linkUrl | safeURL }}" {{ $target | safeHTMLAttr }}
aria-label="{{ $iconName }}"
{{ if $linkLabel }} title="{{ $linkLabel }}"{{ end }}
class="w-12 h-12 flex items-center justify-center rounded-full bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400
shadow-md hover:shadow-xl hover:scale-110 hover:-translate-y-1
transition-all duration-300 border border-gray-200 dark:border-gray-700">
{{ partial "functions/get_icon" (dict "name" $iconName "attributes" "class=\"w-6 h-6\"") }}
</a>
{{ end }}
<a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }}
aria-label="{{ .icon }}"
{{ with .label }} title="{{.}}"{{ end }}
class="w-12 h-12 flex items-center justify-center rounded-full bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400
shadow-md hover:shadow-xl hover:scale-110 hover:-translate-y-1
transition-all duration-300 border border-gray-200 dark:border-gray-700">
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class=\"w-6 h-6\"") }}
</a>
{{ end }}
</div>
{{ end }}
</div>
{{/* Right Column: Flexible span */}}
<div class="md:col-span-8">
{{/* Bio Section */}}
{{ $bio_content := ($block.content.text | emojify | $page.RenderString) | default $profile.bio }}
{{ $bio_content := "" }}
{{ $bio_override_raw := index $content "text" }}
{{ if ne $bio_override_raw nil }}
{{ $bio_content = printf "%v" $bio_override_raw | emojify | $page.RenderString }}
{{ end }}
{{ if not $bio_content }}
{{ with $profile.bio }}{{ $bio_content = printf "%v" . }}{{ end }}
{{ end }}
{{ with $bio_content }}
<div class="mb-12">
<div class="flex items-center gap-4 mb-8">
@@ -145,11 +290,18 @@
</div>
{{ $i18n_about := i18n "about_me" }}
{{ $about_default := cond (eq $i18n_about "about_me") "Professional Summary" $i18n_about }}
{{ $about_heading := $block.content.headings.about | default $about_default }}
{{ $about_heading := $about_default }}
{{ $about_heading_raw := index $headings "about" }}
{{ if eq (printf "%T" $about_heading_raw) "string" }}
{{ $about_heading_raw = strings.TrimSpace $about_heading_raw }}
{{ if ne $about_heading_raw "" }}
{{ $about_heading = $about_heading_raw }}
{{ end }}
{{ end }}
<h2 class="text-xl sm:text-2xl lg:text-3xl font-bold text-gray-900 dark:text-white tracking-tight">{{ $about_heading }}</h2>
</div>
<div class="prose prose-lg dark:prose-invert text-gray-800 dark:text-gray-200 text-base sm:text-lg leading-relaxed">
<div class="bio-text" {{ with $block.design.biography.style }}{{ (printf "style=\"%s\"" .) | safe.HTMLAttr }}{{end}}>
<div class="bio-text" {{ if $bio_style }}{{ (printf "style=\"%s\"" $bio_style) | safeHTMLAttr }}{{ end }}>
{{ . }}
</div>
</div>
@@ -157,78 +309,148 @@
{{ end }}
{{/* CV Download Button */}}
{{ with $block.content.button }}
<div class="mb-16">
<a href="{{.url}}" target="_blank"
class="inline-flex items-center px-8 py-4 bg-gradient-to-r from-primary-600 to-secondary-600 hover:from-primary-700 hover:to-secondary-700 text-white font-bold text-lg rounded-xl shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-300">
{{ partial "functions/get_icon" (dict "name" "document-arrow-down" "attributes" "class='w-5 h-5 mr-3'") }}
{{.text}}
</a>
</div>
{{ $button_raw := index $content "button" }}
{{ if and $button_raw (reflect.IsMap $button_raw) }}
{{ $button_text := "" }}
{{ $button_url := "" }}
{{ with index $button_raw "text" }}{{ $button_text = printf "%v" . }}{{ end }}
{{ with index $button_raw "url" }}{{ $button_url = printf "%v" . }}{{ end }}
{{ $button_text = strings.TrimSpace $button_text }}
{{ $button_url = strings.TrimSpace $button_url }}
{{ if and $button_text $button_url }}
{{ $button_link := $button_url }}
{{ $scheme := (urls.Parse $button_link).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ if not (hasPrefix $button_link "#") }}
{{ $button_link = $button_link | relLangURL }}
{{ end }}
{{ if eq (path.Ext $button_link) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<div class="mb-16">
<a href="{{ $button_link | safeURL }}" {{ $target | safeHTMLAttr }}
class="inline-flex items-center px-8 py-4 bg-gradient-to-r from-primary-600 to-secondary-600 hover:from-primary-700 hover:to-secondary-700 text-white font-bold text-lg rounded-xl shadow-lg hover:shadow-xl hover:scale-105 transition-all duration-300">
{{ partial "functions/get_icon" (dict "name" "document-arrow-down" "attributes" "class='w-5 h-5 mr-3'") }}
{{ $button_text }}
</a>
</div>
{{ end }}
{{ end }}
{{/* Education Section */}}
{{ with $profile.education }}
{{ $education_raw := $profile.education }}
{{ $education := slice }}
{{ if reflect.IsSlice $education_raw }}
{{ $education = $education_raw }}
{{ end }}
{{ if gt (len $education) 0 }}
<div class="mb-16">
<div class="flex items-center gap-4 mb-8">
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
{{ partial "functions/get_icon" (dict "name" "academic-cap" "attributes" "class='w-6 h-6 text-primary-600 dark:text-primary-400'") }}
</div>
{{ $education_heading := $block.content.headings.education | default (i18n "education") }}
{{ $education_heading := i18n "education" }}
{{ $education_heading_raw := index $headings "education" }}
{{ if eq (printf "%T" $education_heading_raw) "string" }}
{{ $education_heading_raw = strings.TrimSpace $education_heading_raw }}
{{ if ne $education_heading_raw "" }}
{{ $education_heading = $education_heading_raw }}
{{ end }}
{{ end }}
<h3 class="text-xl sm:text-2xl lg:text-3xl font-bold text-gray-900 dark:text-white tracking-tight">{{ $education_heading | markdownify }}</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{{ range . }}
{{ range $education }}
{{ if reflect.IsMap . }}
{{ $degree := "" }}
{{ $degreeRaw := index . "degree" | default (index . "area") }}
{{ if ne $degreeRaw nil }}{{ $degree = printf "%v" $degreeRaw }}{{ end }}
{{ $year := "" }}
{{ with index . "year" }}{{ $year = printf "%v" . }}{{ end }}
{{ $start := "" }}
{{ with index . "start" }}{{ $start = printf "%v" . }}{{ end }}
{{ $end := "" }}
{{ with index . "end" }}{{ $end = printf "%v" . }}{{ end }}
{{ $institution := "" }}
{{ with index . "institution" }}{{ $institution = printf "%v" . }}{{ end }}
{{ $icon := "" }}
{{ $iconRaw := index . "icon" }}
{{ if eq (printf "%T" $iconRaw) "string" }}
{{ $icon = $iconRaw }}
{{ end }}
<div class="group h-full flex flex-col bg-gradient-to-br from-white/90 to-primary-50/30 dark:from-gray-800/90 dark:to-primary-900/20 rounded-xl p-6 shadow-md hover:shadow-xl transition-all duration-300 border border-transparent hover:border-primary-200 dark:hover:border-primary-800 backdrop-blur-md">
<div class="flex gap-4">
<div class="flex-shrink-0 w-12 h-12 bg-white dark:bg-gray-800 ring-1 ring-gray-200 dark:ring-gray-700 rounded-full flex items-center justify-center shadow-md">
{{ if .icon }}
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class='w-6 h-6 text-primary-600 dark:text-primary-400'") }}
{{else}}
{{ if $icon }}
{{ partial "functions/get_icon" (dict "name" $icon "attributes" "class='w-6 h-6 text-primary-600 dark:text-primary-400'") }}
{{ else }}
{{ partial "functions/get_icon" (dict "name" "academic-cap" "attributes" "class='w-6 h-6 text-primary-600 dark:text-primary-400'") }}
{{end}}
{{ end }}
</div>
<div class="flex-1 flex-grow">
{{ $degree := .degree | default .area }}
{{ if $degree }}
<p class="text-xl font-bold text-gray-900 dark:text-white group-hover:text-primary-600 dark:group-hover:text-primary-400 transition-colors">
{{ $degree }}
</p>
{{ if .year }}
<p class="text-gray-500 dark:text-gray-400 text-sm mt-2">{{ .year }}</p>
{{ else if .start }}
{{ end }}
{{ if $year }}
<p class="text-gray-500 dark:text-gray-400 text-sm mt-2">{{ $year }}</p>
{{ else if $start }}
<p class="text-gray-500 dark:text-gray-400 text-sm mt-2">
{{ .start }}
{{ if .end }}<br>{{ .end }}{{ end }}
{{ $start }}
{{ if $end }}<br>{{ $end }}{{ end }}
</p>
{{ end }}
<p class="text-gray-700 dark:text-gray-300 text-base">{{ .institution }}</p>
{{ if $institution }}
<p class="text-gray-700 dark:text-gray-300 text-base">{{ $institution }}</p>
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ end }}
</div>
</div>
{{ end }}
{{/* Interests Section */}}
{{ with $profile.interests }}
{{ $interests_raw := $profile.interests }}
{{ $interests := slice }}
{{ if reflect.IsSlice $interests_raw }}
{{ $interests = $interests_raw }}
{{ else if eq (printf "%T" $interests_raw) "string" }}
{{ $interests = slice $interests_raw }}
{{ end }}
{{ if gt (len $interests) 0 }}
<div>
<div class="flex items-center gap-4 mb-8">
<div class="flex-shrink-0 w-12 h-12 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
{{ partial "functions/get_icon" (dict "name" "sparkles" "attributes" "class='w-6 h-6 text-primary-600 dark:text-primary-400'") }}
</div>
{{ $interests_heading := $block.content.headings.interests | default (i18n "interests") }}
{{ $interests_heading := i18n "interests" }}
{{ $interests_heading_raw := index $headings "interests" }}
{{ if eq (printf "%T" $interests_heading_raw) "string" }}
{{ $interests_heading_raw = strings.TrimSpace $interests_heading_raw }}
{{ if ne $interests_heading_raw "" }}
{{ $interests_heading = $interests_heading_raw }}
{{ end }}
{{ end }}
<h3 class="text-xl font-bold text-gray-900 dark:text-white tracking-tight">{{ $interests_heading | markdownify }}</h3>
</div>
<div class="flex flex-wrap gap-3">
{{ range . }}
{{ range $interests }}
{{ $interest := strings.TrimSpace (printf "%v" .) }}
{{ if $interest }}
<span class="inline-block bg-primary-50 dark:bg-gray-800 text-primary-800 dark:text-primary-200 text-base font-medium px-4 py-2 rounded-full border border-primary-200/50 dark:border-primary-800/50
hover:bg-primary-100
dark:hover:bg-primary-500 dark:hover:text-gray-900 dark:hover:border-primary-500
transition-all duration-200 cursor-default">
{{ . | markdownify | emojify }}
{{ $interest | markdownify | emojify }}
</span>
{{ end }}
{{ end }}
</div>
</div>
{{ end }}
+288 -107
View File
@@ -5,16 +5,38 @@
{{/* Initialise */}}
{{ $page := .wcPage }}
{{ $block := .wcBlock }}
{{ $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 true }}
{{ $max_interests := $block.design.max_interests | default 3 }}
{{ $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 }}
{{ $content := $block.content }}
{{ if not (reflect.IsMap $content) }}{{ $content = dict }}{{ end }}
{{ $design := $block.design }}
{{ if not (reflect.IsMap $design) }}{{ $design = dict }}{{ end }}
{{ $show_social := partial "functions/coerce_bool" (dict "value" (index $design "show_social") "default" true) }}
{{ $show_interests := partial "functions/coerce_bool" (dict "value" (index $design "show_interests") "default" false) }}
{{ $show_role := partial "functions/coerce_bool" (dict "value" (index $design "show_role") "default" true) }}
{{ $show_organizations := partial "functions/coerce_bool" (dict "value" (index $design "show_organizations") "default" true) }}
{{ $show_empty_groups := partial "functions/coerce_bool" (dict "value" (index $design "show_empty_groups") "default" false) }}
{{ $max_interests_int := partial "functions/coerce_int" (dict "value" (index $design "max_interests") "default" 3 "min" 0) }}
{{ $max_columns_int := partial "functions/coerce_int" (dict "value" (index $design "max_columns") "default" 4 "min" 2 "max" 4) }}
{{ $align := "center" }}
{{ $align_raw := index $design "align" }}
{{ if eq (printf "%T" $align_raw) "string" }}
{{ $align_candidate := lower (strings.TrimSpace $align_raw) }}
{{ if in (slice "center" "left") $align_candidate }}
{{ $align = $align_candidate }}
{{ end }}
{{ end }}
{{ $sort_field := "name_family" }}
{{ $sort_field_raw := index $content "sort_by" }}
{{ if eq (printf "%T" $sort_field_raw) "string" }}
{{ $sort_field_raw = strings.TrimSpace $sort_field_raw }}
{{ if ne $sort_field_raw "" }}
{{ $sort_field = $sort_field_raw }}
{{ end }}
{{ end }}
{{ $sort_asc := partial "functions/coerce_bool" (dict "value" (index $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 */}}
@@ -22,36 +44,15 @@
{{ $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 }}
{{ $title := "" }}
{{ with index $content "title" }}{{ $title = printf "%v" . | emojify | $page.RenderString }}{{ end }}
{{ $subtitle := "" }}
{{ with index $content "subtitle" }}{{ $subtitle = printf "%v" . | emojify | $page.RenderString }}{{ end }}
{{ $text := "" }}
{{ with index $content "text" }}{{ $text = printf "%v" . | emojify | $page.RenderString }}{{ end }}
{{ $alignClass := cond (eq $align "left") "items-start text-left" "items-center text-center" }}
{{ $ctaAlignClass := cond (eq $align "left") "text-left" "text-center" }}
{{/* Safe numeric handling with clamping using type checks */}}
{{ $max_columns_int := 4 }}
{{ $max_columns_type := printf "%T" $max_columns }}
{{ if or (eq $max_columns_type "int") (eq $max_columns_type "int64") (eq $max_columns_type "float64") (eq $max_columns_type "float32") (eq $max_columns_type "uint") (eq $max_columns_type "uint64") }}
{{ $max_columns_int = int $max_columns }}
{{ else if eq $max_columns_type "string" }}
{{ if gt (len (findRE "^[0-9]+$" $max_columns)) 0 }}
{{ $max_columns_int = int $max_columns }}
{{ end }}
{{ end }}
{{ if lt $max_columns_int 2 }}{{ $max_columns_int = 2 }}{{ end }}
{{ if gt $max_columns_int 4 }}{{ $max_columns_int = 4 }}{{ end }}
{{ $max_interests_int := 3 }}
{{ $max_interests_type := printf "%T" $max_interests }}
{{ if or (eq $max_interests_type "int") (eq $max_interests_type "int64") (eq $max_interests_type "float64") (eq $max_interests_type "float32") (eq $max_interests_type "uint") (eq $max_interests_type "uint64") }}
{{ $max_interests_int = int $max_interests }}
{{ else if eq $max_interests_type "string" }}
{{ if gt (len (findRE "^[0-9]+$" $max_interests)) 0 }}
{{ $max_interests_int = int $max_interests }}
{{ end }}
{{ end }}
{{ if lt $max_interests_int 0 }}{{ $max_interests_int = 0 }}{{ end }}
{{ $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"
@@ -91,9 +92,17 @@
{{ $allProfiles = $allProfiles | append (dict "profile" $profile "profileData" $profileData) }}
{{ end }}
{{ $groups := $block.content.user_groups }}
{{ $groups_raw := index $content "user_groups" }}
{{ $groups := slice }}
{{ if reflect.IsSlice $groups_raw }}
{{ $groups = $groups_raw }}
{{ else if eq (printf "%T" $groups_raw) "string" }}
{{ $groups = slice $groups_raw }}
{{ else if and $groups_raw (reflect.IsMap $groups_raw) }}
{{ $groups = slice $groups_raw }}
{{ end }}
{{ if $groups }}
{{ if gt (len $groups) 0 }}
{{ range $i, $groupItem := $groups }}
{{ $groupName := "" }}
{{ $groupSortField := $sort_field }}
@@ -101,12 +110,22 @@
{{ $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 }}
{{ $groupNameRaw := index $groupItem "name" | default (index $groupItem "title") }}
{{ if ne $groupNameRaw nil }}
{{ $groupName = printf "%v" $groupNameRaw }}
{{ end }}
{{ $groupSortFieldRaw := index $groupItem "sort_by" }}
{{ if eq (printf "%T" $groupSortFieldRaw) "string" }}
{{ $groupSortFieldRaw = strings.TrimSpace $groupSortFieldRaw }}
{{ if ne $groupSortFieldRaw "" }}
{{ $groupSortField = $groupSortFieldRaw }}
{{ end }}
{{ end }}
{{ $groupSortAsc = partial "functions/coerce_bool" (dict "value" (index $groupItem "sort_ascending") "default" $groupSortAsc) }}
{{ else }}
{{ $groupName = $groupItem }}
{{ $groupName = printf "%v" $groupItem }}
{{ end }}
{{ $groupName = strings.TrimSpace $groupName }}
{{ if eq (trim $groupName " ") "" }}
{{/* Skip groups with empty names to avoid matching everything */}}
{{ continue }}
@@ -122,10 +141,17 @@
{{ $groupProfiles := slice }}
{{ range $allProfiles }}
{{ $profile := .profile }}
{{ $uRaw := $profile.user_groups | default (slice) }}
{{ $u := slice }}
{{ range $uRaw }}
{{ $u = $u | append (printf "%v" .) }}
{{ $uRaw := $profile.user_groups }}
{{ if reflect.IsSlice $uRaw }}
{{ range $uRaw }}
{{ $u = $u | append (printf "%v" .) }}
{{ end }}
{{ else if eq (printf "%T" $uRaw) "string" }}
{{ $trimmedGroup := strings.TrimSpace $uRaw }}
{{ if ne $trimmedGroup "" }}
{{ $u = slice $trimmedGroup }}
{{ end }}
{{ end }}
{{ if gt (len (intersect $u (slice (printf "%v" $groupName)))) 0 }}
{{ $profileData := .profileData }}
@@ -212,37 +238,67 @@
</a>
</h3>
{{ $affiliations_raw := $profile.affiliations }}
{{ $affiliations := slice }}
{{ if reflect.IsSlice $affiliations_raw }}
{{ $affiliations = $affiliations_raw }}
{{ else if eq (printf "%T" $affiliations_raw) "string" }}
{{ $affiliations = slice $affiliations_raw }}
{{ end }}
{{ $first_affiliation := "" }}
{{ if gt (len $affiliations) 0 }}
{{ $aff := index $affiliations 0 }}
{{ if reflect.IsMap $aff }}
{{ $affNameRaw := index $aff "name" | default (index $aff "title") }}
{{ if ne $affNameRaw nil }}
{{ $first_affiliation = printf "%v" $affNameRaw }}
{{ end }}
{{ else if eq (printf "%T" $aff) "string" }}
{{ $first_affiliation = $aff }}
{{ end }}
{{ $first_affiliation = strings.TrimSpace $first_affiliation }}
{{ end }}
{{ if $show_role }}
<div class="text-gray-600 dark:text-gray-400 mb-3">
{{ $roleText := "" }}
{{ with $profile.role }}{{ $roleText = . }}{{ end }}
{{ if and (not $roleText) $profile.affiliations }}
{{ range first 1 $profile.affiliations }}
{{ $roleText = .name }}
{{ with $profile.role }}
{{ if eq (printf "%T" .) "string" }}
{{ $roleText = . }}
{{ end }}
{{ end }}
{{ if and (not $roleText) $first_affiliation }}
{{ $roleText = $first_affiliation }}
{{ 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>
{{ end }}
{{ if and $show_organizations $first_affiliation }}
<p class="text-sm mt-1">{{ $first_affiliation }}</p>
{{ end }}
</div>
{{ end }}
{{ with $profile.bio }}
{{ $bio := "" }}
{{ with $profile.bio }}{{ $bio = printf "%v" . }}{{ end }}
{{ with $bio }}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
{{ . }}
</p>
{{ end }}
{{ if and $show_interests $profile.interests }}
{{ $interests_raw := $profile.interests }}
{{ $interests := slice }}
{{ if reflect.IsSlice $interests_raw }}
{{ $interests = $interests_raw }}
{{ else if eq (printf "%T" $interests_raw) "string" }}
{{ $interests = slice $interests_raw }}
{{ end }}
{{ if and $show_interests (gt (len $interests) 0) }}
<div class="mb-4">
<div class="flex flex-wrap gap-1">
{{ if gt $max_interests_int 0 }}
{{ range first $max_interests_int $profile.interests }}
{{ range first $max_interests_int $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">
{{ . }}
{{ printf "%v" . }}
</span>
{{ end }}
{{ end }}
@@ -250,26 +306,55 @@
</div>
{{ end }}
{{ if and $show_social $profile.links }}
{{ $links_raw := $profile.links }}
{{ $links := slice }}
{{ if reflect.IsSlice $links_raw }}
{{ $links = $links_raw }}
{{ else if eq (printf "%T" $links_raw) "string" }}
{{ $links = slice $links_raw }}
{{ end }}
{{ if and $show_social (gt (len $links) 0) }}
<div class="flex gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
{{ range $profile.links }}
{{ $l := .url | default .link }}
{{ $scheme := (urls.Parse $l).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ $l = (.url | default .link) | relLangURL }}
{{ if eq (path.Ext $l) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<a href="{{ $l | safeURL }}"
{{ $target | safeHTMLAttr }}
aria-label="{{ .icon }}"
{{ with .label }} title="{{ . }}"{{ end }}
class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400 transition-colors">
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class=\"w-5 h-5\"") }}
</a>
{{ range $links }}
{{ $linkUrl := "" }}
{{ $iconName := "hero/link" }}
{{ $linkLabel := "" }}
{{ if reflect.IsMap . }}
{{ $urlRaw := index . "url" | default (index . "link") }}
{{ if eq (printf "%T" $urlRaw) "string" }}
{{ $linkUrl = $urlRaw }}
{{ end }}
{{ $iconRaw := index . "icon" }}
{{ if eq (printf "%T" $iconRaw) "string" }}
{{ $iconName = $iconRaw }}
{{ end }}
{{ $labelRaw := index . "label" }}
{{ if eq (printf "%T" $labelRaw) "string" }}
{{ $linkLabel = $labelRaw }}
{{ end }}
{{ else if eq (printf "%T" .) "string" }}
{{ $linkUrl = . }}
{{ end }}
{{ $linkUrl = strings.TrimSpace $linkUrl }}
{{ if $linkUrl }}
{{ $scheme := (urls.Parse $linkUrl).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ if not (hasPrefix $linkUrl "#") }}
{{ $linkUrl = $linkUrl | relLangURL }}
{{ end }}
{{ if eq (path.Ext $linkUrl) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<a href="{{ $linkUrl | safeURL }}"
{{ $target | safeHTMLAttr }}
aria-label="{{ $iconName }}"
{{ if $linkLabel }} title="{{ $linkLabel }}"{{ end }}
class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400 transition-colors">
{{ partial "functions/get_icon" (dict "name" $iconName "attributes" "class=\"w-5 h-5\"") }}
</a>
{{ end }}
{{ end }}
</div>
{{ end }}
@@ -347,45 +432,115 @@
{{ $profile.title }}
</a>
</h3>
{{ $affiliations_raw := $profile.affiliations }}
{{ $affiliations := slice }}
{{ if reflect.IsSlice $affiliations_raw }}
{{ $affiliations = $affiliations_raw }}
{{ else if eq (printf "%T" $affiliations_raw) "string" }}
{{ $affiliations = slice $affiliations_raw }}
{{ end }}
{{ $first_affiliation := "" }}
{{ if gt (len $affiliations) 0 }}
{{ $aff := index $affiliations 0 }}
{{ if reflect.IsMap $aff }}
{{ $affNameRaw := index $aff "name" | default (index $aff "title") }}
{{ if ne $affNameRaw nil }}
{{ $first_affiliation = printf "%v" $affNameRaw }}
{{ end }}
{{ else if eq (printf "%T" $aff) "string" }}
{{ $first_affiliation = $aff }}
{{ end }}
{{ $first_affiliation = strings.TrimSpace $first_affiliation }}
{{ end }}
{{ if $show_role }}
<div class="text-gray-600 dark:text-gray-400 mb-3">
{{ with $profile.role }}<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>
{{ $roleText := "" }}
{{ with $profile.role }}
{{ if eq (printf "%T" .) "string" }}
{{ $roleText = . }}
{{ end }}
{{ end }}
{{ if and (not $roleText) $first_affiliation }}
{{ $roleText = $first_affiliation }}
{{ end }}
{{ with $roleText }}<p class="font-medium">{{ . }}</p>{{ end }}
{{ if and $show_organizations $first_affiliation }}
<p class="text-sm mt-1">{{ $first_affiliation }}</p>
{{ end }}
</div>
{{ end }}
{{ with $profile.bio }}
{{ $bio := "" }}
{{ with $profile.bio }}{{ $bio = printf "%v" . }}{{ end }}
{{ with $bio }}
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">{{ . }}</p>
{{ end }}
{{ if and $show_interests $profile.interests }}
{{ $interests_raw := $profile.interests }}
{{ $interests := slice }}
{{ if reflect.IsSlice $interests_raw }}
{{ $interests = $interests_raw }}
{{ else if eq (printf "%T" $interests_raw) "string" }}
{{ $interests = slice $interests_raw }}
{{ end }}
{{ if and $show_interests (gt (len $interests) 0) }}
<div class="mb-4">
<div class="flex flex-wrap gap-1">
{{ 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>
{{ range first $max_interests_int $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">{{ printf "%v" . }}</span>
{{ end }}
{{ end }}
</div>
</div>
{{ end }}
{{ if and $show_social $profile.links }}
{{ $links_raw := $profile.links }}
{{ $links := slice }}
{{ if reflect.IsSlice $links_raw }}
{{ $links = $links_raw }}
{{ else if eq (printf "%T" $links_raw) "string" }}
{{ $links = slice $links_raw }}
{{ end }}
{{ if and $show_social (gt (len $links) 0) }}
<div class="flex gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
{{ range $profile.links }}
{{ $l := .url | default .link }}
{{ $scheme := (urls.Parse $l).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ $l = (.url | default .link) | relLangURL }}
{{ if eq (path.Ext $l) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<a href="{{ $l | safeURL }}" {{ $target | safeHTMLAttr }} aria-label="{{ .icon }}" {{ with .label }} title="{{ . }}"{{ end }} class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400 transition-colors">
{{ partial "functions/get_icon" (dict "name" .icon "attributes" "class=\"w-5 h-5\"") }}
</a>
{{ range $links }}
{{ $linkUrl := "" }}
{{ $iconName := "hero/link" }}
{{ $linkLabel := "" }}
{{ if reflect.IsMap . }}
{{ $urlRaw := index . "url" | default (index . "link") }}
{{ if eq (printf "%T" $urlRaw) "string" }}
{{ $linkUrl = $urlRaw }}
{{ end }}
{{ $iconRaw := index . "icon" }}
{{ if eq (printf "%T" $iconRaw) "string" }}
{{ $iconName = $iconRaw }}
{{ end }}
{{ $labelRaw := index . "label" }}
{{ if eq (printf "%T" $labelRaw) "string" }}
{{ $linkLabel = $labelRaw }}
{{ end }}
{{ else if eq (printf "%T" .) "string" }}
{{ $linkUrl = . }}
{{ end }}
{{ $linkUrl = strings.TrimSpace $linkUrl }}
{{ if $linkUrl }}
{{ $scheme := (urls.Parse $linkUrl).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ if not (hasPrefix $linkUrl "#") }}
{{ $linkUrl = $linkUrl | relLangURL }}
{{ end }}
{{ if eq (path.Ext $linkUrl) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<a href="{{ $linkUrl | safeURL }}" {{ $target | safeHTMLAttr }} aria-label="{{ $iconName }}" {{ if $linkLabel }} title="{{ $linkLabel }}"{{ end }} class="text-gray-500 hover:text-primary-600 dark:text-gray-400 dark:hover:text-primary-400 transition-colors">
{{ partial "functions/get_icon" (dict "name" $iconName "attributes" "class=\"w-5 h-5\"") }}
</a>
{{ end }}
{{ end }}
</div>
{{ end }}
@@ -397,15 +552,41 @@
{{ end }}
{{/* Call to Action */}}
{{ with $block.content.cta }}
<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 }}
{{ partial "functions/get_icon" (dict "name" . "attributes" "class=\"ml-2 w-5 h-5\"") }}
{{ $cta_raw := index $content "cta" }}
{{ if and $cta_raw (reflect.IsMap $cta_raw) }}
{{ $cta_text := "" }}
{{ $cta_url := "" }}
{{ $cta_icon := "" }}
{{ with index $cta_raw "text" }}{{ $cta_text = printf "%v" . }}{{ end }}
{{ with index $cta_raw "url" }}{{ $cta_url = printf "%v" . }}{{ end }}
{{ with index $cta_raw "icon" }}
{{ if eq (printf "%T" .) "string" }}
{{ $cta_icon = . }}
{{ end }}
</a>
</div>
{{ end }}
{{ $cta_text = strings.TrimSpace $cta_text }}
{{ $cta_url = strings.TrimSpace $cta_url }}
{{ if and $cta_text $cta_url }}
{{ $cta_link := $cta_url }}
{{ $scheme := (urls.Parse $cta_link).Scheme }}
{{ $target := "" }}
{{ if not $scheme }}
{{ if not (hasPrefix $cta_link "#") }}
{{ $cta_link = $cta_link | relLangURL }}
{{ end }}
{{ if eq (path.Ext $cta_link) ".pdf" }}{{ $target = "target=\"_blank\" rel=\"noopener\"" }}{{ end }}
{{ else if in (slice "http" "https") $scheme }}
{{ $target = "target=\"_blank\" rel=\"noopener\"" }}
{{ end }}
<div class="mt-12 {{ $ctaAlignClass }}">
<a href="{{ $cta_link | safeURL }}" {{ $target | safeHTMLAttr }} 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">
{{ $cta_text }}
{{ if $cta_icon }}
{{ partial "functions/get_icon" (dict "name" $cta_icon "attributes" "class=\"ml-2 w-5 h-5\"") }}
{{ end }}
</a>
</div>
{{ end }}
{{ end }}
</div>
+14 -4
View File
@@ -108,7 +108,7 @@
},
"show_interests": {
"type": "boolean",
"description": "Display up to three interests per profile",
"description": "Display interests per profile (capped by max_interests)",
"default": false
},
"show_social": {
@@ -152,7 +152,13 @@
"title": "Meet the team",
"subtitle": "Research + Engineering",
"text": "We combine safety research with high-velocity engineering.",
"user_groups": ["Principal Investigators", "PhD Students"],
"user_groups": [
"Principal Investigators",
"Research Engineers",
{ "name": "Alumni", "sort_by": "graduation_year", "sort_ascending": false }
],
"sort_by": "name_family",
"sort_ascending": true,
"cta": {
"text": "Join us",
"url": "/apply",
@@ -162,8 +168,12 @@
"design": {
"show_role": true,
"show_organizations": true,
"show_interests": false,
"show_social": true
"show_interests": true,
"max_interests": 3,
"show_social": true,
"align": "left",
"max_columns": 3,
"show_empty_groups": false
}
}
]
@@ -0,0 +1,20 @@
{{ $value := .value }}
{{ $result := .default }}
{{ if ne $value nil }}
{{ $valueType := printf "%T" $value }}
{{ if eq $valueType "bool" }}
{{ $result = $value }}
{{ else if or (eq $valueType "int") (eq $valueType "int8") (eq $valueType "int16") (eq $valueType "int32") (eq $valueType "int64") (eq $valueType "uint") (eq $valueType "uint8") (eq $valueType "uint16") (eq $valueType "uint32") (eq $valueType "uint64") }}
{{ $result = ne (int $value) 0 }}
{{ else if or (eq $valueType "float32") (eq $valueType "float64") }}
{{ $result = ne (float $value) 0 }}
{{ else if eq $valueType "string" }}
{{ $trimmed := lower (strings.TrimSpace $value) }}
{{ if in (slice "1" "true" "yes" "y" "on") $trimmed }}
{{ $result = true }}
{{ else if in (slice "0" "false" "no" "n" "off") $trimmed }}
{{ $result = false }}
{{ end }}
{{ end }}
{{ end }}
{{ return $result }}
@@ -0,0 +1,26 @@
{{ $value := .value }}
{{ $result := .default }}
{{ if ne $value nil }}
{{ $valueType := printf "%T" $value }}
{{ if or (eq $valueType "int") (eq $valueType "int8") (eq $valueType "int16") (eq $valueType "int32") (eq $valueType "int64") (eq $valueType "uint") (eq $valueType "uint8") (eq $valueType "uint16") (eq $valueType "uint32") (eq $valueType "uint64") (eq $valueType "float32") (eq $valueType "float64") }}
{{ $result = int $value }}
{{ else if eq $valueType "string" }}
{{ $trimmed := strings.TrimSpace $value }}
{{ if gt (len (findRE "^-?[0-9]+$" $trimmed)) 0 }}
{{ $result = int $trimmed }}
{{ else if gt (len (findRE "^-?[0-9]*\\.[0-9]+$" $trimmed)) 0 }}
{{ $result = int (float $trimmed) }}
{{ end }}
{{ end }}
{{ end }}
{{ if isset . "min" }}
{{ if lt $result .min }}
{{ $result = .min }}
{{ end }}
{{ end }}
{{ if isset . "max" }}
{{ if gt $result .max }}
{{ $result = .max }}
{{ end }}
{{ end }}
{{ return $result }}